Malen mit Licht: Lightpainting

Texte und Bilder malen mit Licht. Diese Idee ist nicht neu:
Man nimmt eine Kamera, stellt diese auf Langzeitbelichtung und malt mit einer Taschenlampe, oder einer LED, Bilder in die Luft. Funktioniert bestimmt super, vorausgesetzt man kann gut zeichnen.
Kann ich aber leider nicht, also bewunderte ich die Leute einfach und fand mich damit ab, selbst so etwas nie machen zu können. Aber vor zwei Jahren, auf dem 30c3 (Chaos Computer Congress) gab es einen Stand von Blinkinlabs. Die Jungs hatten mehrere Kartons voll mit Arduinos und LED-Streifen im Gepäck. Ich kaufte damals so ein Set und fand raus, dass man damit Bilder abblinken konnte. Leider ging der Streifen nach ein paar Tagen und Testaufnahmen kaputt. Blöd, wenn da Wasser dran kommt...

Seit gefühlten Ewigkeiten und zig tausend anderen Ideen lag der Streifen in irgendeiner Schublade und verstaubte. Zwar stand die Reperatur, bzw. der Nachbau auf einer ToDo-Liste, aber ziemlich weit unten.

Seit Anfang des Jahres filme ich manche Projekte mit einer GoPro und lade einen kurzen Zusammenschnitt bei Youtube hoch. Bisher mit mäßigem Erfolg, aber es macht mir Spass und ab und an wird der ein oder andere Zuschauer durch die paar Videos auf meinen Blog aufmerksam. Immerhin ein paar Klicks. Coole Sache, Parker.
Das änderte sich jedoch schlagartig, als ich das erste Projekt für's Kliemannsland hochlud: Es wurde via Twitter "geliked" und die Leute wurden aufmerksam. Plötzlich regnet es Abos und meine Videos finden Beachtung. Was ein tolles Gefühl. Endlich schauen unbekannte Menschen meine Videos, stellen Fragen, loben und geben Tipps (und beleidigen auch, aber das gehört wohl einfach dazu). Innerhalb einer Woche wurden aus 250 Abonnenten unfassbare 6000:

Zu dem Zeitpunkt dachte ich, dass sei jetzt das Ende der Fahnenstange und entschloss mich ein kleines "Danke"-Video zu machen (mittlerweile sind es jedoch bereits über 10.000). Es sollte etwas kleines sein. Maximal ein paar Stunden Arbeit. Wenig Vorbereitungszeit. Schnell etwas Cooles zaubern. Eben ein fixes Danke. Mir fiel der LED-Streifen von damals wieder ein: "Wäre doch cool, sich mit einem Bild zu bedanken."
Nachdem ich mir den Streifen genauer angeguckt hatte, stellte sich heraus, dass dieser zusätzlich an mehreren Stellen gebrochen war: Vmtl. hatte ich ihn in der Schublade über die letzten Jahre etwas öfters aus Versehen geknickt. Blöd. Also schnell selbst bauen. Ist ja auch nur ein Arduino mit 'nem bisschen Licht dran.
Die benötigten Teile hatte ich zum Glück alle da:

Teileliste:

  • 1x Arduino Nano
  • 60x WS2812B (RGB-LED Streifen)
  • 1x Taster
  • 1x LED
  • 1x Widerstand (220 Ohm)
  • 1x Widerstand (200 Ohm)
  • 1x Widerstand (4,7 kOhm)
  • 1x Kondensator (1000 µF, 6.3V (oder größer)
  • 1x USB-Buchse
  • 1x Powerbank

Die Einzelteile habe ich dann wie folgt erst einmal auf ein Steckbrett gehauen:



Kurze Erklärung:
Der Taster dient zum Starten bzw. Beenden der Animation. Dieser wird (wieder) mittels Pull-Up-Widerstand (~4,7 kOhm) an's Board geklemmt. Die LED zeigt an, dass der Arduino bereit ist und erlischt, solange die Animation läuft. Angeschlossen wird sie mit einem Vorwiderstand von mind. 220Ohm. Den Kondensator habe ich lediglich zur Absicherung des LED-Streifens integriert, um heftige, mögliche Spannungsschwankungen ausgleichen zu können. Der LED-Streifen benötigt seine 5V und wird mit nur einem Datenpin mit Infos versorgt.

Wie funktioniert das jetzt?
Das ist relativ simpel: Man nimmt sich ein Bild von 60 Pixeln Höhe und teilt dieses, statt wie üblich in Reihen, in Spalten auf. Diese Informationen werden auf dem Arduino gespeichert, der nach und nach die Spalten auf die Leiste schiebt und anzeigt. Den Streifen befestigt man mitsamt des Arduinos und Platine an einem Stock und läuft mit diesem vor einer Kamera entlang, welche eine "Langzeit"-Aufnahme macht.

Pattern Paint
Entweder man schreibt sich die Software zum Erzeugen der Spalteninformationen selbst, oder man nimmt eine fertige Lösung: Ich entschied mich für Pattern Paint. Das Programm ist genau dafür gemacht und OpenSource:

Bild umwandeln:

  • "Blinky Tape" auswählen:

  • "File -> Open -> Scrolling Pattern". Dort dann das gewünschte, 60 Pixel hohe, Bild öffnen:

  • Wenn das Bild erfolgreich geladen wurde, kann man es auch direkt schon exportieren und somit ein "Header"-File erzeugen:

Fertig. Pattern Paint hat alles erledigt.
Nun muss das allerdings noch auf den Arduino geschrieben werden.

Arduinogefrickel
Zum Ansteuern der LEDs habe ich mir "etwas" Unterstützung bei FastLED geholt. Das ist eine Bibliothek, die einem Schnittstellen zum vereinfachten Ansprechen des Strips anbietet. Top! Man muss auch nicht immer alles neu erfinden. Also: Runterladen und beispielsweise in den "Library"-Ordner der Arduino IDE schmeißen. (Der Ort unterscheidet sich je nach OS: Unter Windows ist dieser in den eigenen Dokumenten zu finden. Bei 'nem Mac im Programmverzeichnis selbst.)

Hier mal mein Geschreibsel als Beispiel:

/*
Lightpainting-Script
flazer.com/de

based on the work of blinkinlabs.com
*/

#include <FastLED.h>
#include <animation.h>

//SOME PATTERNS. COMMENT BACK IN, WHAT EVER YOU LIKE
#include "lamda.h"
//#include "mushroom.h"
//#include "turtle.h"
//#include "YOUR_PATTERN.h"

#define LED_COUNT 60*5
struct CRGB leds[LED_COUNT];

#define STRIP_OUT    5 // DATA PIN OF LED-STRIP
#define BUTTON_IN    3 // PIN OF START-BUTTON
#define STATUS_LED   2 // PIN OF STATUS-LED

#define BRIGHT_STEP_COUNT 5
int brightnesSteps[BRIGHT_STEP_COUNT] = {5,15,40,70,93}; // DIFFERENT STEPS OF BRIGHTNESS
int brightness = 4; // MAX BRIGHTNESS AS DEFAULT
int lastButtonState = 0; // LAST STATE OF BUTTON
int run_mode = 0; // CURRENT RUN MODE (BLINKING OR NOT)
int frameDelay = 30; // Number of ms each frame should be displayed.
int framesDone = 0; // COUNTER OF FINISHED FRAMES (COLUMNS)
boolean repeatAnimation = true; //SHOULD THE ANIMATION REPEAT

void setup()
{  
  LEDS.addLeds<WS2812B, STRIP_OUT, GRB > (leds, LED_COUNT);
  LEDS.showColor(CRGB(0, 0, 0));
  LEDS.setBrightness(brightnesSteps[brightness]);
  LEDS.show();
  
  pinMode(BUTTON_IN, INPUT);
  pinMode(STATUS_LED, OUTPUT);
  setLED(1);
}

void loop() {
  button();
  if(run_mode == 1) {
    animation.draw(leds);
    framesDone++;
  }
  
  //Stop all
  if(!repeatAnimation && framesDone >= frameCount) {
    stop();  
  }

  delay(frameDelay);
}

void stop() {
  animation.reset();
  run_mode = 0;
  framesDone = 0;  
  blackout();
  setLED(1);
}

void button() {
  int buttonState = digitalRead(BUTTON_IN);
  if((buttonState != lastButtonState)) {
    if(buttonState == 0) {
      if(run_mode > 0) {
        stop();       
      }else {
        run_mode = 1;
        setLED(0);
      }
    }
  }
  lastButtonState = buttonState;
}

void blackout() {
  for(int led = 0; led < LED_COUNT; led = led + 1) {
    leds[led] = CRGB(0, 0, 0);
  }
   LEDS.show();
}

void setLED(int status) {
  digitalWrite(STATUS_LED, status);
}

Achtung: Der Code ist nicht lauffähig. Hier fehlen noch die generierten .h-Files.
Hier kannst du meine Bilder runterladen, die ich zum Testen gebastelt habe: Download.
Einfach entpacken und zum Projekt legen.
Zusätzlich muss noch die "animation.h"-Datei hinzugefügt werden. Diese kannst du entweder von hier kopieren:

#ifndef ANIMATION_H
#define ANIMATION_H

#include <Arduino.h>
#include <FastLED.h>

class Animation {
 public:
  typedef enum {
    RGB24 = 0,
    RGB565_RLE = 1,
#ifdef SUPPORTS_PALLETE_ENCODING
    INDEXED = 2,
    INDEXED_RLE = 3
#endif
  } Encoding;

  // Initialize the animation with no data. This is intended for the case
  // where the animation will be re-initialized from a memory structure in ROM
  // after the sketch starts.
  Animation();

  // Initialize the animation
  // @param frameCount Number of frames in this animation
  // @param frameData Pointer to the frame data. Format of this data is encoding-specficic
  // @param encoding Method used to encode the animation data
  // @param ledCount Number of LEDs in the strip
  // @param frameDelay Number of milliseconds to wait between frames
  Animation(uint16_t frameCount,
            PGM_VOID_P frameData,
            Encoding encoding,
            uint16_t ledCount,
            uint16_t frameDelay);

  // Re-initialize the animation with new information
  // @param frameCount Number of frames in this animation
  // @param frameData Pointer to the frame data. Format of this data is encoding-specficic
  // @param encoding Method used to encode the animation data
  // @param ledCount Number of LEDs in the strip
  // @param frameDelay Number of milliseconds to wait between frames
  void init(uint16_t frameCount,
            PGM_VOID_P frameData,
            Encoding encoding,
            uint16_t ledCount,
            uint16_t frameDelay);
 
  // Reset the animation, causing it to start over from frame 0.
  void reset();
  
  // Draw the next frame of the animation
  // @param strip[] LED strip to draw to.
  void draw(struct CRGB strip[]);

  uint16_t getLedCount() const;
  uint16_t getFrameCount() const;
  uint16_t getFrameDelay() const;

 private:
  uint16_t ledCount;              // Number of LEDs in the strip
  uint16_t frameCount;            // Number of frames in this animation
  uint16_t frameDelay;            // Milliseconds to wait between frames

  Encoding encoding;              // Encoding type
  PGM_VOID_P frameData;           // Pointer to the begining of the frame data
  
  uint16_t frameIndex;            // Current animation frame
  PGM_VOID_P currentFrameData;    // Pointer to the current position in the frame data

#ifdef SUPPORTS_PALLETE_ENCODING
  uint8_t colorTableEntries;      // Number of entries in the color table, minus 1 (max 255)
  struct CRGB colorTable[256];    // Color table

  void loadColorTable();          // Load the color table from memory
#endif

  typedef void (Animation::*DrawFunction)(struct CRGB strip[]);
  DrawFunction drawFunction;

  void drawRgb24(struct CRGB strip[]);
  void drawRgb565_RLE(struct CRGB strip[]);

#ifdef SUPPORTS_PALLETE_ENCODING
  void drawIndexed(struct CRGB strip[]);
  void drawIndexed_RLE(struct CRGB strip[]);
#endif
};

#endif

...oder du ziehst dir das File aus dem Git-Repo von Blinky-Tape.
Wenn alles richtig gemacht wurde, dann ist das Script nun kompilierbar und blinkt das Lambda-Symbol in einer Endlosschelife ab.

Ein kleiner Hack für zwischendurch:
Das vorab generierte Pattern ist leider nun nicht ganz funktionsfähig. Das liegt allerdings daran, dass irgendwann anscheinend die Library geupdated wurde, nicht aber Pattern Paint. Das wurde wohl schlichtweg vergessen. Nun hatte ich dann zwar "schnell" einige Hacks gebastelt und Pattern Paint umgebogen, fand dann aber einen simpleren Fix, indem ich mein Script abänderte und die generierte Datei etwas ergänzte. Geht schneller und unkomplizierter:

In der generierten Datei die erste Zeile austauschen:

const uint8_t animationData[] PROGMEM = {

Ersetzen durch:

const int frameCount = 60;
const uint8_t animationData[] PROGMEM = {

Dabei gibt "frameCount" die Anzahl der Spalten an.

Die letzte Zeile muss ebenfalls geändert werden:

Animation animation(114, animationData, ENCODING_RGB24, 60);

Ersetzen durch:

Animation animation(frameCount, animationData, Animation::RGB24, 60, 33);

(Die 60 gibt hier die Höhe der Spalte an.)

Nun kann die Datei im .ino-Script eingetragen werden. Heißt:
lamda.h aus- und YOUR_PATTERN.h einkommentieren. Natürlich muss "YOUR_PATTERN" durch den gewählten Dateinamen ersetzt werden:

Das Script jetzt nur noch kompilieren und hochladen.

Powerbank
Mir wurde erst später bewusst, dass ich das Geraffel ja auch irgendwie noch mit Strom versorgen muss. Unterwegs ist das so schwierig mit einer Steckdose. Also die dicke "Anker"-Powerbank aus dem Regal gegriffen und angeschlossen. Allerdings passierte überhaupt gar nichts. Nach etwas googlen fand ich auch die Antwort:
Ein USB-Ladegerät gibt sich durch einen Widerstand unter 200Ohm zwischen den Datenleitungen zu erkennen.
Glück gehabt: Solche Größen habe ich da. Also einen Widerstand in Schrumpfschlauch verpackt und an den USB-Stecker gelötet. Perfekt. Funktioniert. Alles blinkt und bewegt sich.

Stock aka Besenstiel
Nachdem alles funktionierend auf meinem Schreibtisch fröhlich vor sich hin blinkte, mich das Projekt nun bereits über neun Stunden gekostet hatte, es kurz vor Mitternacht war und ich es unbedingt noch ausprobieren wollte, entschied ich mich für die simpelste Variante des Montierens und griff beherzt zu einem bunten Mix aus Gaffa, Tesafilm und Isolierband.
Resultat: Abenteuerlich aber es funktioniert und tut seinen Dienst. Außerdem ist es ja draußen eh dunkel. Da sieht das niemand:


Das Ergebnis lässt sich dann sogar (für eine solche Konstruktion) sehen, zumindest bin ich damit zufrieden:


Hier natürlich das gesamte Video mit dem fertigen Ergebnis: