From fef2254f8cfc764a67e7532dfd7109071e3f4167 Mon Sep 17 00:00:00 2001 From: iou1name Date: Wed, 21 Mar 2018 11:06:44 -0400 Subject: [PATCH] initial commit --- .gitignore | 8 + README.md | 6 + draw.py | 36 +++++ linedraw.py | 374 ++++++++++++++++++++++++++++++++++++++++++++++ servers/opp.py | 66 ++++++++ startBluetooth.py | 49 ++++++ stream.py | 59 ++++++++ 7 files changed, 598 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 draw.py create mode 100644 linedraw.py create mode 100644 servers/opp.py create mode 100755 startBluetooth.py create mode 100755 stream.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f6a1023 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +__pycache__/ +*/__pycache__/ +*.pyc +*.jpg +*.png +*.svg +*.ngc + diff --git a/README.md b/README.md new file mode 100644 index 0000000..2341537 --- /dev/null +++ b/README.md @@ -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 + diff --git a/draw.py b/draw.py new file mode 100644 index 0000000..106784a --- /dev/null +++ b/draw.py @@ -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) + diff --git a/linedraw.py b/linedraw.py new file mode 100644 index 0000000..4cc0f85 --- /dev/null +++ b/linedraw.py @@ -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 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 = '\n' + for l in lines: + l = ",".join([str(p[0]*0.5)+","+str(p[1]*0.5) for p in l]) + out += '\n' + out += '' + 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 = '\n' + for l in lines: + l = ",".join([str(p[0]*0.5)+","+str(p[1]*0.5) for p in l]) + out += '\n' + out += "" + 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) + diff --git a/servers/opp.py b/servers/opp.py new file mode 100644 index 0000000..57b8e4d --- /dev/null +++ b/servers/opp.py @@ -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) + diff --git a/startBluetooth.py b/startBluetooth.py new file mode 100755 index 0000000..10a5aa8 --- /dev/null +++ b/startBluetooth.py @@ -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()) diff --git a/stream.py b/stream.py new file mode 100755 index 0000000..df6d115 --- /dev/null +++ b/stream.py @@ -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) +