Single-LED Light Painting: Dynamische Bilder mit DIY-motorisiertem System erstellen
Komponenten und Zubehör
| NEMA17 Schrittmotor | 2 |
| Theremino Treiber DRV8825 für Schrittmotoren | 2 |
| SD-Kartenleser | 1 |
| Arduino Mega 2560 | 1 |
| Verschiedene Zahnriemen und V-Rollen | 1 |
Notwendige Werkzeuge und Maschinen
- 3D-Drucker (generisch)
Apps und Online-Dienste
- Arduino IDE
- GIMP
- Autodesk Fusion 360
Über dieses Projekt
Idee
Nachdem ich mehrere Videos angeschaut und zahlreiche Artikel über Light Painting gesehen hatte, beschloss ich, es selbst zu versuchen. Light Painting nutzt eine Kamera mit sehr langer Belichtungszeit, um eine kleine Lichtquelle einzufangen. Dadurch wird ein einzelner Lichtpunkt zu einem langen Streifen in einem einzelnen Bild.
Aber was, wenn jemand ein detaillierteres Bild erstellen oder viele verschiedene Farben verwenden möchte? So kam ich auf die Idee, eine 2-Achsen-CNC-Maschine zu bauen, die eine einzelne RGB-LED besitzt, die Farbe wechseln und ein Bild „malen“ kann.
Der Plan
Dieses Projekt erfordert vier Hauptkomponenten: eine 2-Achsen-CNC-Maschine, eine RGB-LED, eine SD-Karte und eine Kamera, die Langzeitbelichtungen ermöglicht. Zuerst liest der Arduino Mega die SD-Karte und findet eine Bitmap zum Drucken.
Dann fährt er horizontal und leuchtet die entsprechenden LEDs, während er nach jeder Bildbreite eine Zeile nach unten bewegt. Schließlich wartet er kurz und sucht nach der nächsten Bitmap, bis keine Bilder mehr vorhanden sind.
Aufbau der Apparatur
Dank meiner Erfahrung im Design und Bau von CNC-Maschinen war dieser Schritt nicht allzu schwierig. Ich wollte etwas Modulares schaffen, das auch für andere Projekte erweitert werden kann, und entschied mich für ein einfaches Design mit zwei Zahnriemen, die an Querträgern befestigt sind und sich entlang paralleler Aluminiumprofile bewegen.
Dadurch lässt sich die Länge jeder Achse sehr flexibel anpassen. Die Enden der X-Achse haben 3D-gedruckte Endkappen, von denen eine eine Halterung für den X-Achsen-Schrittmotor und das Lager besitzt.
Bitmaps lesen
Ich wählte das Bitmap-Dateiformat aufgrund seiner Einfachheit und der leichten Lesbarkeit. Basierend auf dem Dateiformat gibt es einige wichtige Adressen in der Datei, die gelesen werden müssen: 0x12 (Breite), 0x16 (Höhe), 0x1C (Farbtiefe), 0xA (Position der Pixeldaten) und 0x36 (wo die Pixeldaten normalerweise beginnen).
Die Daten werden in Blöcken von entweder zwei oder vier Bytes (16 oder 32 Bit) gelesen, was auch den Zeiger auf die nächste Adresse vorrückt. Die Lesefunktion geht durch und erfasst alle wichtigen Daten, einschließlich Offsets und Größen. Dann liest sie jeden Pixel zeilenweise.
Bilder vorbereiten
Da die meisten Kameras auf maximal 30 Sekunden Belichtungszeit begrenzt sind, gibt es ein Limit von etwa 288 Gesamtpixeln, die in dieser Zeit angezeigt werden können. Das entspricht etwa einem 18 × 16 Bild. Für meine Bilder lud ich GIMP und erstellte sehr einfache Pixelbilder: einen Pokéball, ein Herz und einen springenden Mario. Diese drei Bilder legte ich in ein Verzeichnis namens „bitmaps“ im Stammverzeichnis der SD-Karte. Das Programm liest alle Bilder aus diesem Ordner.
Malprogramm
Da Schrittmotoren kein internes Positionierungs-Rückmeldesystem haben, müssen ihre Positionen per Software verfolgt werden. Das von mir geschriebene Programm behält die LED-Position mit einem Gittersystem bei, um einfaches Skalieren zu ermöglichen. Beim Start setzt der Arduino Mega die Schrittmotorpositionen auf 0, 0 und findet und liest das erste Bild. Dann blinkt die LED fünfmal, um dem Fotografen zu signalisieren, dass es bald losgeht. Die Bitmap wird zeilenweise gelesen: Für jede Zeile wird jede Spalte gelesen. Anhand der aktuellen Zeile und Spalte können die Schrittmotoren auf diese Positionen bewegt werden. An jeder Position wird die LED auf die Farbe des entsprechenden Pixels geändert.
(Re-)Erstellen eines Bildes
Nach Einlegen der SD-Karte und Anschließen einer 12V-Stromquelle für die Motoren war es Zeit, die Maschine einzuschalten. An meiner Kamera stellte ich 20 Sekunden Belichtungszeit, Blende F36, ISO 100 und eine Belichtungskorrektur von -5 Stufen ein, um Geistereffekte zu minimieren. Das erste gezeichnete Bild war ein Pokéball:
Obwohl es etwas unscharf ist, ist die Form noch klar erkennbar. Dann erstellte es ein Herz-Bitmap:
Da dieses Bild nur 9 × 9 Pixel groß war, ist jedes einzelne Pixel viel weniger definiert. Zuletzt malte ich ein Bild von Mario springend:
Dieses Bild zeigt starke Geistereffekte, hauptsächlich aufgrund der vielen hellen Pixel.
Zukünftige Verbesserungsideen
Die Light Paintings waren besser als ich anfangs dachte, aber es gibt noch Verbesserungspotenzial. Das Hauptziel wäre, die Unschärfe zu reduzieren, indem die LED im Dunkeln bewegt wird und nur leuchtet, wenn sie stillsteht. Diese Technik würde die Schärfe der nachgebildeten Bilder erheblich verbessern.
Code
Light Painting Programm (C/C++)
// Bitmap-Lesefunktion teilweise von Adafruit
#include <SD.h>
#include <SPI.h>
#include "DRV8825.h"
#define MOTOR_STEPS 200
#define RPM 150
#define MICROSTEPS 4
// Pin-Definitionen
#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 // 1 oder -1 für Richtungsänderung
#define Y_DIR_FLAG 1
#define STEPS_PER_MM (3.75 * MICROSTEPS)
#define SPACE_BETWEEN_POSITIONS 5 // 5mm pro Bewegung
#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);
}
CAD-Dateien
Benutzerdefinierte Teile und Gehäuse
Schaltplan
Herstellungsprozess
- Warum Designer zu natürlichen Materialien im Produktdesign zurückkehren
- LED mit PIR-Bewegungsmelder am Raspberry Pi steuern
- FMEA meistern: Schritt-für-Schritt-Anleitung für Instandhaltungsmanager zum Aufbau und Nutzung der Fehlermöglichkeits- und Einflussanalyse
- Temperatur- und Luftfeuchtedaten zwischen zwei Arduinos via Firebase übertragen
- Zwei Python-Variablen tauschen ohne temporäre Variable – Mühelos
- Arduino Coffin Dance Melodie-Projekt – Viralen Hit nachbauen & abspielen
- Alte Fernbedienungen revitalisieren: DIY Smart Devices mit Arduino bauen
- Arduino Rover per Firmata & Xbox One Gamepad steuern
- Soundgesteuerte 8-LED-Lichtanzeige mit Arduino
- Intelligenten Arduino-Vierbeiner-Roboter bauen: Schritt-für-Schritt-Anleitung