initial commit

This commit is contained in:
iou1name 2018-03-21 11:06:44 -04:00
commit fef2254f8c
7 changed files with 598 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
__pycache__/
*/__pycache__/
*.pyc
*.jpg
*.png
*.svg
*.ngc

6
README.md Normal file
View File

@ -0,0 +1,6 @@
Stupid bullshit to support my drawing machine.
Dependencies: `pyserial, Pillow, nOBEX`
nOBEX: https://github.com/nccgroup/nOBEX
Linedraw: https://github.com/LingDong-/linedraw

36
draw.py Normal file
View File

@ -0,0 +1,36 @@
#!/usr/bin/env python3
"""
Brings together the different components needed to make the drawing
machine chooch.
"""
import os
import linedraw
import stream
def draw(rec_filename):
"""
Takes the filename received from the bluetooth server, vectorizes
it and streams the resulting G-code to the GRBL controller.
"""
print("Vectorizing " + rec_filename + "...")
linedraw.sketch(rec_filename)
gcode_filename = rec_filename.replace("received", "converted")
gcode_filename = os.path.splitext(gcode_filename)[0] + ".ngc"
print("Streaming " + gcode_filename + "...")
stream.stream(gcode_filename)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(
description="Vectorizes the given image and streams the resultant" \
+ "G-code to GRBL.")
parser.add_argument(
"filename",
help="The path to the image to be drawn.")
args = parser.parse_args()
draw(args.filename)

374
linedraw.py Normal file
View File

@ -0,0 +1,374 @@
import os
from random import *
import math
from PIL import Image, ImageDraw, ImageOps
no_cv = True
export_path = "output/out.svg"
draw_contours = True
draw_hatch = False
try:
import numpy as np
import cv2
except:
print("Cannot import numpy/openCV. Switching to NO_CV mode.")
no_cv = True
F_Blur = {
(-2,-2):2,(-1,-2):4,(0,-2):5,(1,-2):4,(2,-2):2,
(-2,-1):4,(-1,-1):9,(0,-1):12,(1,-1):9,(2,-1):4,
(-2,0):5,(-1,0):12,(0,0):15,(1,0):12,(2,0):5,
(-2,1):4,(-1,1):9,(0,1):12,(1,1):9,(2,1):4,
(-2,2):2,(-1,2):4,(0,2):5,(1,2):4,(2,2):2,
}
F_SobelX = {(-1,-1):1,(0,-1):0,(1,-1):-1,(-1,0):2,(0,0):0,(1,0):-2,(-1,1):1,(0,1):0,(1,1):-1}
F_SobelY = {(-1,-1):1,(0,-1):2,(1,-1):1,(-1,0):0,(0,0):0,(1,0):0,(-1,1):-1,(0,1):-2,(1,1):-1}
def appmask(IM,masks):
PX = IM.load()
w,h = IM.size
NPX = {}
for x in range(0,w):
for y in range(0,h):
a = [0]*len(masks)
for i in range(len(masks)):
for p in masks[i].keys():
if 0<x+p[0]<w and 0<y+p[1]<h:
a[i] += PX[x+p[0],y+p[1]] * masks[i][p]
if sum(masks[i].values())!=0:
a[i] = a[i] / sum(masks[i].values())
NPX[x,y]=int(sum([v**2 for v in a])**0.5)
for x in range(0,w):
for y in range(0,h):
PX[x,y] = NPX[x,y]
def distsum(*args):
return sum([ ((args[i][0]-args[i-1][0])**2 + (args[i][1]-args[i-1][1])**2)**0.5 for i in range(1,len(args))])
def sortlines(lines):
print("optimizing stroke sequence...")
clines = lines[:]
slines = [clines.pop(0)]
while clines != []:
x,s,r = None,1000000,False
for l in clines:
d = distsum(l[0],slines[-1][-1])
dr = distsum(l[-1],slines[-1][-1])
if d < s:
x,s,r = l[:],d,False
if dr < s:
x,s,r = l[:],s,True
clines.remove(x)
if r == True:
x = x[::-1]
slines.append(x)
return slines
def auto_canny(img, sigma=0.33):
"""
Automatically determines appropriate upper and lower boundries for the Canny function.
"""
med = np.median(img)
lower = int(max(0, (1.0 - sigma) * med))
upper = int(min(255, (1.0 + sigma) * med))
edges = cv2.Canny(img, lower, upper)
return edges
def find_edges(IM):
print("finding edges...")
no_cv = True
if no_cv:
#appmask(IM,[F_Blur])
appmask(IM,[F_SobelX,F_SobelY])
else:
im = np.array(IM)
im = cv2.GaussianBlur(im,(3,3),0)
#im = cv2.Canny(im,100,200)
im = auto_canny(im)
IM = Image.fromarray(im)
return IM.point(lambda p: p > 128 and 255)
def getdots(IM):
print("getting contour points...")
PX = IM.load()
dots = []
w,h = IM.size
for y in range(h-1):
row = []
for x in range(1,w):
if PX[x,y] == 255:
if len(row) > 0:
if x-row[-1][0] == row[-1][-1]+1:
row[-1] = (row[-1][0],row[-1][-1]+1)
else:
row.append((x,0))
else:
row.append((x,0))
dots.append(row)
return dots
def connectdots(dots):
print("connecting contour points...")
contours = []
for y in range(len(dots)):
for x,v in dots[y]:
if v > -1:
if y == 0:
contours.append([(x,y)])
else:
closest = -1
cdist = 100
for x0,v0 in dots[y-1]:
if abs(x0-x) < cdist:
cdist = abs(x0-x)
closest = x0
if cdist > 3:
contours.append([(x,y)])
else:
found = 0
for i in range(len(contours)):
if contours[i][-1] == (closest,y-1):
contours[i].append((x,y,))
found = 1
break
if found == 0:
contours.append([(x,y)])
for c in contours:
if c[-1][1] < y-1 and len(c)<4:
contours.remove(c)
return contours
def getcontours(IM,sc=2):
print("generating contours...")
IM = find_edges(IM)
IM1 = IM.copy()
IM2 = IM.rotate(-90,expand=True).transpose(Image.FLIP_LEFT_RIGHT)
dots1 = getdots(IM1)
contours1 = connectdots(dots1)
dots2 = getdots(IM2)
contours2 = connectdots(dots2)
for i in range(len(contours2)):
contours2[i] = [(c[1],c[0]) for c in contours2[i]]
contours = contours1+contours2
for i in range(len(contours)):
for j in range(len(contours)):
if len(contours[i]) > 0 and len(contours[j])>0:
if distsum(contours[j][0],contours[i][-1]) < 8:
contours[i] = contours[i]+contours[j]
contours[j] = []
for i in range(len(contours)):
contours[i] = [contours[i][j] for j in range(0,len(contours[i]),8)]
contours = [c for c in contours if len(c) > 1]
for i in range(0,len(contours)):
contours[i] = [(v[0]*sc,v[1]*sc) for v in contours[i]]
for i in range(0,len(contours)):
for j in range(0,len(contours[i])):
# contours[i][j] = int(contours[i][j][0]+10*perlin.noise(i*0.5,j*0.1,1)),int(contours[i][j][1]+10*perlin.noise(i*0.5,j*0.1,2))
contours[i][j] = int(contours[i][j][0]+10),int(contours[i][j][1]+10)
return contours
def hatch(IM,sc=16):
print("hatching...")
PX = IM.load()
w,h = IM.size
lg1 = []
lg2 = []
for x0 in range(w):
for y0 in range(h):
x = x0*sc
y = y0*sc
if PX[x0,y0] > 144:
pass
elif PX[x0,y0] > 64:
lg1.append([(x,y+sc/4),(x+sc,y+sc/4)])
elif PX[x0,y0] > 16:
lg1.append([(x,y+sc/4),(x+sc,y+sc/4)])
lg2.append([(x+sc,y),(x,y+sc)])
else:
lg1.append([(x,y+sc/4),(x+sc,y+sc/4)])
lg1.append([(x,y+sc/2+sc/4),(x+sc,y+sc/2+sc/4)])
lg2.append([(x+sc,y),(x,y+sc)])
lines = [lg1,lg2]
for k in range(0,len(lines)):
for i in range(0,len(lines[k])):
for j in range(0,len(lines[k])):
if lines[k][i] != [] and lines[k][j] != []:
if lines[k][i][-1] == lines[k][j][0]:
lines[k][i] = lines[k][i]+lines[k][j][1:]
lines[k][j] = []
lines[k] = [l for l in lines[k] if len(l) > 0]
lines = lines[0]+lines[1]
for i in range(0,len(lines)):
for j in range(0,len(lines[i])):
print(perlin.noise(i*0.5,j*0.1,1))
#lines[i][j] = int(lines[i][j][0]+sc*perlin.noise(i*0.5,j*0.1,1)),int(lines[i][j][1]+sc*perlin.noise(i*0.5,j*0.1,2))-j
lines[i][j] = int(lines[i][j][0]+sc),int(lines[i][j][1]+sc)-j
return lines
def sketch(path, resolution=1024, hatch_size=16, contour_simplify=2):
image = Image.open(path)
w,h = image.size
image = image.convert("L")
image = ImageOps.autocontrast(image ,10)
lines = []
if draw_contours:
lines += getcontours(image.resize((int(resolution/contour_simplify), int(resolution/contour_simplify*h/w))), contour_simplify)
if draw_hatch:
lines += hatch(image.resize((int(resolution/hatch_size), int(resolution/hatch_size*h/w))), hatch_size)
lines = sortlines(lines)
export_path = path.replace("received", "converted")
export_path = os.path.splitext(export_path)[0]
with open(export_path + ".svg", "w") as file:
file.write(makePathSvg(lines))
with open(export_path + ".ngc", "w") as file:
file.write(makeGcode(lines))
print(len(lines), "strokes.")
print("done.")
return lines
def makePolySvg(lines):
"""
Uses the provided set of contours to generate an SVG file and returns it as a string. Contours get
written as polyline as objects.
"""
print("generating svg file...")
out = '<svg xmlns="http://www.w3.org/2000/svg" version="1.1">\n'
for l in lines:
l = ",".join([str(p[0]*0.5)+","+str(p[1]*0.5) for p in l])
out += '<polyline points="'+l+'" stroke="black" stroke-width="2" fill="none" />\n'
out += '</svg>'
return out
def makePathSvg(lines):
"""
Uses the provided set of contours to generate an SVG file and returns it as a string. Contours get
written as path objects.
"""
print("generating svg file...")
out = '<svg xmlns="http://www.w3.org/2000/svg" version="1.1">\n'
for l in lines:
l = ",".join([str(p[0]*0.5)+","+str(p[1]*0.5) for p in l])
out += '<path d="M'+l+'" stroke="black" stroke-width="2" fill="none" />\n'
out += "</svg>"
return out
def makeGcode(lines, paper_size="letter"):
"""
Converts the provided contour lines into G-code commands and returns them as a string.
"""
paper_sizes = {
"letter": (67.46875, 87.3125),
"ledger": (87.3125, 139.2903226),
"max": (90, 140)}
tot = []
for line in lines:
tot += line
max_x = max([p[0] for p in tot])
max_y = max([p[1] for p in tot])
d_x = paper_sizes[paper_size][0] / max_x
d_y = paper_sizes[paper_size][1] / max_y
scale = min(d_x, d_y)
print("generating gcode file...")
out = "$X\n$32=1\nM03\nF600\nG17 G21 G90 G54\nG01\n\n"
for line in lines:
start = line.pop(0)
out += "S1000\n"
#out += f"X{start[0]*0.5} Y{start[1]*0.5}\n"
out += "X" + str(start[0]*scale) + " Y" + str(start[1]*scale) + "\n"
out += "S0\n"
for point in line:
#out += f"X{point[0]*0.5} Y{point[1]*0.5}\n"
out += "X" + str(point[0]*scale) + " Y" + str(point[1]*scale) + "\n"
out += "\n"
out += "S1000\nX0 Y0\nM2\n"
return out
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(
description="Convert image to vectorized line drawing for plotters.")
parser.add_argument(
'-i',
'--input',
nargs='?',
help='Input path')
parser.add_argument(
'-o',
'--output',
nargs='?',
help='Output path.')
parser.add_argument(
'--no_contour',
action='store_true',
help="Don't draw contours.")
parser.add_argument(
'--hatch',
action='store_true',
help='Enable hatching.')
parser.add_argument(
'--no_cv',
action='store_true',
help="Don't use OpenCV.")
parser.add_argument(
"--resolution",
default=1024,
type=int,
help="Resolution. eg. 512, 1024, 2048")
parser.add_argument(
'--contour_simplify',
default=2,
type=int,
help='Level of contour simplification. eg. 1, 2, 3')
parser.add_argument(
'--hatch_size',
default=16,
type=int,
help='Patch size of hatches. eg. 8, 16, 32')
args = parser.parse_args()
export_path = args.output
draw_hatch = args.hatch
draw_contours = not args.no_contour
no_cv = args.no_cv
sketch(args.input, args.resolution, args.contour_simplify, args.hatch_size)

66
servers/opp.py Normal file
View File

@ -0,0 +1,66 @@
#
# Released as open source by NCC Group Plc - http://www.nccgroup.com/
#
# Developed by Sultan Qasim Khan, Sultan.QasimKhan@nccgroup.trust
#
# http://www.github.com/nccgroup/nOBEX
#
# Released under GPLv3, a full copy of which can be found in COPYING.
#
import os
from nOBEX import headers, responses, server
from draw import draw
class OPPServer(server.Server):
"""OBEX Object Push Profile Server"""
def __init__(self, directory, address=None):
super(OPPServer, self).__init__(address)
self.directory = directory
if not os.path.exists(self.directory):
os.mkdir(self.directory)
def start_service(self, port=None):
return super(OPPServer, self).start_service("opush", port)
def put(self, socket, request):
name = b""
length = 0
body = b""
while True:
for header in request.header_data:
if isinstance(header, headers.Name):
name = header.decode()
print("Receiving %s" % name)
elif isinstance(header, headers.Length):
length = header.decode()
print("Length %i" % length)
elif isinstance(header, headers.Body):
body += header.decode()
elif isinstance(header, headers.End_Of_Body):
body += header.decode()
if request.is_final():
break
# Ask for more data.
self.send_response(socket, responses.Continue())
# Get the next part of the data.
request = self.request_handler.decode(socket)
self.send_response(socket, responses.Success())
name = name.strip("\x00")
name = os.path.split(name)[1]
path = os.path.join(self.directory, name)
print("Writing %s" % repr(path))
# print("Writing %s" % path)
open(path, "wb").write(body)
draw(path)

49
startBluetooth.py Executable file
View File

@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""
Starts the Bluetooth server for the drawing machine. Taken from the
multiserver example in the nOBEX git repo.
"""
import os
import sys
import signal
import traceback
from threading import Thread
from servers.opp import OPPServer
def thread_serve(serv_class, arg):
t = Thread(target=serve, args=(serv_class, arg), daemon=True)
t.start()
return t
def serve(serv_class, *args, **kwargs):
server = serv_class(*args, **kwargs)
socket = server.start_service()
while True:
try:
server.serve(socket)
except:
traceback.print_exc()
def signal_handler(signal, frame):
print() # newline to move ^C onto its own line on display
sys.exit(0)
def main():
opp_conf = "./received"
signal.signal(signal.SIGINT, signal_handler)
# obexd conflicts with our own OBEX servers
os.system("killall obexd")
t = thread_serve(OPPServer, opp_conf)
# wait for completion (never)
t.join()
return 0
if __name__ == "__main__":
sys.exit(main())

59
stream.py Executable file
View File

@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""
Simple script to stream a G-code file to a GRBL controller.
"""
import os
import time
import serial
def stream(filename):
"""Opens the given filename and streams it to GRBL."""
usbs = [dev for dev in os.listdir("/dev/") if dev.startswith("ttyUSB")]
for usb in usbs:
try:
s = serial.Serial(os.path.join("/dev/", usb), 115200)
break
except serial.serialutil.SerialException:
continue
else:
print("Could not find any USB connections. Exiting.")
return
with open(filename, 'r') as file:
data = file.read().splitlines()
len_cmds = len(data)
progress = []
# Wake up grbl
s.write(b"\r\n\r\n")
time.sleep(2) # Wait for grbl to initialize
s.flushInput() # Flush startup text in serial input
for n, line in enumerate(data):
l = line.strip().encode("utf-8") # Strip all EOL characters for consistency
# print('Sending: ' + str(l))
s.write(l + b"\n") # Send g-code block to grbl
grbl_out = s.readline() # Wait for grbl response with carriage return
# print('Receive: ' + str(grbl_out).strip())
percent = int(n/len_cmds)
if not int(n/len_cmds) % 5:
if not percent in progress:
print(str(percent) + "%")
progress.append(percent)
s.close()
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(
description="Streams the G-code file to GRBL.")
parser.add_argument(
"filename",
help="The file to stream.")
args = parser.parse_args()
stream(args.filename)