Software
Image pre-processing
assuming GIMP:
- Push up the contrast
- Image ==> Mode ==> Indexed
- B/W (1 bit palette)
- Dithering: Floyd-Steinberg
Processing code
for the conversion from the image file to the pen tip path (Last update: Thu 14 Feb 2013, 19.39)
Step by Step:
- Install Processing
- Download and Install toxiclib
- Create a sketch with the code below:
<source lang="javascript"> import toxi.geom.*;
// tsp variables int particleRouteLength; Vec2D[] particles; int[] particleRoute; int maxParticles;
// image variable PImage img;
float millisLastFrame = 0; float frameTime = 0; // scale of the drawing float s = 2.0;
void setup() {
maxParticles = 15000; //img = loadImage("lenna-lg_BW_loRes.png"); img = loadImage("test.png"); //img = loadImage("test_masa.png"); //img = loadImage("lenna_BW_loRes2.png"); size(img.width*(int)s, img.height*(int)s); //size(400, 600); // count black pixels int i; maxParticles = 0; for ( int x = 0; x < img.width; x++ ) { for ( int y = 0; y < img.height; y++ ) { i = ( ( y * img.width ) + x ); // getting pixel index if ( img.pixels[i] == color( 0, 0, 0 ) ) { maxParticles++; } } }
println("black dots: " + maxParticles); // allocate and fill points vector particles = new Vec2D[maxParticles]; i = 0; int j = 0; for ( int x = 0; x < img.width; x++ ) { for ( int y = 0; y < img.height; y++ ) { i = ( ( y * img.width ) + x ); if ( img.pixels[i] == color( 0, 0, 0 ) ) { Vec2D p1 = new Vec2D(x, y); particles[j] = p1; j++; } } } millisLastFrame = millis(); initPath(); // initialize path (NN heuristic) for (int l = 0; l < 5; l++ ) { // optimize path with 2-opt heuristic for (int k = 0; k < 1000; k++ ) optimizePath(); // profiling ... frameTime = (millis() - millisLastFrame)/1000; millisLastFrame = millis(); println("Frame time: " + millisLastFrame); } noLoop();
}
void initPath() {
int temp; println("initializing path (NN)"); Vec2D p1, p2; particleRouteLength = maxParticles; // array of free ramaining particles to be queried boolean freeParticles[] = new boolean[maxParticles]; particleRoute = new int[particleRouteLength]; int closestParticle; float distMin; p1 = particles[0]; freeParticles[0] = true; particleRoute[0] = 0; // Nearest neighbor ("Simple, Greedy") algorithm path optimization: int i = 0, j; float dx, dy, distance; while (i < particleRouteLength) { distMin = Float.MAX_VALUE; // re-initialize mimimun distance value closestParticle = 0; // re-initialize closest particle for (j = 0; j < particleRouteLength; j++) { if (freeParticles[j] == false) { p2 = particles[j]; // get next particle to calculate distance dx = p1.x - p2.x; dy = p1.y - p2.y; distance = (float) (dx*dx+dy*dy); // Only looking for closest; do not need sqrt factor! if (distance < distMin) { closestParticle = j; // update the closest particle index distMin = distance; // update the minimum distance value } } } freeParticles[closestParticle] = true; // remove the particle from the ones to be queried particleRoute[i] = closestParticle; //set the next particle in the path i++; // increment while counter } // Initial routing is complete frameTime = (millis() - millisLastFrame)/1000; millisLastFrame = millis(); println("Frame time: " + millisLastFrame);
}
void optimizePath() {
// 2-opt heuristic optimization: // Identify a pair of edges that would become shorter by reversing part of the tour. int temp; //println("optimizing path (2-opt) " ); for (int i = 0; i < 5000; ++i) { // 1000 tests per frame; you can edit this number. int indexA = floor(random(particleRouteLength - 1)); int indexB = floor(random(particleRouteLength - 1)); if (Math.abs(indexA - indexB) < 2) continue; if (indexB < indexA) { // swap A, B. temp = indexB; indexB = indexA; indexA = temp; }
Vec2D a0 = particles[particleRoute[indexA]]; Vec2D a1 = particles[particleRoute[indexA + 1]]; Vec2D b0 = particles[particleRoute[indexB]]; Vec2D b1 = particles[particleRoute[indexB + 1]];
// Original distance: float dx = a0.x - a1.x; float dy = a0.y - a1.y; float distance = (float) (dx*dx+dy*dy); // only a comparison; do not need sqrt factor! dx = b0.x - b1.x; dy = b0.y - b1.y; distance += (float) (dx*dx+dy*dy); // only a comparison; do not need sqrt factor! // Possible shorter distance? dx = a0.x - b0.x; dy = a0.y - b0.y; float distance2 = (float) (dx*dx+dy*dy); // only a comparison; do not need sqrt factor! dx = a1.x - b1.x; dy = a1.y - b1.y; distance2 += (float) (dx*dx+dy*dy); // only a comparison; do not need sqrt factor!
if (distance2 < distance) { // Reverse tour between a1 and b0. int indexhigh = indexB; int indexlow = indexA + 1; while (indexhigh > indexlow) { temp = particleRoute[indexlow]; particleRoute[indexlow] = particleRoute[indexhigh]; particleRoute[indexhigh] = temp; indexhigh--; indexlow++; } } }
}
void draw() {
//image(img, 0, 0); image(img, width*s, height*s); int i = 0; stroke(128, 128, 255); // Stroke color (blue) strokeWeight (.5); // stroke weight println("in draw, n.part : " + particleRouteLength);
String[] lines = new String[particleRouteLength]; for (i = 0; i < particleRouteLength; i++) { lines[i] = particles[particleRoute[i]].x + " " + particles[particleRoute[i]].y; } saveStrings("path.txt", lines);
// loop the particles drawing a line between successive points for ( i = 0; i < (particleRouteLength - 1); ++i) { Vec2D p1 = particles[particleRoute[i]]; Vec2D p2 = particles[particleRoute[i + 1]]; line(p1.x*s, p1.y*s, p2.x*s, p2.y*s); } //Aggiungo due linee per visualizzare il punto da cui parte il disegno. Bond.
stroke(255,0,0);//Impostazioni di colore per la prima linea strokeWeight (10); point(particles[particleRoute[0]].x*s, particles[particleRoute[0]].y*s);
} </source>
Python code
TO DO
- Permettere di scegliere l'input file da linea di comando
- Riscrivere meglio il sistema di run level; a debug level deve mostrare gli eventuali errori;
release note
Questa è la versione con cui è stata realizzata lena, a cui è stata aggiunta la progress bar di Simo, fatta girare su un raspi; Al 99% funzionerà anche attaccata al drawbot. Su dropbox l'ultima testata 0.4
code
<source lang="python">
- /***************************************************************************
- * Copyright (C) 2013 by Bondanza Mattia *
- * mattia.bond@hotmail.it *
- * *
- * This program is free software; you can redistribute it and/or modify *
- * it under the terms of the GNU General Public License as published by *
- * the Free Software Foundation; either version 2 of the License, or *
- * (at your option) any later version. *
- * *
- * This program is distributed in the hope that it will be useful, *
- * but WITHOUT ANY WARRANTY; without even the implied warranty of *
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
- * GNU General Public License for more details. *
- * *
- * You should have received a copy of the GNU General Public License *
- * along with this program; if not, write to the *
- * Free Software Foundation, Inc., *
- * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
- ***************************************************************************/
- runmode describes operation level:
- <anything> You expect everythig to work fine
- "debug" Gives verbose outputs
- "off circuit" Runs from random computer
- "off circuit debug" Runs from random computer with verbose outputs
- "help" Print the help messages of class drawbot and motor
runmode = ""
from time import sleep, time # No good function import math from ProgressBar import *
if runmode != "off circuit" and runmode != "off circuit debug" and runmode != "help":
import RPi.GPIO as GPIO
- Angles are expressed in degrees, linear measures in millimeters and intervals in seconds.
- X axis goes from left pulley to right pulley.
- Y axis goes from up to down.
class DrawingError( Exception ):
"This is an exception class that describes errors involved in robo-drawing" def __init__( self, value ): self.error = value def __str__( self ): return self.error
class Motor:
"""This class allows communication between RaspberryPi and stepper motor drivers. Tested upon <nome dei driver>.""" def __init__( self, StepPin, DirPin, step_time ): self.StepPin = StepPin # RaspberryPi pin connected to CLK driver pin self.DirPin = DirPin # RaspberryPi pin connected to DIR driver pin self.step_time = step_time # Step execution time [S] if runmode != "off circuit" and runmode != "off circuit debug": GPIO.setup(self.StepPin, GPIO.OUT) GPIO.setup(self.DirPin, GPIO.OUT) GPIO.output(self.StepPin, False) GPIO.output(self.DirPin, False) def step( self, direction ): "Generates a one-step signal in the given direction (True = counter-clockwise)" GPIO.output(self.DirPin, direction) GPIO.output(self.StepPin, True) sleep( self.step_time ) GPIO.output(self.StepPin, False) sleep( self.step_time )
class Drawbot:
def refreshPosition( self, x = -1, y = -1 ): """Set pen position to the given coordinates. If none are given calculates position from cord lengths.""" if( x == -1 ): self.x = ( self.pulley_distance ** 2 + self.sx_len ** 2 - self.dx_len ** 2 ) / ( 2 * self.pulley_distance ) self.y = ( self.sx_len ** 2 - self.x ** 2 ) ** 0.5 else: self.x = x self.y = y def __init__( self, pulley_distance, pulley_radius, step_angle, init_lenght_sx, init_lenght_dx, max_dist, motor_sx, motor_dx): """Creates a Drawbot instance with the given features: pulley_distance = Distance between pulleys [mm] init_lenght_dx = Initial right cord length [mm] init_lenght_sx = Initial left cord length [mm] max_dist = Highest permitted distance between points [mm] motor_xx = Instances describing mounted motors""" self.pulley_distance = pulley_distance # Distance between pulleys [mm] self.dx_len = init_lenght_dx # Initial right cord length [mm] self.sx_len = init_lenght_sx # Initial left cord length [mm] self.sq_max_dist = max_dist ** 2 # Highest permitted distance between points [mm^2] self.step_len = math.radians( step_angle )*pulley_radius# Absolute value of one-step cord lengthen [mm] self.refreshPosition() self.motor_sx = motor_sx self.motor_dx = motor_dx def __str__( self ): "Returns a description of the drawbot instance" return """Position: x = %4f; y = %4f
\rLeft pulley cord length = %4f \rRight pulley cord length = %4f""" % ( self.x, self.y, self.sx_len, self.dx_len )
def moveMotorTo( self, len_sx, len_dx ): """Calculates steps to reach given position (cords length) To obtain best results long distances are divided into smaller segments""" if runmode == "debug" or runmode == "off circuit debug": print "debug: Requested Len %4.1f %4.1f " % ( len_sx, len_dx ) steps_sx = ( len_sx - self.sx_len ) / self.step_len steps_dx = ( self.dx_len - len_dx ) / self.step_len if steps_sx < 0 : #determines direction dir_sx = True else : dir_sx = False if steps_dx < 0 : dir_dx = True else : dir_dx = False steps_sx = math.fabs( steps_sx ) steps_dx = math.fabs( steps_dx ) # Rounds to the nearest integer if math.modf( steps_sx )[ 0 ] > 0.5 : steps_sx += 1 if math.modf( steps_dx )[ 0 ] > 0.5 : steps_dx += 1 steps_sx = int( steps_sx ) steps_dx = int( steps_dx ) if runmode == "debug" or runmode == "off circuit debug": print "debug: requested step: ",dir_sx, steps_sx," ",dir_dx, steps_dx #__TODO__ implement step altern. # This move motors one per time, works fine with 1mm segments. if runmode != "off circuit" and runmode != "off circuit debug": for i in range( steps_sx ): self.motor_sx.step( dir_sx ) for i in range( steps_dx ): self.motor_dx.step( dir_dx )
# Updates cords lenghts. if dir_sx == True : self.sx_len -= steps_sx * self.step_len else : self.sx_len += steps_sx * self.step_len if dir_dx == True: self.dx_len += steps_dx * self.step_len else : self.dx_len -= steps_dx * self.step_len # Updates position from cords lengths. self.refreshPosition() def goTo( self, x, y ): """This method moves the pen trought long distances keeping precision calculating intermediate points (partial_px,y) every max_dist mm""" sq_distance = ( x - self.x ) ** 2 + ( y - self.y )** 2 if sq_distance > self.sq_max_dist : # Calculates x and y increments div_number = ( int( math.sqrt( sq_distance / self.sq_max_dist ) ) + 1 ) div_x = ( x - self.x ) / div_number div_y = ( y - self.y ) / div_number #E quindi riinvoca se stessa per ogni tratto. partial_px = self.x partial_py = self.y for i in range( div_number ): partial_px += div_x partial_py += div_y self.goTo( partial_px, partial_py ) else : # The distance is inferior to max_dist, brutally goes to the point. if runmode == "debug" or runmode == "off circuit debug": print "debug: moving to ( %3.2f ; %3.2f )" % ( x, y ) self.moveMotorTo( math.hypot( x, y ), math.hypot( self.pulley_distance - x, y ) ) def checkPathDimension( self, path ): max_r= 0 max_left = 0 max_down = 0 max_up = 0 # Searches for most external points in the path. for i in path: if path[0][0] - i[0] > max_r : max_r = path[0][0] - i[0] elif i[0] - path[0][0] > max_left : max_left = i[0] - path[0][0] if path[0][1] - i[1] > max_down : max_down = path[0][1] - i[1] elif i[1] - path[0][1] > max_up: max_up = i[1] - path[0][1] print """Drawing dimension from initial point:
\r right: %3.1fcm \r left:%3.1fcm \r up: %3.1fcm \r down: %3.1fcm""" % ( max_left/10, max_r/10, max_down/10, max_up/10 )
# Cheks wether the drawbot has enough space to perform drawing. # __TODO__ Deve centrare l'immagine se puo essere spostata per essere adatta. # dovra dire quando tappare il pennarello etch if max_left + self.x > self.pulley_distance: raise DrawingError, DrawingError: The path given exceed the working area of drawbot ( right side %3.1fmm ). \rReplace the cursor and retry. % ( max_left + self.x - self.pulley_distance) if self.x - max_r < 0: raise DrawingError, DrawingError: The path given exceed the working area of drawbot ( left side %3.1fmm ). \rReplace the cursor and retry. % ( max_r - self.x ) if self.y - max_down < 0: raise DrawingError, DrawingError: The path given exceed the working area of drawbot ( upper side %3.1fmm ). \rReplace the cursor and retry. % ( max_down - self.y ) def estimateTime( self, path, parent = True ):
"""This is a recursive function built on the method runPath(), goTo() and moveMotorTo() Returns the time expected the system to complete the path drawing It virtually moves the drowbot on the path to get an accurate value of the time.""" time = 0.0 #Only the parent function ( parent = True ) uses offsets to translate the drawing #In the consuete position. For the next step of the recursion offsets are useless #so they are inizialized to 0 by default x_offset = 0 y_offset = 0
if parent : #Save the actual status of the drawbot, to ripristinate it when the function ends dx_len = self.dx_len sx_len = self.sx_len #Calculate offsets x_offset = path[0][0] - self.x y_offset = path[0][1] - self.y
#This cycle is very similar to the function runPath() goTo() and moveMotorTo()
for i in path: x = i[0] - x_offset y = i[1] - y_offset sq_distance = ( x - self.x ) ** 2 + ( y - self.y )** 2 if sq_distance > self.sq_max_dist : div_number = ( int( math.sqrt( sq_distance / self.sq_max_dist ) ) + 1 ) stepx = ( x - self.x ) / div_number stepy = ( y - self.y ) / div_number
partial_px = self.x partial_py = self.y for i in range( div_number ): partial_px += stepx partial_py += stepy #Re-invocate itself for each partial point and sum the time time += self.estimateTime( ( ( partial_px, partial_py ), ), False ) else : length_left = math.hypot( x, y) length_right = math.hypot( self.pulley_distance - x, y ) step_sx = ( length_left - self.sx_len ) / self.step_len step_dx = ( length_right - self.dx_len ) / self.step_len
dir_sx = False dir_dx = False
if step_sx > 0 : dir_sx = True if step_dx > 0 : dir_dx = True
step_sx = math.fabs( step_sx ) step_dx = math.fabs( step_dx )
if math.modf( step_sx )[ 0 ] > 0.5 : step_sx += 1 if math.modf( step_dx )[ 0 ] > 0.5 : step_dx += 1
step_sx = int( step_sx ) step_dx = int( step_dx )
#Return the time time += 2 * step_sx * self.motor_sx.step_time + 2 * step_dx * self.motor_dx.step_time
if dir_sx == True : self.sx_len += step_sx * self.step_len else : self.sx_len -= step_sx * self.step_len
if dir_dx == True: self.dx_len += step_dx * self.step_len else : self.dx_len -= step_dx * self.step_len
#Update the chord length self.refreshPosition()
if parent : #If it is in the parent function ripristinate the initial condition of the # object Drawbot self.dx_len = dx_len self.sx_len = sx_len self.refreshPosition() #And print the estimate time in h, min, s print "Estimated running time", time_cp = time h = int( time_cp / 3600 ) if h != 0: print " %dh" % h, time_cp %= 3600 min = int( time_cp / 60 ) if min != 0 : print" %dmin" % min, time_cp %= 60 print " %ds" % time_cp, average_steps = 1 / ( self.motor_sx.step_time + self.motor_dx.step_time ) print " at an average motor speed of %d steps per second" % average_steps #In both case return the time in second for further elaboration return time
def runPath( self, PathAdd, scale = 1 ): """Loads and starts pathAdd file. Scale parameter is used to change path dimensions.""" fin = open( PathAdd, "r" ) file = fin.readlines() path = [] print "\nLoading File..." for i in file: coordinates = i.split() if runmode == "debug" or runmode == "off circuit debug": print """debug: read coordinate ( %s ; %s ) from the file.""" % ( coordinates[0], coordinates[1] ) path.append( ( float( coordinates[0] ) * scale, float( coordinates[1] ) * scale ) ) print "File Loaded.\n" print "* Point number: ", len( path ) print "\n* ", self.checkPathDimension( path ) print "\n* ", est_time = self.estimateTime( path ) sel = raw_input( "\nStarting drawing the path (y/n)? ") if sel == "n" or sel == "no": return print "Starting drawing.\n"
#Create a progress bar with the class in ProgressBar.py
prb = ProgressBar( 0.0, est_time ) # Starts from initial position. x_offset = path[0][0] - self.x y_offset = path[0][1] - self.y
#This is used as init time for drawing process
time_init = time()
for i in path: self.goTo( i[0] - x_offset, i[1] - y_offset )
#This refresh the progress bar on the time passed from time_init
prb.update( time() - time_init )
- Initialises general purpose Raspberry ports
def gpio_init( ) :
GPIO.cleanup() GPIO.setmode( GPIO.BOARD ) GPIO.setup(15, GPIO.OUT)
- Program starts here -------------------------------------------------------------*
try: #If in help mode only shows the help of Drawbot and Motor classes #__TODO__ using optpars lib insert -f filename parameter, -s scale parameter... if runmode == "help": help( Drawbot ) help( Motor ) #Exit without any output raise KeyboardInterrupt
#If in-circuit initialize the GPIO if runmode != "off circuit" and runmode != "off circuit debug": gpio_init() # Create the Motor and Drawbot objects, giving the system parameter. # Run in help mode for further information MotorSx = Motor( 12, 13, 0.0025 ) MotorDx = Motor( 7, 11, 0.0025 ) Macchina1 = Drawbot( 1200, 9.5, 0.9, 900, 900, 1, MotorSx, MotorDx ) #Print the initial and final state of the drawbot and plot a path print "\nInitial state:\n" + str( Macchina1 ) Macchina1.runPath( "./path/me.txt", 0.8 ) print "\nFinished.\nDrawbot state: \n" print str(Macchina1)
#Clear GPIO if runmode != "off circuit" and runmode != "off circuit debug": GPIO.output(15, False) GPIO.cleanup()
except DrawingError, exception:
print str( exception )
except KeyboardInterrupt : if runmode != "help": print "Interrupted from keyboard." print "Exiting." if runmode != "off circuit" and runmode != "off circuit debug" : GPIO.output(15, False) GPIO.cleanup( );
except : print "An error occurs during the code execution. Exiting." if runmode != "off circuit" and runmode != "off circuit debug" and runmode != "help" : GPIO.output(15, False) GPIO.cleanup( ); </source>