-
Notifications
You must be signed in to change notification settings - Fork 19
/
AutoKey.py
executable file
·465 lines (373 loc) · 16.5 KB
/
AutoKey.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
#!/usr/bin/env python
# encoding: utf-8
'''
AutoKey -- Tool to print 3D blanks, keys and bump-keys
@author: Christian Holler (:decoder)
@license: Creative Commons BY-NC-SA 4.0 (see LICENSE)
http://creativecommons.org/licenses/by-nc-sa/4.0/
@contact: decoder@own-hero.net
'''
# Ensure print() compatibility with Python 3
from __future__ import print_function
import sys
import os
import argparse
import shutil
import subprocess
import re
cv2_available = True
try:
import numpy as np
import cv2
except ImportError:
cv2_available = False
inkscape_autotrace_avail = True
try:
subprocess.check_call(["inkscape", "--version"])
subprocess.check_call(["potrace", "--version"])
except:
inkscape_autotrace_avail = False
__version__ = 1.1
BASE_DIR = os.path.dirname(os.path.realpath(__file__))
BRAND_DIR = os.path.join(BASE_DIR, "branding")
# Isolate globals
BLUE = [255,0,0] # rectangle color
BLACK = [0,0,0] # sure BG
WHITE = [255,255,255] # sure FG
DRAW_BG = {'color' : BLACK, 'val' : 0}
DRAW_FG = {'color' : WHITE, 'val' : 1}
rect = (0,0,1,1)
drawing = False # flag for drawing curves
draw_lines = False # flag for drawing lines
rectangle = False # flag for drawing rect
rect_over = False # flag to check if rect drawn
rect_or_mask = 100 # flag for selecting rect or mask mode
value = DRAW_FG # drawing initialized to FG
thickness = 3 # brush thickness
(img,img2,mask) = (None, None, None)
(at_ct, at_lt, at_cat, at_cs, at_lrt) = (10, 10, 60, 50, 1)
prev_point = None
def isolate(filename, out_filename):
global img,img2,drawing,draw_lines,value,mask,rectangle,rect,rect_or_mask,ix,iy,rect_over,prev_point
# Parts of this code are taken from OpenCV2 grabcut example,
# licensed under BSD License.
#
# Original Code: https://github.com/opencv/opencv/blob/master/samples/python/grabcut.py
# License: https://github.com/opencv/opencv/blob/master/LICENSE
img = cv2.imread(filename)
img2 = img.copy() # a copy of original image
mask = np.zeros(img.shape[:2],dtype = np.uint8) # mask initialized to PR_BG
output = np.zeros(img.shape,np.uint8) # output image to be shown
svg = np.zeros(img.shape,np.uint8)
bwimg = np.zeros(img.shape,np.uint8)
def onmouse(event, x, y, flags, param):
global img,img2,drawing,draw_lines,value,mask,rectangle,rect,rect_or_mask,ix,iy,rect_over,prev_point
# Draw Rectangle
if event == cv2.EVENT_RBUTTONDOWN:
rectangle = True
ix,iy = x,y
elif event == cv2.EVENT_MOUSEMOVE:
if rectangle == True:
img = img2.copy()
cv2.rectangle(img,(ix,iy),(x,y),BLUE,2)
rect = (ix,iy,abs(ix-x),abs(iy-y))
rect_or_mask = 0
elif event == cv2.EVENT_RBUTTONUP:
rectangle = False
rect_over = True
cv2.rectangle(img,(ix,iy),(x,y),BLUE,2)
rect = (ix,iy,abs(ix-x),abs(iy-y))
rect_or_mask = 0
print("Now press the key 'n' a few times until no further change.")
if event == cv2.EVENT_LBUTTONDOWN:
if rect_over == False:
print("Use the right mouse button to draw a rectangle first.")
else:
drawing = True
cv2.circle(img,(x,y),thickness,value['color'],-1)
cv2.circle(mask,(x,y),thickness,value['val'],-1)
elif event == cv2.EVENT_MOUSEMOVE:
if drawing == True:
cv2.circle(img,(x,y),thickness,value['color'],-1)
cv2.circle(mask,(x,y),thickness,value['val'],-1)
elif event == cv2.EVENT_LBUTTONUP:
if drawing == True:
drawing = False
cv2.circle(img,(x,y),thickness,value['color'],-1)
cv2.circle(mask,(x,y),thickness,value['val'],-1)
if draw_lines:
if prev_point is not None:
cv2.line(img,prev_point,(x,y),value['color'],thickness)
cv2.line(mask,prev_point,(x,y),value['val'],thickness)
prev_point = None
else:
prev_point = (x,y)
# input and output windows
cv2.namedWindow('output')
cv2.namedWindow('input')
cv2.setMouseCallback('input',onmouse)
cv2.moveWindow('input',img.shape[1]+10,90)
def update_at_ct(val):
global at_ct
at_ct = val
def update_at_lt(val):
global at_lt
at_lt = val
def update_at_cat(val):
global at_cat
at_cat = val
def update_at_cs(val):
global at_cs
at_cs = val
def update_at_lrt(val):
global at_lrt
at_lrt = val
def update_out():
global res
bar = np.zeros((img.shape[0],5,3),np.uint8)
res = np.hstack((img2,bar,img,bar,output,bar,svg))
cv2.imshow('Traced Profile', res)
def empty():
pass
update_out()
cv2.createTrackbar( "CT", "Traced Profile", at_ct, 10, update_at_ct )
#cv2.createTrackbar( "CAT", "Traced Profile", at_cat, 100, update_at_cat )
#cv2.createTrackbar( "CS", "Traced Profile", at_cs, 100, update_at_cs )
#cv2.createTrackbar( "LT", "Traced Profile", at_lt, 100, update_at_lt )
#cv2.createTrackbar( "LRT", "Traced Profile", at_lrt, 100, update_at_lrt )
while(1):
update_out()
cv2.imshow('input',img)
k = 0xFF & cv2.waitKey(1)
# key bindings
if k == 27: # esc to exit
break
elif k == ord('0'): # BG drawing
print("Mark lock (non-profile) regions with left mouse button.")
value = DRAW_BG
prev_point = None
elif k == ord('1'): # FG drawing
print("Mark keyway (profile) regions with left mouse button.")
value = DRAW_FG
prev_point = None
elif k == ord('l'): # line mode
print("Switched to line mode (p to switch back).")
draw_lines = True
elif k == ord('p'): # line mode
print("Switched to point mode.")
draw_lines = False
elif k == ord('s'): # save image
cv2.imwrite('grabcut_summary.png',res)
cv2.imwrite('grabcut_output.png',bwimg)
subprocess.check_call(["inkscape", "--verb=FitCanvasToDrawing", "--verb=FileSave", "--verb=FileQuit", "tmp.svg"])
shutil.copy("tmp.svg", out_filename)
break
elif k == ord('r'): # reset everything
print("Reset.")
rect = (0,0,1,1)
drawing = False
rectangle = False
rect_or_mask = 100
rect_over = False
value = DRAW_FG
img = img2.copy()
mask = np.zeros(img.shape[:2],dtype = np.uint8) # mask initialized to PR_BG
output = np.zeros(img.shape,np.uint8) # output image to be shown
elif k == ord('n'): # segment the image
print("For finer touchups, mark lock and keyway after pressing keys 0 and 1, then press 'n' again.")
if (rect_or_mask == 0): # grabcut with rect
bgdmodel = np.zeros((1,65),np.float64)
fgdmodel = np.zeros((1,65),np.float64)
cv2.grabCut(img2,mask,rect,bgdmodel,fgdmodel,5,cv2.GC_INIT_WITH_RECT)
rect_or_mask = 1
elif rect_or_mask == 1: # grabcut with mask
bgdmodel = np.zeros((1,65),np.float64)
fgdmodel = np.zeros((1,65),np.float64)
cv2.grabCut(img2,mask,rect,bgdmodel,fgdmodel,5,cv2.GC_INIT_WITH_MASK)
elif k == ord('m'):
cv2.imwrite('tmp.pbm',bwimg)
subprocess.check_call([
"potrace",
"tmp.pbm",
"--tight", "-s",
"-o", "tmp.svg"
])
subprocess.check_call(["inkscape", "-h", str(img.shape[0]), "-b", "white", "-e", "tmp.png", "tmp.svg"])
svg = cv2.imread("tmp.png")
mh = img.shape[0] - svg.shape[0]
mw = img.shape[1] - svg.shape[1]
# FIXME: This is only a temporary hack so we don't crash if the
# rendered SVG is larger than the original picture.
if mh <= 0:
mh = 0
if mw <= 0:
mw = 0
svg = cv2.copyMakeBorder(svg, mh/2, mh/2 + mh % 2, mw/2, mw/2 + mw % 2, cv2.BORDER_CONSTANT, value=(255, 255, 255, 255))
mask2 = np.where((mask==1) + (mask==3),255,0).astype('uint8')
output = cv2.bitwise_and(img2,img2,mask=mask2)
blackMask = np.where((mask==1) + (mask==3),255,0).astype('uint8')
whiteMask = np.where((mask==0) + (mask==2),255,0).astype('uint8')
kernel = np.ones((at_ct,at_ct),np.uint8)
whiteMask = cv2.morphologyEx(whiteMask, cv2.MORPH_CLOSE, kernel)
bwimg = np.zeros(output.shape, np.uint8)
bwimg[blackMask == 255] = 0
bwimg[whiteMask == 255] = 255
output[whiteMask == 255] = (0,0,255)
cv2.destroyAllWindows()
return
def main(argv=None):
'''Command line options.'''
program_name = "AutoKey3D"
program_version = "v%s" % __version__
program_version_string = '%s %s' % (program_name, program_version)
if argv is None:
argv = sys.argv[1:]
# setup argparser
parser = argparse.ArgumentParser()
parser.add_argument('--version', action='version', version=program_version_string)
# Actions
parser.add_argument("--bumpkey", dest="bumpkey", action='store_true', help="Create a bumpkey")
parser.add_argument("--blank", dest="blank", action='store_true', help="Create a key blank")
parser.add_argument("--key", dest="key", help="Create a key with specified combination (comma-separated numbers)", metavar="COMBINATION")
parser.add_argument("--isolate", dest="isolate", help="Interactively isolate profile from raw picture (EXPERIMENTAL)", metavar="FILE")
# Settings
parser.add_argument("--definition", dest="definition", required=False, help="Path to the definition file to use", metavar="FILE")
parser.add_argument("--profile", dest="profile", required=True, help="Path to the profile file to read/write", metavar="FILE")
parser.add_argument("--tolerance", dest="tol", required=False, help="Override tolerance with specified value", metavar="TOL")
parser.add_argument("--branding-model", dest="branding_model", required=False, help="Override model used in branding text", metavar="MODEL")
parser.add_argument("--thin-handle", dest="thin_handle", action='store_true', required=False, help="Use a thin handle suitable for impressioning grips")
parser.add_argument('args', nargs=argparse.REMAINDER)
if len(argv) == 0:
parser.print_help()
return 2
# process options
opts = parser.parse_args(argv)
# Check that one action is specified
actions = [ "bumpkey", "blank", "key", "isolate" ]
haveAction = False
for action in actions:
if getattr(opts, action):
if haveAction:
print("Error: Cannot specify multiple actions at the same time", file=sys.stderr)
return 2
haveAction = True
if not haveAction:
print("Error: Must specify at least one of these actions: %s" % " ".join(actions), file=sys.stderr)
return 2
if opts.isolate:
if not cv2_available:
print("Error: --isolate requires cv2 (python-opencv) and numpy (python-numpy) to be installed.", file=sys.stderr)
return 2
elif not inkscape_autotrace_avail:
print("Error: --isolate requires inkscape and potrace to be installed.", file=sys.stderr)
return 2
if os.path.exists(opts.profile):
print("Error: Refusing to overwrite existing --profile destination file.", file=sys.stderr)
return 2
return isolate(opts.isolate, opts.profile)
if not opts.definition:
print("Error: --definition is required for specified action.", file=sys.stderr)
# Do the key branding
with open(os.path.join(BRAND_DIR, "branding-template.svg"), 'r') as f:
branding = f.read()
model = os.path.basename(opts.definition).replace(".scad", "")
if opts.branding_model:
model = opts.branding_model
# Read system definition
with open(opts.definition, 'r') as f:
definition = f.read()
need_default_keycombcuts = not "module keycombcuts()" in definition
need_default_keytipcuts = not "module keytipcuts()" in definition
# Read profile definition
profile_definition_file = "%s.def" % opts.profile.replace(".svg", "")
with open(profile_definition_file, 'r') as f:
profile_definition = f.read()
khcx_override = "khcx=" in profile_definition
khcz_override = "khcz=" in profile_definition
khcxoff_override = "khcxoff=" in profile_definition
def_tol = None
def_kl = None
# Look for length in system definition for branding
for line in definition.splitlines():
m = re.match("\s*kl\s*=\s*([\d\.]+)\s*;", line)
if m:
def_kl = m.group(1)
next
# Look for tolerance in profile definition for branding
for idx,line in enumerate(profile_definition.splitlines()):
m = re.match("\s*tol\s*=\s*([\d\.]+)\s*;", line)
if m:
def_tol = m.group(1)
def_tol_idx = idx
next
if def_kl is None:
print("Error: Failed to find key length in system definition file")
sys.exit(1)
if def_tol is None:
print("Error: Failed to find key tolerance in system definition file")
sys.exit(1)
if opts.tol:
lines = profile_definition.splitlines()
lines[def_tol_idx] = "tol = %s;" % opts.tol
profile_definition = "\n".join(lines)
def_tol = opts.tol
branding = branding.replace("%model%", model)
branding = branding.replace("%length%", "%s" % def_kl)
branding = branding.replace("%tol%", "%s" % def_tol)
with open(os.path.join(BRAND_DIR, "branding.svg"), 'w') as f:
f.write(branding)
DEVNULL = open(os.devnull, 'w')
subprocess.check_call(["inkscape", "--export-filename", os.path.join(BRAND_DIR, "branding.eps"), os.path.join(BRAND_DIR, "branding.svg"),])
subprocess.check_call(["pstoedit", "-nb", "-dt", "-f", "dxf:-polyaslines", os.path.join(BRAND_DIR, "branding.eps"), os.path.join(BRAND_DIR, "branding.dxf")], stderr=DEVNULL)
# Read base settings
with open(os.path.join(BASE_DIR, "base-settings.scad"), 'r') as f:
baseSettings = f.read()
if khcx_override:
baseSettings = baseSettings.replace("khcx=", "//khcx=")
if khcz_override:
baseSettings = baseSettings.replace("khcz=", "//khcz=")
if khcxoff_override:
baseSettings = baseSettings.replace("khcxoff=", "//khcxoff=")
# Compose real settings
with open(os.path.join(BASE_DIR, "settings.scad"), 'w') as f:
f.write("/* AUTO-GENERATED FILE - DO NOT EDIT */\n\n")
f.write("include <pre-settings.scad>;\n")
if opts.bumpkey:
f.write("bumpkey = true;\n")
else:
f.write("bumpkey = false;\n")
if opts.blank:
f.write("blank = true;\n")
else:
f.write("blank = false;\n")
if opts.key:
combination = opts.key.split(",")
for idx in range(0, len(combination)):
try:
int(combination[idx])
except ValueError:
combination[idx] = '"%s"' % combination[idx]
f.write("combination = [%s];\n" % ",".join(combination))
else:
f.write("combination = 0;\n")
if opts.thin_handle:
f.write("thin_handle = true;\n")
else:
f.write("thin_handle = false;\n")
f.write(profile_definition)
f.write("\n")
f.write(definition)
f.write("\n")
f.write(baseSettings)
f.write("\n")
if need_default_keytipcuts:
f.write("include <includes/default-keytipcuts.scad>;")
f.write("\n")
if need_default_keycombcuts:
f.write("include <includes/default-keycombcuts.scad>;")
f.write("\n")
subprocess.check_call(["inkscape", "--export-filename", os.path.join(BASE_DIR, "profile.eps"), opts.profile])
subprocess.check_call(["pstoedit", "-nb", "-dt", "-f", "dxf:-polyaslines", os.path.join(BASE_DIR, "profile.eps"), os.path.join(BASE_DIR, "profile.dxf")], stderr=DEVNULL)
subprocess.check_call(["openscad", os.path.join(BASE_DIR, "key.scad") ])
if __name__ == "__main__":
sys.exit(main())