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:
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);
}

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

#/***************************************************************************
# *   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( );