PIXELBOT – le robot qui dessine sur les murs

Hugo, Gaby, Gabriel, Bréval

Objectif : reproduire sur un mur une image matricielle à l’aide d’un feutre/crayon/bombe de peinture, suspendu(e) à deux fils, enroulés autour de bobines, mues par moteurs pas à pas.

Problématique et introduction

PIXELBOT est capable de transformer une image numérique en une création physique, en reproduisant fidèlement les pixels sur un support réel. À la croisée de la programmation, de la mécanique et de l’électronique, le PixelBot illustre concrètement comment les technologies modernes peuvent donner vie au monde numérique.

Ce projet met en avant des compétences en conception, en automatisation et en traitement d’image, tout en répondant à un défi technique stimulant : passer du virtuel au réel avec précision et créativité.

À travers ce site, découvrez le fonctionnement du PixelBot, les différentes étapes de sa conception, ainsi que les défis relevés pour mener à bien ce projet.

 

Domaines

  • Mécanique :
  • Électricité :
  • Numérique :

 

Matériel

  • Arduino UNO
  • Moteurs pas à pas avec pilote
  • Servomoteur pour le pointage
  • Lecteur de carte SD pour le stockage des images

 

Travail demandé

  • Établir la Loi entrée – sortie (position pixel ↔ pas moteur)
  • Finir conception 3D :
    • incrustation aimants sur support moteur
    • équilibrage du porte feutre
  • Programmation
    • pilotage moteur
      • programmation mode manuel \((g, d)\)
      • programmation déplacement vers \((x, y)\) : fonction moteTo(x,y)
    • mouvement feutre
      • programmation du marquage d’un point : fonction pointer()
    • lecture fichier image
    • programme complet (faire modèle)

 

Cahier des Charges

Cas d’utilisation

Hypothèses

Exigences

 

Description du système

Principes généraux

Chaînes fonctionnelles

Architecture choisie pour la maquette

Modélisation Cinématique

Schéma de câblage

 

Conception

Maquette volumique

CryptDrive

Loi entrée-sortie

relation mathématique donnant \((g, d)\) en fonction de \((x, x)\) : 

 

Préparation des images

Les images dessinées par PIXELBOT sont des images matricielles à 2 niveaux : noir ou blanc.

Pour faciliter la lecture des données par l’Arduino, on convertit un fichier image (jpg, png, …) en fichier texte comportant des « 0 » et des « 1 », à l’aide d’un programme Python

Programme Python
from PIL import Image               # Import de la classe Image
import os

##################################################################################################
##################################################################################################
##################################################################################################
def ouvrir_image(chemin_fichier):
    if os.path.isfile(chemin_fichier):
        try:
            return Image.open(chemin_fichier)
        except:
            return

##################################################################################################
##################################################################################################
##################################################################################################
def redimensionner(img, W, H,  mode = 0):
    """ Redimensionnement en WxH
    """
    rm = W/H  # aspect ratio de l'écran du Minitel

    if mode == 0:
        pass

    elif mode == 1: # redimensionnement avec déformation
        if r > rm: # l'image d'origine est trop large
            nw = rm*h
            e = (w-nw)/2
            img = img.crop((e, 0, w-e, h))
        else:
            nh = w / rm
            e = (h-nh)/2
            img = img.crop((0, e, w, h-e))

    elif mode == 2: # tronquage de l'image
        if r < rm:
            nw = rm*h
            e = (w-nw)/2
            img = img.crop((e, 0, w-e, h))
        else:
            nh = w / rm
            e = (h-nh)/2
            img = img.crop((0, e, w, h-e))
    return img.resize((W, H))



##################################################################################################
##################################################################################################
##################################################################################################
def img2pts(nom_fichier, w, h, mode = 0, seuil = 128):
    img = ouvrir_image(nom_fichier)      # Ouverture d'un fichier image
    if img is None:
        return

    # Redimensionnement (3 modes)
    img = redimensionner(img, w, h, mode)
    #img.show()

    # Conversion en N&B
    img = img.convert("L")
    img = img.convert("1", dither=Image.Dither.ORDERED) # Image.Dither.FLOYDSTEINBERG
    #img.show()

    nom_fichier_pts = os.path.splitext(nom_fichier)[0]+"_pts.txt"
    with open(nom_fichier_pts, "w") as f:
        for y in range(img.height):
            for x in range(img.width):
                #print(img.getpixel((x,y)))
                f.write(str(img.getpixel((x,y))//255))
            f.write("\n")


img2pts("spock.png", 200, 300)

Exemples de fichiers :

 

Programme Arduino

Principe :

  • Le support de feutre doit être placé en haut à gauche de la zone de dessin
    On doit prendre des mesures de cette position par rapport au moteur gauche (constantes)
  • L’image constituées de points est généré à partir d’un fichier image (PNG, JPG, …) à l’aide d’un programme Python
    Tableau à 2 dimensions de 0 et de 1 : 1 = point à tracer
  • L’image de points est placé sur une carte microSD sous la forme d’un fichier .txt
  • Le programme lit le fichier texte sur la carte microSD et parcours le tableau à 2 dimensions
    • Ordre de lecture depuis le coin haut gauche, ligne par ligne
    • Si un point doit être tracé (valeur 1),
      • le porte-feutre se déplace directement jusqu’à sa position
      • le porte feutre actionne le feutre
Programme Arduino
/*
  Programme d'affichage d'une image en noir&blanc
  Lecture d'un fichier sur la carte SD

  Conversion image --> txt : 
     programme graffiti.py

  Schémas sur Cryptpad

 */
#include <SPI.h>
#include <SD.h>
#include <Servo.h>


/*************************************************************************/
// Fichier "image"
File imgFile;
const char * fileName = "/PASCAL.TXT";

/*************************************************************************/
// Moteurs pas à pas
const int PPT = 240;  // nombre de pas par tour
const int DIA = 30;   // diamètre poulie (mm)
const int WMOT = 10;  // vitesse angulaire moteur (rpm)
#define STEP_PIN_G 3  
#define DIR_PIN_G 4   
#define STEP_PIN_D 5 
#define DIR_PIN_D 6  

#define PER_PAS 5000  // durée entre deux pas moteur [ms]

/*************************************************************************/
// Dimensions initiales (à mesurer sur le système !!!)

// entraxe des poulies
const float L = 270; // mm
// écart vertical
const float E = 55;  // mm
// longueurs initiales des ficelles (pour x = y = 0)
float LG = E;  // mm
float LD = sqrt(sq(E)+sq(L));  // mm
int PG;
int PD;


/*************************************************************************/
// taille de l'image (dépend du fichier)
int w = 0; // pixels
int h = 0; // pixels

/*************************************************************************/
// Servomoteur du pointeur
const byte POINTER_PIN = 9;
const byte POS_0 = 80;   // position "feutre relevé"
const byte POS_1 = 60;  // position "feutre posé"
Servo pointer_servo;


/**************************************************************************************************/
void setup() {
  Serial.begin(9600);
  while (!Serial) {
  }

  pinMode(STEP_PIN_G, OUTPUT);
  pinMode(DIR_PIN_G, OUTPUT);
  pinMode(STEP_PIN_D, OUTPUT);
  pinMode(DIR_PIN_D, OUTPUT);

  /*************************************************************************/
  /* Initialisation lecteur SD */
  Serial.println("init SD...");
  if (!SD.begin(10)) {
    Serial.println("init SD failed!");
    while (1);
  }
  Serial.println("init SD done.");

  /* Ouverture du fichier */
  if (!SD.exists(fileName)) {
    Serial.print("Fichier non trouvé : ");
    Serial.println(fileName);
    while (1);
  }
  
  /* Détermination des dimensions */
  imgFile = SD.open(fileName);
  
  while (imgFile.available()) {
    String data = imgFile.readStringUntil('\n');
    w = data.length()-1;
    h += 1;
  }
  imgFile.close();

  Serial.print("Taille : ");
  Serial.print(w);
  Serial.print("\t");
  Serial.println(h);

  /*************************************************************************/
  // Initialisation du servomoteur
  pointer_servo.attach(POINTER_PIN);

  /*************************************************************************/
  //Lecture du fichier et pointage 
  imgFile = SD.open(fileName);
  byte pixel[1];
  int x = 0; // pixels
  int y = 0; // pixels
  PG = mm2pas(LG);
  PD = mm2pas(LD);
  while (imgFile.available()) {
      imgFile.read(pixel, 1);

      // pointage
      if (pixel[0] == 49) {  // 1 --> pointage
        // déplacement
        moveTo(x, y); // pixels
        pointer();
      }

      // prochaine position
      x++;
      if (pixel[0] == 10) { // retour ligne
        y++;
        x = 0;
        Serial.println("--------");
      }
      //Serial.print(x);
      //Serial.print(", ");
      //Serial.print(y);
      //Serial.print("\t");
      //Serial.println(pixel[0]);
  }

  // Fermeture du fichier
  imgFile.close();
}

void loop() {
  // rien ici : le dessin n'est fait qu'une seule fois, dans le setup
}

// Conversion pixels --> mm
float pixels2mm(int x) { // marche aussi pour y !
  return L*float(x)/float(w);
}

// Conversion mm --> pas
int mm2pas(float x) { // marche aussi pour y !
  return int(x * float(PPT) / (3.1416*DIA));
}

// Mise en marche des 2 moteurs
void motorsRun(int PG, int PD) {
  motorSteps(STEP_PIN_G, DIR_PIN_G, -PG, PER_PAS);
  motorSteps(STEP_PIN_D, DIR_PIN_D, PD, PER_PAS);
}

/* Déplacement du feutre à la position (x,y) */
void moveTo(int x, int y) { // x et y absolus en pixels
  Serial.print("Move to : ");
  Serial.print(x);
  Serial.print('\t');
  Serial.println(y);

  // calcul des longueurs (en pas)
  int PG_ = PG;  // anciennes longueurs ficelles (pas)
  int PD_ = PD;

  PG = mm2pas(sqrt(sq(pixels2mm(x))   + sq(pixels2mm(y)+E)));   // nouvelles longueurs ficelles (pas)
  PD = mm2pas(sqrt(sq(L-pixels2mm(x)) + sq(pixels2mm(y)+E)));   // pas

  Serial.print("Ficelles : ");
  Serial.print(PG);
  Serial.print('\t');
  Serial.println(PD);

  int dPG = PG - PG_;
  int dPD = PD - PD_;

  Serial.print("Nbr pas : ");
  Serial.print(dPG);
  Serial.print('\t');
  Serial.println(dPD);

  int pasG;
  int pasD;
  int nPas;
  if (abs(dPG) > abs(dPD)) {
    pasG = sgn(dPG)*abs(dPG/dPD);
    pasD = sgn(dPD);
    nPas = abs(dPD);
  } else {
    pasG = sgn(dPG);
    pasD = sgn(dPD)*abs(dPD/dPG);
    nPas = abs(dPG);
  }

  for (int i = 0; i < nPas; i++) {
    motorsRun(pasG, pasD);
    dPD -= pasD;
    dPG -= pasG;
  }

  motorsRun(dPG, dPD);

  delay(50); // idée : calculer le temps nécessaire au mouvement pour plus de rapidité
}

/* Réalisation d'un point */
void pointer() {
  for (int pos = POS_0; pos >= POS_1; pos -= 1) { // goes from 180 degrees to 0 degrees
    pointer_servo.write(pos);              // tell servo to go to position in variable 'pos'
    delay(10);                       // waits 15 ms for the servo to reach the position
  }
  delay(200); // idée : calculer le temps nécessaire au mouvement pour plus de rapidité
  for (int pos = POS_1; pos <= POS_0; pos += 1) { // goes from 180 degrees to 0 degrees
    pointer_servo.write(pos);              // tell servo to go to position in variable 'pos'
    delay(2);                       // waits 15 ms for the servo to reach the position
  }
}

template <typename T> int sgn(T val) {
    return (T(0) < val) - (val < T(0));
}

void motorSteps(byte step_pin, byte dir_pin, int steps, unsigned int periode) {
  if (steps < 0) {
    digitalWrite(dir_pin, LOW); // Set direction to clockwise
    steps = -steps;
  } else {
    digitalWrite(dir_pin, HIGH); // Set direction to counterclockwise
  }

  for (int i = 0; i < steps; i++) {
    digitalWrite(step_pin, HIGH);
    delayMicroseconds(periode);
    digitalWrite(step_pin, LOW);
    delayMicroseconds(periode);
  }
}

 

 

Résultat

Vidéo

 

Photos

Site WEB

 

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *