log-to-heatmap.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. #! /usr/bin/env python
  2. import json
  3. import os
  4. import sys
  5. import re
  6. import argparse
  7. from math import floor
  8. from os.path import dirname
  9. class Heatmap(object):
  10. coords = [
  11. [
  12. # Row 0
  13. [ 4, 0], [ 4, 2], [ 2, 0], [ 1, 0], [ 2, 2], [ 3, 0], [ 3, 2],
  14. [ 3, 4], [ 3, 6], [ 2, 4], [ 1, 2], [ 2, 6], [ 4, 4], [ 4, 6],
  15. ],
  16. [
  17. # Row 1
  18. [ 8, 0], [ 8, 2], [ 6, 0], [ 5, 0], [ 6, 2], [ 7, 0], [ 7, 2],
  19. [ 7, 4], [ 7, 6], [ 6, 4], [ 5, 2], [ 6, 6], [ 8, 4], [ 8, 6],
  20. ],
  21. [
  22. # Row 2
  23. [12, 0], [12, 2], [10, 0], [ 9, 0], [10, 2], [11, 0], [ ],
  24. [ ], [11, 2], [10, 4], [ 9, 2], [10, 6], [12, 4], [12, 6],
  25. ],
  26. [
  27. # Row 3
  28. [17, 0], [17, 2], [15, 0], [14, 0], [15, 2], [16, 0], [13, 0],
  29. [13, 2], [16, 2], [15, 4], [14, 2], [15, 6], [17, 4], [17, 6],
  30. ],
  31. [
  32. # Row 4
  33. [20, 0], [20, 2], [19, 0], [18, 0], [19, 2], [], [], [], [],
  34. [19, 4], [18, 2], [19, 6], [20, 4], [20, 6],
  35. ],
  36. [
  37. # Row 5
  38. [ ], [23, 0], [22, 2], [22, 0], [22, 4], [21, 0], [21, 2],
  39. [24, 0], [24, 2], [25, 0], [25, 4], [25, 2], [26, 0], [ ],
  40. ],
  41. ]
  42. def set_attr_at(self, block, n, attr, fn, val):
  43. blk = self.heatmap[block][n]
  44. if attr in blk:
  45. blk[attr] = fn(blk[attr], val)
  46. else:
  47. blk[attr] = fn(None, val)
  48. def coord(self, col, row):
  49. return self.coords[row][col]
  50. @staticmethod
  51. def set_attr(orig, new):
  52. return new
  53. def set_bg(self, (block, n), color):
  54. self.set_attr_at(block, n, "c", self.set_attr, color)
  55. #self.set_attr_at(block, n, "g", self.set_attr, False)
  56. def set_tap_info(self, (block, n), count, cap):
  57. def _set_tap_info(o, _count, _cap):
  58. ns = 4 - o.count ("\n")
  59. return o + "\n" * ns + "%.02f%%" % (float(_count) / float(_cap) * 100)
  60. if not cap:
  61. cap = 1
  62. self.heatmap[block][n + 1] = _set_tap_info (self.heatmap[block][n + 1], count, cap)
  63. @staticmethod
  64. def heatmap_color (v):
  65. colors = [ [0.3, 0.3, 1], [0.3, 1, 0.3], [1, 1, 0.3], [1, 0.3, 0.3]]
  66. fb = 0
  67. if v <= 0:
  68. idx1, idx2 = 0, 0
  69. elif v >= 1:
  70. idx1, idx2 = len(colors) - 1, len(colors) - 1
  71. else:
  72. val = v * (len(colors) - 1)
  73. idx1 = int(floor(val))
  74. idx2 = idx1 + 1
  75. fb = val - float(idx1)
  76. r = (colors[idx2][0] - colors[idx1][0]) * fb + colors[idx1][0]
  77. g = (colors[idx2][1] - colors[idx1][1]) * fb + colors[idx1][1]
  78. b = (colors[idx2][2] - colors[idx1][2]) * fb + colors[idx1][2]
  79. r, g, b = [x * 255 for x in r, g, b]
  80. return "#%02x%02x%02x" % (r, g, b)
  81. def __init__(self, layout):
  82. self.log = {}
  83. self.total = 0
  84. self.max_cnt = 0
  85. self.layout = layout
  86. def update_log(self, (c, r)):
  87. if not (c, r) in self.log:
  88. self.log[(c, r)] = 0
  89. self.log[(c, r)] = self.log[(c, r)] + 1
  90. self.total = self.total + 1
  91. if self.max_cnt < self.log[(c, r)]:
  92. self.max_cnt = self.log[(c, r)]
  93. def get_heatmap(self):
  94. with open("%s/heatmap-layout.%s.json" % (dirname(sys.argv[0]), self.layout), "r") as f:
  95. self.heatmap = json.load (f)
  96. ## Reset colors
  97. for row in self.coords:
  98. for coord in row:
  99. if coord != []:
  100. self.set_bg (coord, "#d9dae0")
  101. for (c, r) in self.log:
  102. coords = self.coord(c, r)
  103. b, n = coords
  104. cap = self.max_cnt
  105. if cap == 0:
  106. cap = 1
  107. v = float(self.log[(c, r)]) / cap
  108. self.set_bg (coords, self.heatmap_color (v))
  109. self.set_tap_info (coords, self.log[(c, r)], self.total)
  110. return self.heatmap
  111. def get_stats(self):
  112. usage = [
  113. # left hand
  114. [0, 0, 0, 0, 0],
  115. # right hand
  116. [0, 0, 0, 0, 0]
  117. ]
  118. finger_map = [0, 0, 1, 2, 3, 4, 4]
  119. for (c, r) in self.log:
  120. if r == 5: # thumb cluster
  121. if c <= 6: # left side
  122. usage[0][4] = usage[0][4] + self.log[(c, r)]
  123. else:
  124. usage[1][4] = usage[1][4] + self.log[(c, r)]
  125. else:
  126. fc = c
  127. hand = 0
  128. if fc >= 7:
  129. fc = fc - 7
  130. hand = 1
  131. fm = finger_map[fc]
  132. usage[hand][fm] = usage[hand][fm] + self.log[(c, r)]
  133. hand_usage = [0, 0]
  134. for f in usage[0]:
  135. hand_usage[0] = hand_usage[0] + f
  136. for f in usage[1]:
  137. hand_usage[1] = hand_usage[1] + f
  138. total = self.total
  139. if total == 0:
  140. total = 1
  141. stats = {
  142. "hands": {
  143. "left": {
  144. "usage": float(hand_usage[0]) / total * 100,
  145. "fingers": {
  146. "0 - pinky": 0,
  147. "1 - ring": 0,
  148. "2 - middle": 0,
  149. "3 - index": 0,
  150. "4 - thumb": 0,
  151. }
  152. },
  153. "right": {
  154. "usage": float(hand_usage[1]) / total * 100,
  155. "fingers": {
  156. "0 - thumb": 0,
  157. "1 - index": 0,
  158. "2 - middle": 0,
  159. "3 - ring": 0,
  160. "4 - pinky": 0,
  161. }
  162. },
  163. }
  164. }
  165. hmap = ['left', 'right']
  166. fmap = ['0 - pinky', '1 - ring', '2 - middle', '3 - index', '4 - thumb',
  167. '0 - thumb', '1 - index', '2 - middle', '3 - ring', '4 - pinky']
  168. for hand_idx in range(len(usage)):
  169. hand = usage[hand_idx]
  170. for finger_idx in range(len(hand)):
  171. stats['hands'][hmap[hand_idx]]['fingers'][fmap[finger_idx + hand_idx * 5]] = float(hand[finger_idx]) / total * 100
  172. return stats
  173. def dump_all(out_dir, heatmaps):
  174. for layer in heatmaps.keys():
  175. if len(heatmaps[layer].log) == 0:
  176. continue
  177. with open ("%s/%s.json" % (out_dir, layer), "w") as f:
  178. json.dump(heatmaps[layer].get_heatmap(), f)
  179. print >>sys.stderr, "%s stats:" % (layer)
  180. json.dump (heatmaps[layer].get_stats(), sys.stderr,
  181. indent = 4, sort_keys = True)
  182. print >>sys.stderr, ""
  183. print >>sys.stderr, ""
  184. def main(opts):
  185. heatmaps = {"Dvorak": Heatmap("Dvorak"),
  186. "ADORE": Heatmap("ADORE")
  187. }
  188. cnt = 0
  189. restrict_row = opts.restrict_row
  190. out_dir = opts.outdir
  191. while True:
  192. line = sys.stdin.readline()
  193. if not line:
  194. break
  195. m = re.search ('KL: col=(\d+), row=(\d+), pressed=(\d+), layer=(.*)', line)
  196. if not m:
  197. continue
  198. cnt = cnt + 1
  199. (c, r, l) = (int(m.group (2)), int(m.group (1)), m.group (4))
  200. if restrict_row != -1 and r != restrict_row:
  201. continue
  202. if c in opts.ignore_columns:
  203. continue
  204. heatmaps[l].update_log ((c, r))
  205. if opts.dump_interval != -1 and cnt >= opts.dump_interval:
  206. cnt = 0
  207. dump_all(out_dir, heatmaps)
  208. dump_all (out_dir, heatmaps)
  209. if __name__ == "__main__":
  210. parser = argparse.ArgumentParser (description = "keylog to heatmap processor")
  211. parser.add_argument ('outdir', action = 'store',
  212. help = 'Output directory')
  213. parser.add_argument ('--row', dest = 'restrict_row', action = 'store', type = int,
  214. default = -1, help = 'Restrict processing to this row only')
  215. parser.add_argument ('--dump-interval', dest = 'dump_interval', action = 'store', type = int,
  216. default = 100, help = 'Dump stats and heatmap at every Nth event, -1 for dumping at EOF only')
  217. parser.add_argument ('--ignore-column', dest = 'ignore_columns', action = 'append', type = int,
  218. default = [], help = 'Ignore the specified columns')
  219. args = parser.parse_args()
  220. main(args)