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