Software

From FabLabGenovaWiki
Jump to: navigation, search

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:

  1. Install Processing
  2. Download and Install toxiclib
  3. 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">

  1. /***************************************************************************
  2. * Copyright (C) 2013 by Bondanza Mattia *
  3. * mattia.bond@hotmail.it *
  4. * *
  5. * This program is free software; you can redistribute it and/or modify *
  6. * it under the terms of the GNU General Public License as published by *
  7. * the Free Software Foundation; either version 2 of the License, or *
  8. * (at your option) any later version. *
  9. * *
  10. * This program is distributed in the hope that it will be useful, *
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of *
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
  13. * GNU General Public License for more details. *
  14. * *
  15. * You should have received a copy of the GNU General Public License *
  16. * along with this program; if not, write to the *
  17. * Free Software Foundation, Inc., *
  18. * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
  19. ***************************************************************************/
  1. runmode describes operation level:
  2. <anything> You expect everythig to work fine
  3. "debug" Gives verbose outputs
  4. "off circuit" Runs from random computer
  5. "off circuit debug" Runs from random computer with verbose outputs
  6. "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
  1. Angles are expressed in degrees, linear measures in millimeters and intervals in seconds.
  2. X axis goes from left pulley to right pulley.
  3. 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 )
       
  1. Initialises general purpose Raspberry ports

def gpio_init( ) :

   GPIO.cleanup()
   GPIO.setmode( GPIO.BOARD )
   GPIO.setup(15, GPIO.OUT)
  1. 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>