Pintura de Luz con un Solo LED: Crea Imágenes Dinámicas con Sistema Motorizado DIY
Componentes y materiales
- 2 Motores paso a paso NEMA17
- 2 Drivers DRV8825 para motores paso a paso
- Lector de tarjeta SD
- Arduino Mega 2560
- Correas dentadas y ruedas V diversas
Herramientas y máquinas necesarias
- Impresora 3D (genérica)
Aplicaciones y servicios online
- Arduino IDE
- GIMP (para crear imágenes)
- Autodesk Fusion 360 (diseño CAD)
Sobre el proyecto
Idea
Tras ver varios videos y artículos sobre pintura de luz, decidí probarlo. La pintura de luz usa una cámara con exposición muy larga para capturar una pequeña fuente de luz, convirtiendo un punto de luz en una estela larga en una sola imagen.
Pero, ¿qué pasa si alguien quiere crear una imagen más detallada o usar muchos colores? Así surgió la idea de construir una máquina CNC de 2 ejes con un solo LED RGB que cambie de color y "pinte" una imagen.
El Plan
El proyecto requiere cuatro componentes principales: una máquina CNC de 2 ejes, un LED RGB, una tarjeta SD y una cámara capaz de tomas de larga exposición. Primero, el Arduino Mega lee la tarjeta SD y busca un bitmap para imprimir. Luego recorre horizontalmente encendiendo los LEDs correspondientes mientras baja una fila cada vez que supera el ancho de la imagen. Finalmente espera y busca el siguiente bitmap, deteniéndose cuando no hay más imágenes.
Construcción de la estructura
Gracias a mi experiencia diseñando máquinas CNC, este paso no fue difícil. Quería algo modular y expandible, así que usé un diseño simple con dos correas dentadas unidas a travesaños que se mueven sobre perfiles de aluminio paralelos. Esto permite personalizar mucho la longitud de cada eje. Los extremos del eje X tienen tapas impresas en 3D, una con soporte para el motor y rodamiento del eje X.
Lectura de Bitmaps
Elegí el formato bitmap por su simplicidad y facilidad de lectura. Según el formato, hay direcciones importantes en el archivo: 0x12 (ancho), 0x16 (alto), 0x1C (profundidad de color), 0xA (ubicación de datos de píxeles) y 0x36 (inicio típico de datos de píxeles). Los datos se leen en bloques de 2 o 4 bytes (16 o 32 bits), avanzando el puntero. La función de lectura obtiene todos los datos importantes, incluyendo offsets y tamaños, y luego lee cada píxel fila por fila.
Preparación de las imágenes
Como la mayoría de cámaras limitan la exposición a 30 segundos, hay un límite de ~288 píxeles totales en ese tiempo, equivalentes a una imagen de ~18x16 píxeles. Creé arte pixel simple en GIMP: una Pokéball, un corazón y Mario saltando. Los coloqué en una carpeta "bitmaps" en la raíz de la tarjeta SD. El programa lee todas las imágenes de esta carpeta.
Programa de Pintura
Como los motores paso a paso no tienen retroalimentación de posición interna, sus posiciones deben trackearse por software. Mi programa usa un sistema de cuadrícula para rastrear la posición del LED y permitir escalado fácil. Al iniciar, las posiciones de los steppers se ponen a 0,0 y se lee la primera imagen. El LED parpadea 5 veces para avisar al fotógrafo que es hora de empezar la captura. El bitmap se lee recorriendo cada fila y dentro de cada fila cada columna. Conociendo la fila y columna actual, los motores mueven el LED a esa posición y cambian su color al del píxel correspondiente.
(Re)Creando una imagen
Tras insertar la tarjeta SD y conectar 12V para los motores, encendí la máquina. En la cámara: exposición 20s, apertura F36, ISO 100, compensación -5 EV para minimizar ghosting. La primera imagen fue una Pokéball. Aunque un poco borrosa, la forma se ve clara.
Luego creó un corazón (9x9 píxeles), donde cada píxel se define menos.
Finalmente, Mario saltando. Esta imagen tiene mucho ghosting por la abundancia de píxeles brillantes.
Ideas futuras para mejoras
Los resultados fueron mejores de lo esperado, pero hay margen de mejora. Lo principal: reducir el desenfoque haciendo que el LED se mueva apagado y solo se encienda cuando está quieto. Esta técnica mejorarían enormemente la nitidez.
Código
Programa de Pintura de Luz (C/C++)
// Lectura de bitmap parcialmente de Adafruit
#include <SD.h>
#include <SPI.h>
#include "DRV8825.h"
#define MOTOR_STEPS 200
#define RPM 150
#define MICROSTEPS 4
// Definiciones de pines
#define STEPPER_X_DIR 7
#define STEPPER_X_STEP 6
#define STEPPER_X_EN 8
#define STEPPER_Y_DIR 4
#define STEPPER_Y_STEP 5
#define STEPPER_Y_EN 12
#define X 0
#define Y 1
#define X_DIR_FLAG -1
#define Y_DIR_FLAG 1
#define STEPS_PER_MM (3.75 * MICROSTEPS)
#define SPACE_BETWEEN_POSITIONS 5
#define R A0
#define G A1
#define B A2
#define SD_CS 22
int currentPositions[] = {0, 0};
DRV8825 stepperX(MOTOR_STEPS, STEPPER_X_DIR, STEPPER_X_STEP, STEPPER_X_EN);
DRV8825 stepperY(MOTOR_STEPS, STEPPER_Y_DIR, STEPPER_Y_STEP, STEPPER_Y_EN);
void setup() {
Serial.begin(115200);
init_steppers();
SD.begin(SD_CS);
createBitmaps();
stepperX.disable();
stepperY.disable();
while(1);
}
void loop() { }
void createBitmaps(){
File dir = SD.open("bitmaps");
while(true){
File bitmap = dir.openNextFile();
if(!bitmap){ break; }
paintBitmap(bitmap);
delay(15000);
}
}
#define BUFFPIXEL 20
void paintBitmap(File bmpFile){
int bmpWidth, bmpHeight;
uint8_t bmpDepth;
uint32_t bmpImageOffset;
uint32_t rowSize;
uint8_t sdbuffer[3 * BUFFPIXEL];
uint8_t buffidx = sizeof(sdbuffer);
boolean goodBmp = false;
boolean flip = true;
int w, h, row, col;
uint8_t r, g, b;
uint32_t pos = 0, startTime = millis();
Serial.println();
Serial.print("Loading image '");
Serial.print(bmpFile.name());
Serial.println("'");
if (read16(bmpFile) == 0x4D42) {
Serial.print("File size: "); Serial.println(read32(bmpFile));
(void)read32(bmpFile);
bmpImageOffset = read32(bmpFile);
Serial.print("Image Offset: "); Serial.println(bmpImageOffset, DEC);
Serial.print("Header size: "); Serial.println(read32(bmpFile));
bmpWidth = read32(bmpFile);
bmpHeight = read32(bmpFile);
if (read16(bmpFile) == 1) {
bmpDepth = read16(bmpFile);
Serial.print("Bit Depth: "); Serial.println(bmpDepth);
if ((bmpDepth == 24) && (read32(bmpFile) == 0)) {
goodBmp = true;
Serial.print("Image size: "); Serial.print(bmpWidth); Serial.print('x'); Serial.println(bmpHeight);
rowSize = (bmpWidth * 3 + 3) & ~3;
if (bmpHeight < 0) { bmpHeight = -bmpHeight; flip = false; }
w = bmpWidth; h = bmpHeight;
if(bmpWidth*bmpHeight > 290){
Serial.println("File is too large to be printed."); return;
}
for(uint8_t i=0; i<5; i++){
analogWrite(R, 150); delay(500); analogWrite(R, 0); delay(500);
}
for (row = 0; row < h; row++) {
moveToPosition(0, row);
if (flip) pos = bmpImageOffset + (bmpHeight - 1 - row) * rowSize;
else pos = bmpImageOffset + row * rowSize;
if (bmpFile.position() != pos) {
bmpFile.seek(pos); buffidx = sizeof(sdbuffer);
}
for (col = 0; col < w; col++) {
if (buffidx >= sizeof(sdbuffer)) {
bmpFile.read(sdbuffer, sizeof(sdbuffer)); buffidx = 0;
}
b = sdbuffer[buffidx++];
g = sdbuffer[buffidx++];
r = sdbuffer[buffidx++];
moveToPosition(col, row);
activateLED(r,g,b);
}
analogWrite(R, 0); analogWrite(G, 0); analogWrite(B, 0);
}
Serial.print("Loaded in "); Serial.print(millis() - startTime); Serial.println(" ms");
}
}
}
bmpFile.close();
moveToPosition(0,0);
if (!goodBmp) Serial.println("BMP format not recognized.");
}
uint16_t read16(File f) {
uint16_t result;
((uint8_t *)&result)[0] = f.read();
((uint8_t *)&result)[1] = f.read();
return result;
}
uint32_t read32(File f) {
uint32_t result;
((uint8_t *)&result)[0] = f.read();
((uint8_t *)&result)[1] = f.read();
((uint8_t *)&result)[2] = f.read();
((uint8_t *)&result)[3] = f.read();
return result;
}
void activateLED(int r, int g, int b){
Serial.print(F("LED has value of: ")); Serial.print(r); Serial.print(", "); Serial.print(g); Serial.print(", "); Serial.println(b);
analogWrite(R, r); analogWrite(G, g); analogWrite(B, b);
}
void moveToPosition(int x, int y){
int newPosX = (x-currentPositions[X])*STEPS_PER_MM*X_DIR_FLAG*SPACE_BETWEEN_POSITIONS;
int newPosY = (y-currentPositions[Y])*STEPS_PER_MM*Y_DIR_FLAG*SPACE_BETWEEN_POSITIONS;
stepperX.move(newPosX);
stepperY.move(newPosY);
currentPositions[X] = x;
currentPositions[Y] = y;
Serial.print("Stepper positions: "); Serial.print(currentPositions[X]); Serial.print(", "); Serial.println(currentPositions[Y]);
}
void init_steppers(){
stepperX.begin(RPM); stepperX.setEnableActiveState(LOW); stepperX.enable(); stepperX.setMicrostep(MICROSTEPS);
stepperY.begin(RPM); stepperY.setEnableActiveState(LOW); stepperY.enable(); stepperY.setMicrostep(MICROSTEPS);
}
Piezas personalizadas y envolventes
Archivos CAD disponibles en el proyecto original.
Esquemáticos

Proceso de manufactura
- Dominando el AMFE: Guía paso a paso para gerentes de mantenimiento para construir y usar Análisis de Modos de Falla y Efectos
- Por qué los diseñadores vuelven a los materiales naturales en el diseño de productos
- Controle un LED con Sensor PIR de Movimiento en Raspberry Pi
- Transferir datos de temperatura y humedad entre dos Arduinos usando Firebase
- Proyecto Arduino Melodía Coffin Dance: Construya y Reproduzca el Viral Tune
- Revitaliza mandos a distancia antiguos: Crea dispositivos inteligentes DIY con Arduino
- Controla un Rover Arduino con Firmata y Mando Xbox One - Guía Completa
- Pantalla de 8 LED activada por sonido con Arduino
- Construye un Robot Cuadrúpedo Inteligente con Arduino: Guía Paso a Paso
- Transferencia Inalámbrica de Datos por LED (Li-Fi) con Arduino
- Intercambie dos variables Python sin variable temporal: fácil y limpio