This commit is contained in:
2019-01-09 18:16:52 -08:00
parent 3b74e57e1d
commit a8d9efb6bb
2 changed files with 240 additions and 155 deletions

View File

@@ -1,3 +1,4 @@
sudo apt-get install python3-pil python3-pil.imagetk
sudo apt install libcairo2-dev pkg-config sudo apt install libcairo2-dev pkg-config
virtualenv -p python3 .env virtualenv -p python3 .env
source .env/bin/activate source .env/bin/activate

394
main.py
View File

@@ -1,212 +1,296 @@
import math from tkinter import Tk, Label, filedialog, Button
from PIL import Image, ImageTk
from svgpathtools import svg2paths, Line, QuadraticBezier, CubicBezier
import cairo, subprocess, bezier, os, math, time
import numpy as np import numpy as np
touch_height = 20
raise_height = 2
head_x_offset = 50
speed = 500
lift_markers = True
PREAMBLE = ''' class GCoder(Tk):
G1 Z20 def __init__(self):
M107 super().__init__()
M190 S0
M104 S0
G28 ; home all axes
G0 F{1}
G1 Z{0}
G1 Z{0}
'''.format(touch_height + raise_height, speed)
FINISH = """ # Setup the file structure
G1 Z{0} F7000 if not os.path.exists("output"):
M104 S0 os.makedirs("output")
G28 X0 Y0
M84
""".format(75)
import cairo, subprocess, bezier, os # Height at which the pen touches and draws on the surface
from svgpathtools import svg2paths, Line, QuadraticBezier, CubicBezier self.touch_height = 20
# How far to raise the pen tip to raise it off the page
self.raise_height = 2
# The inherent offset from true 0 we have from the pen bracket
self.head_x_offset = 50
# XY movement speed
self.speed = 500
# Weather we render lift markers
self.lift_markers = True
# Setup the file structure # X and Y offsets to place the image on A11 paper
if not os.path.exists("output"): self.offset_x = 75 + self.head_x_offset
os.makedirs("output") self.offset_y = 20
# Convert the bmp to a vector svg # Bed dimensions to fit A11 paper
file_name = "geom" self.bed_max_x = 280
self.bed_min_x = self.offset_x
self.bed_max_y = 280
self.bed_min_y = 20
self.started = False
subprocess.call(["mogrify", "-format", "bmp", "input-images/{}.svg".format(file_name)]) self.gcode_preamble = '''
G91 ; Set to relative mode for the initial pen lift
G1 Z20 ; Lift head by 20
G90 ; Set back to absolute position mode
M107 ; Fan off
M190 S0 ; Set bed temp
M104 S0 ; Set nozzle temp
G28 ; home all axes
G0 F{1} ; Set the feed rate
G1 Z{0} ; Move the pen to just above the paper
'''.format(self.touch_height + self.raise_height, self.speed)
subprocess.call(["mkbitmap", "input-images/{}.bmp".format(file_name), "-x", self.gcode_end = '''
"-f", "15", G1 Z{0} F7000 ; Raise the pen high up so we can fit a cap onto it
#"-b", "0", M104 S0 ; Set the nozzle to 0
"-o", "input-images/{}-n.bmp".format(file_name) G28 X0 Y0 ; Home back to (0,0) for (x,y)
]) M84 ; Turn off the motors
'''.format(75)
subprocess.call(["potrace", w, h = 300, 300
"-t", "20",
"-z", "white",
"-b", "svg",
"input-images/{}-n.bmp".format(file_name),
"--rotate", "90",
"-o", "tmp/conversion-output.svg",
])
# read in the svg self.geometry("{}x{}".format(w, h))
paths, attributes = svg2paths("tmp/conversion-output.svg")
gcode = "" self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 300, 300)
gcode += PREAMBLE
started = False self.context = cairo.Context(self.surface)
self.context.scale(1, 1)
scale = 0.0045 self.context.set_line_width(0.4)
offset_x = 75 + head_x_offset
offset_y = 20
# Walk through the paths and create the GCODE self.button = Button(self, text="Select Image", command=self.file_select_callback)
for path in paths: self.button.pack()
previous_x = None self.mainloop()
previous_y = None
# rotated = path.rotated(90) def file_select_callback(self):
filepath = filedialog.askopenfilename(initialdir=".", title="Select file",
filetypes=(("jpeg files", "*.jpg"), ("all files", "*.*")))
if len(filepath) is 0:
return
for part in path: self.context.rectangle(0, 0, 300, 300)
self.context.set_source_rgba(1, 1, 1, 1.0)
self.context.fill()
start = part.start self.context.set_source_rgba(0, 0, 0, 1.0)
end = part.end
start_x = start.real * scale + offset_x filename = os.path.basename(filepath)
start_y = start.imag * scale + offset_y self.convert_image(filename)
self.convert_gcode()
self.render_gcode()
end_x = end.real * scale + offset_x self._image_ref = ImageTk.PhotoImage(
end_y = end.imag * scale + offset_y Image.frombuffer("RGBA", (300, 300), self.surface.get_data().tobytes(), "raw", "BGRA", 0, 1))
self.label = Label(self, image=self._image_ref)
self.label.pack(expand=True, fill="both")
# Check to see if the endpoint of the last cycle continues and wether we need to lift the pen or not def convert_image(self, file_name):
lift = True
if previous_x is not None and previous_y is not None:
if abs(start.real - previous_x) < 30 and abs(start.imag - previous_y) < 30:
lift = False
# if the pen needs to lift, base_name = file_name.split(".")[0]
# if lift:
previous_x = end.real
previous_y = end.imag
if lift: print("Converting input file [{}]".format(file_name))
gcode += "G1 Z{}\n".format(raise_height + touch_height)
else:
gcode += "# NOT LIFTING\n"
if isinstance(part, CubicBezier): print("Running mogrify...")
start = time.time()
subprocess.call(["mogrify", "-format", "bmp", "input-images/{}".format(file_name)])
print("Run took [{:.2f}] seconds".format(time.time() - start))
nodes = np.asfortranarray([ print("Running mkbitmap...")
[start.real, part.control1.real, part.control2.real, end.real], start = time.time()
[start.imag, part.control1.imag, part.control2.imag, end.imag], subprocess.call(["mkbitmap", "input-images/{}.bmp".format(base_name), "-x",
]) "-f", "15",
# "-b", "0",
"-o", "input-images/{}-n.bmp".format(base_name)
])
print("Run took [{:.2f}] seconds".format(time.time() - start))
curve = bezier.Curve.from_nodes(nodes) print("Running potrace...")
start = time.time()
subprocess.call(["potrace",
"-t", "20",
"-z", "white",
"-b", "svg",
"input-images/{}-n.bmp".format(base_name),
"--rotate", "0",
"-o", "tmp/conversion-output.svg",
])
print("Run took [{:.2f}] seconds\n".format(time.time() - start))
evals = [] def render_gcode(self):
pos = np.linspace(0.1, 1, 10)
for i in pos: file = open("output/gcode-output.gcode", "r")
evals.append(curve.evaluate(i))
largest_x = 0
largest_y = 0
smallest_x = 300
smallest_y = 300
x = None
y = None
for line in file:
split = line.split(" ")
command = split[0]
operands = split[1:]
prev_x = x
prev_y = y
if command == "G1":
for operand in operands:
if operand.startswith("X"):
x = float(operand[1:])
if x > largest_x: largest_x = x
if x < smallest_x: smallest_x = x
elif operand.startswith("Y"):
y = float(operand[1:])
if y > largest_y: largest_y = y
if y < smallest_y: smallest_y = y
elif operand.startswith("Z{}".format(self.touch_height + self.raise_height)):
# signify a lift
if prev_x is not None and prev_y is not None and self.lift_markers:
self.context.arc(prev_x - self.head_x_offset, prev_y, 0.5, 0, 2*math.pi)
self.context.stroke()
prev_x = None
prev_y = None
x = None
y = None
if (prev_x != x and prev_x is not None) or (prev_y != y and prev_y is not None):
self.context.line_to(prev_x - self.head_x_offset, prev_y)
self.context.line_to(x - self.head_x_offset, y)
self.context.stroke()
print("Largest X : " + str(largest_x))
print("Smallest X : " + str(smallest_x))
gcode += "G1 X{} Y{}\n".format(start_x, start_y) print("Largest Y : " + str(largest_y))
gcode += "G1 Z{} \n".format(touch_height) print("Smallest Y : " + str(smallest_y))
for i in evals: if largest_x > self.bed_max_x:
x = i[0][0] print("X OVERFLOW")
y = i[1][0] if largest_y > self.bed_max_y:
gcode += "G1 X{} Y{}\n".format(x * scale + offset_x, y * scale + offset_y) print("Y OVERFLOW")
if smallest_x < self.bed_min_x:
print("X_UNDERFLOW")
if smallest_y < self.bed_min_y:
print("Y_UNDERFLOW")
#gcode += "G1 X{} Y{}\n".format(end.real * scale + offset_x, end.imag * scale + offset_y) def convert_gcode(self):
# read in the svg
paths, attributes = svg2paths("tmp/conversion-output.svg")
bounding_x_max = None
bounding_x_min = None
bounding_y_max = None
bounding_y_min = None
if isinstance(part, Line): for path in paths:
gcode += "G1 X{} Y{}\n".format(start_x, start_y)
gcode += "G1 Z{} \n".format(touch_height)
gcode += "G1 X{} Y{}\n".format(end_x, end_y)
bbox = path.bbox()
gcode += FINISH if bounding_x_max is None or bbox[0] > bounding_x_max:
bounding_x_max = bbox[0]
if bounding_x_min is None or bbox[1] < bounding_x_min:
bounding_x_min = bbox[1]
output_gcode = open("output/gcode-output.gcode", "w") if bounding_y_max is None or bbox[2] > bounding_y_max:
output_gcode.write(gcode) bounding_y_max = bbox[2]
output_gcode.close() if bounding_y_min is None or bbox[3] > bounding_y_min:
bounding_y_min = bbox[3]
file = open("output/gcode-output.gcode", "r") print("Maximum X : {}".format(bounding_x_max))
print("Minimum Y : {}".format(bounding_x_min))
print("Maximum X : {}".format(bounding_y_max))
print("Minimum Y : {}".format(bounding_y_min))
x = None max_dim = max(bounding_x_max, bounding_x_min, bounding_y_max, bounding_y_min)
y = None scale = (300 - self.offset_x) / max_dim
print("Scaling to : {}\n".format(scale))
with cairo.SVGSurface("rendered-output.svg", 300, 300) as surface: # Start the gcode
gcode = ""
gcode += self.gcode_preamble
context = cairo.Context(surface) # Walk through the paths and create the GCODE
context.scale(1, 1) for path in paths:
context.set_line_width(0.4)
largest_x = 0 previous_x = None
largest_y = 0 previous_y = None
smallest_x = 300
smallest_y = 300
for line in file: for part in path:
split = line.split(" ") start = part.start
command = split[0] end = part.end
operands = split[1:]
prev_x = x start_x = start.real * scale + self.offset_x
prev_y = y start_y = start.imag * scale + self.offset_y
if command == "G1": end_x = end.real * scale + self.offset_x
for operand in operands: end_y = end.imag * scale + self.offset_y
if operand.startswith("X"):
x = float(operand[1:])
if x > largest_x: largest_x = x
if x < smallest_x: smallest_x = x
elif operand.startswith("Y"):
y = float(operand[1:])
if y > largest_y: largest_y = y
if y < smallest_y: smallest_y = y
elif operand.startswith("Z{}".format(touch_height + raise_height)):
# signify a lift # Check to see if the endpoint of the last cycle continues and wether we need to lift the pen or not
if prev_x is not None and prev_y is not None and lift_markers: lift = True
context.arc(prev_x, prev_y, 0.5, 0, 2*math.pi) if previous_x is not None and previous_y is not None:
context.stroke() if abs(start.real - previous_x) < 30 and abs(start.imag - previous_y) < 30:
lift = False
prev_x = None # if the pen needs to lift,
prev_y = None # if lift:
x = None previous_x = end.real
y = None previous_y = end.imag
if (prev_x != x and prev_x is not None) or (prev_y != y and prev_y is not None): if lift:
context.line_to(prev_x, prev_y) gcode += "G1 Z{}\n".format(self.raise_height + self.touch_height)
context.line_to(x, y) else:
context.stroke() gcode += "# NOT LIFTING\n"
if isinstance(part, CubicBezier):
print("Largest X : " + str(largest_x)) nodes = np.asfortranarray([
print("Largest Y : " + str(largest_y)) [start.real, part.control1.real, part.control2.real, end.real],
print("Smallest X : " + str(smallest_x)) [start.imag, part.control1.imag, part.control2.imag, end.imag],
print("Smallest Y : " + str(smallest_y)) ])
if largest_x > 280: curve = bezier.Curve.from_nodes(nodes)
print("X OVERFLOW")
if largest_y > 280:
print("Y OVERFLOW")
if smallest_x < 125: evals = []
print("X_UNDERFLOW") pos = np.linspace(0.1, 1, 10)
if smallest_y < 20: for i in pos:
print("Y_UNDERFLOW") evals.append(curve.evaluate(i))
gcode += "G1 X{} Y{}\n".format(start_x, start_y)
gcode += "G1 Z{} \n".format(self.touch_height)
for i in evals:
x = i[0][0]
y = i[1][0]
gcode += "G1 X{} Y{}\n".format(x * scale + self.offset_x, y * scale + self.offset_y)
if isinstance(part, Line):
gcode += "G1 X{} Y{}\n".format(start_x, start_y)
gcode += "G1 Z{} \n".format(self.touch_height)
gcode += "G1 X{} Y{}\n".format(end_x, end_y)
gcode += self.gcode_end
output_gcode = open("output/gcode-output.gcode", "w")
output_gcode.write(gcode)
output_gcode.close()
if __name__ == "__main__":
GCoder()