Benutzer-Werkzeuge

Webseiten-Werkzeuge


Seitenleiste

projektesose18:rembrandtpublic:start

Einleitung

Unser Roboter ist nach Rembrandt van Rijn benannt, einem bekannten Künstlers des Barock. Er trug mit dazu bei, dass die Kunst in den Alltag der breiten Mittelschicht Einzug fand. In seinen frühen Anfängen entstand eine Reihe von Radierungen, in denen er sich experimentell mit verschiedenen Gesichtsausdrücken darstellte.

Wir haben das als Motivation genommen einen Roboter zu bauen, der Bilder verarbeiten und dann die Kanten des Bildes nachzeichnen soll. Dazu muss der Roboter Bilder aufnehmen können, sie dann so bearbeiten, dass er die Kanten erkennt und diese dann auf ein Blatt Papier zeichnet. Zur Bildaufnahme benutzen wir eine Webcam und die Bildbearbeitung und -analyse läuft über einen angeschlossenen Computer. Der Roboter besteht aus zwei Wagen, die durch Steppermotoren bewegt werden. Durch die Ansteuerung dieser Motoren wird dann ein Bild gezeichnet.

Zur Bedienung von Rembrandt 2.0 müssen Arduino und Webcam an den Computer angeschlossen und das Steckbrett über ein Netzteil mit 8V Spannung versorgt werden. Die Wagen sollten sich an der linken oberen Blattecke (aus Sicht des/der Portraitierten) befinden und an der Wäscheklammervorrichtung am oberen Wagen muss ein Filzstift befestigt werden, sodass die Spitze möglichst senkrecht auf dem Papier aufsitzt. Wird das Programm gestartet, erscheint ein Livebild der Webcam, sodass man Gelegenheit hat, den Bildausschnitt zu optimieren. Zum Starten muss lediglich die Enter-Taste gedrückt werden. Es erscheint das Kantenbild, welches der Roboter nun beginnt zu zeichnen. (Theoretisch sollte der Fortschritt dieses Prozesses über Einfärbung der bereits gezeichneten Pixel visualisiert werden. In der Realität kann man das Bild aber nur aktualisieren, indem man das Canvas-Fenster für einen Augenblick aus dem Bildschirmbereich hinauszieht.) Für Abbruch und Neustart kann die Entf.-Taste gedrückt werden.

Umsetzung

Überblick

Die Aufgaben, die wir während unsere Projektphase zu erfüllen hatten, waren der Umbau des bestehenden MazeMate-Aufbaus, die Bildaufnahme und -bearbeitung, eine Extraktion der Zeichenbefehle aus dem bearbeiteten Bild und die Kommunikation zwischen Processing und Arduino. Das Programm an sich besteht aus sechs Teilen: Dem Hauptdokument rembrandt.pde, mit den Unterprogrammen edgeDetection.pde für die Bildbearbeitung, sketchEdges.pde für die Extraktion der Zeichenbefehle, die dann über moveTo.pde an das Arduinoprogramm rembrandt.ino übertragen werden und signature.pde, welches am Ende eine eigene Signatur hinzufügt. Im folgenden werden die einzelnen Aufgaben und Teilprogramme näher erläutert.

Umbau

Wir haben den Roboter MazeMate verwendet und ihn an unsere Anforderungen angepasst. Ursprünglich war noch ein Elektromagnet dabei, den haben wir ausgetauscht mit einer Vorrichtung aus Wäscheklammern, die einen Stift halten kann.

Abbildung 1: Aufbau des Roboters

Das Kernstück der Konstruktion ist der Wagen mit dem Stifthalter. Er besteht aus zwei prinzipiell baugleichen Ebenen. Sie werden durch Stepper-Motoren mit Zahnrädern auf Zahnschienen bewegt. Damit nicht das gesamte Gewicht des Wagens auf den Zahnrädern am Stepper liegt, laufen beide Teile auf jeweils vier Rädern. Um den Wagen geradlinig bewegen zu können, werden die Räder durch kleine Hölzer in der Spur gehalten. Dabei fährt die untere Ebene neben dem Zeichenblatt auf der Grundplatte und die obere um 90 Grad gedreht auf der unteren. Oben auf dem kleinen Teil des Wagens ist das Steckbrett mit der kompletten Schaltung des Roboters angebracht. Dazu gehören die beiden Stepper für die Bewegung und ein weiterer Stepper, der die Stifthalterung bewegt, so das der Stift auf- und abgesetzt wird.

Auf der Grundplatte ist neben dem Zeichenblatt und dem Wagen noch Platz für einen Laptop. Dieser wird benötigt, da der Arduino nicht genug Rechenleistung für Bildverarbeitung bzw. Processing hat. Also ist der Laptop mit der Webcam und dem Arduino verbunden. Außerdem benötigt die Schaltung ein Netzteil oder einen Akku, da der Arduino nur mit 5V läuft, die Stepper jedoch 8V benötigen.

Liste der Bauteile:

  1. Holzplatte 50x80cm
  2. 3x Kanthölzer à 50cm
  3. 3x Winkel
  4. Schrauben und Kabelbinder
  5. 8x Rad mit 3cm Durchmesser
  6. Holzplatte für untere Ebene 40x15cm
  7. Holzplatte für obere Ebene 15x15cm
  8. Hölzer zur Spurhaltung 200cm
  9. Zahnschiene à 25cm
  10. 3x Stepper
  11. 2x Zahnrad
  12. Steckbrett
  13. Arduino Nano
  14. 3x Treiber für Stepper
  15. 3x Kondensator
  16. Kabel
  17. Webcam
  18. Netzteil
  19. Wäscheklammern
  20. Holzplatte als Zeichenfläche 29x39cm
  21. Zwei A4-Moosgummi-Blätter als weiche Unterlage
  22. Filzstift und Papier

Pins

Tabelle 1: Pinbelegung
Bauteile Signalfunktion Pinnummer
Stepper bewegt in y-Richtung Direction D2
StepD3
Stepper bewegt den Stift Direction D4
StepD5
Stepper bewegt in x-Richtung Direction D6
StepD7

Bildbearbeitung

Das Teilprogramm EdgeDetection dient der Hervorhebung der Kanten in einem ihm übergebenen Foto. Es basiert auf dem von John Francis Canny entwickelten Canny-Algorithmus und besteht aus mehreren Teilschritten:

  1. Graustufenfilter
  2. Rauschentfernung mit Gaußmaske
  3. Gradientenberechnung mit Sobelmasken
  4. Winkelbestimmung
  5. Non-Maximum Suppression
  6. binärer Schwarz-Weiß-Filter
Abbildung 2: Von links nach rechts: Originales Graustufenbild, Bild nach Rauschentfernung, Gradientenbild, Bild nach Non-Maximum Suppression, binäres Schwarz-Weiß-Bild

Eine Anleitung und Hintergrundinformationen zum Algorithmus haben wir Andreas Cobet: Detektion und Erkennung von Text in Videos mit niedriger Qualität entnommen.

In allen unten aufgeführten Teilschritten wurden die Änderungen während des Durchlaufs nicht am Originalbild vorgenommen, sondern in eine Kopie übertragen, mit welcher am Ende das Originalbild überschrieben wurde. Im Folgenden werden nur die Schritte 2-5 erläutert, da der Graustufenfilter sowie der binäre Schwarz-Weiß-Filter bereits in Processing enthalten waren.

Rauschentfernung mit Gaußmaske

Bevor das in Graustufen überführte Bild auf seine Kanten untersucht werden kann, muss sichergestellt werden, dass jegliches zufällige Bildrauschen unterdrückt ist. Es könnte sonst zur Erkennung von Kanten führen, wo eigentlich keine Kanten sind.

Dies geschieht mithilfe einer Gaußmaske (siehe Abb. 3), d.h. einer 5×5-Matrix, deren Einträge anschaulich eine Gaußglocke bilden (siehe Abb. 4). Die Gaußglocke wird auch gauß'sche Normalverteilung genannt und beschreibt in der Stochastik die Wahrscheinlichkeitsdichte einer Zufallsverteilung, die durch eine Vielzahl voneinander unabhängiger Einflüsse entsteht.

Abbildung 3: Gausmaske
Abbildung 4: Gaußglocke (Bildquelle)

Für das Bildrauschen wird angenommen, dass die Helligkeit eines Pixels dieser Verteilung gehorcht. Mit der größten Wahrscheinlichkeit entspricht der Grauwert des Pixels der tatsächlichen Helligkeit (Maximum der Gaußglocke), doch es besteht auch eine gewisse Wahrscheinlichtkeit, dass der Pixel durch Störfaktoren heller oder dunkler abgespeichert wurde.

Die Rauschentfernung wird nun realisiert, indem das Bild Pixel für Pixel durchlaufen wird: Über jeden Pixel wird dabei die Gaußmaske gelegt, sodass der zentrale Wert „15“ genau über dem betrachteten Pixel liegt. Es befinden sich somit 25 Pixel unterhalb der Gaußmaske, wobei jedem einzelnen ein Gaußwert zugeordnet werden kann. Der Grauwert des zentralen Pixels wird nun neu berechnet, indem für jeden der 25 Pixel der Grauwert mit dem jeweilligen Gaußwert multipliziert, und am Ende alles summiert wird. Der neue Wert für den zentralen Pixel entspricht somit einem gewichteten Mittelwert seiner Umgebung. Zuletzt wird noch durch 159 geteilt, damit das Resultat im erlaubten Grauwertbereich von 0 bis 255 liegt.

In Abb. 5 wird der Algorithmus anschaulich: Der Pixel im Zentrum der Gaußmaske ist durch Störeffekte bei der Aufnahme dunkler als seine Umgebung, hat also einen niedrigeren Grauwert. Mit der Maske wird nun ein gewichteter Mittelwert gebildet, der auch die umliegenden, helleren Bildpunkte miteinbezieht. Obwohl dem zentralen Pixel die größte Gewichtung zukommt, erhält er nach Durchlauf der Maske durch seine hellen Nachbarn einen höheren Grauwert.

Abbildung 5: Die Gaußmaske wird über den zu betrachtenden Pixel gelegt und ein gewichteter Mittelwert gebildet.

Gradientenberechnung mit Sobelmasken

Stellt man sich das Bild als ein Gebirge vor, wobei der Grauwert die Höhe angibt, dann sind die weißen Bildbereiche die Berggipfel und die schwarzen Bereiche die Täler. Auf der Suche nach Kanten interessieren aber weder die Gipfel noch die Täler, sondern das dazwischen. Eine Kante wird man dort finden, wo das Gelände sehr steil ist, d.h. wo sich ein schneller Farbverlauf von hell nach dunkel befindet.

Ein nützlicher Indikator für die Größe der Steigung ist der Gradient. Für eine Funktion, die jedem (x|y)-Punkt eine Höhe z zuordnet, ist der Gradient an der Stelle (x|y) der Vektor (Gx|Gy), der in Richtung des größten Anstiegs zeigt und dessen Länge eben jenem Anstieg entspricht. In Abb. 6 ist ein solches Gradientenvektorfeld dargestellt: Für jeden Punkt des „Gebirges“ gibt es in der x-y-Ebene einen Vektorpfeil, der fort vom Miminum und in Richtung des größten Anstiegs zeigt.

Abbildung 6: Beispiel eines Gradientenfeldes Bildquelle

In der Mathematik wird der Gradient über die partiellen Ableitungen gebildet; da das „Gebirge“ eines Bildes jedoch aus diskreten Grauwerten besteht und nicht stetig ist, wird eine andere Methode verwendet: die Anwendung von zwei Sobelmasken, eine für den Gradientenanteil in x- und eine für den in y-Richtung. Die Sobelmasken sind 3×3-Matrizen, die in Abb. 7 gezeigt werden.

Abbildung 7: Sobelmasken für x- und y-Richtung

Wie auch schon bei der Gaußmaske werden die Sobelmasken auf den zu betrachtenden Pixel gelegt und ein gewichteter Mittelwert mit den umliegenden Pixeln gebildet. Die Beträge beider Resultate werden dann addiert und dem zentralen Pixel als neuer Grauwert zugewiesen. Ist dieser Wert betragsmäßig sehr groß, liegt der Pixel höchstwahrscheinlich auf einer Kante. Dabei muss darauf geachtet werden, dass der Wert nicht größer als 255 sein darf.

Anhand von Abb. 8 lässt sich gut erkennen, wieso die Sobelmasken tatsächlich die Gradientenlängen berechnen: Exemplarisch wird nur der Anteil in x-Richtung berechnet. Der zentrale Pixel unter der Maske befindet sich auf einer Kante, er sollte also einen großen Wert zugeordnet bekommen. Der eigene Grauwert spielt keine Rolle, er wird mit 0 multipliziert. Viel wichtiger sind die Grauwerte der Pixel links und rechts: links werden sie negativ gewichtet, rechts positiv. Sind die Helligkiten links und rechts ähnlich, so heben sich die Produkte auf. Der zentrale Pixel bekommt einen kleinen Wert zugewiesen. Besteht aber eine große Helligkeitsdifferenz zwischen links und rechts, wird das Ergebnis entweder sehr positiv oder sehr negativ. Im Beispiel aus Abb. 8 besteht zwischen den Nachbarpixeln links und rechts sowie oben und unten ein großer Unterschied. Für die Sobelmasken in x- und y-Richtung kommen also stark von 0 abweichende Werte heraus. Die Summe ihrer Beträge wird sehr hoch sein, sodass der Pixel als zur Kante gehöhrend markiert wird.

Abbildung 8: Die Sobelmaske für die x-Richtung wird auf den zu betrachtenden Pixel gelegt und ein gewichteter Mittelwert gebildet.

Winkelbestimmung

Durch die Sobelmasken wurde bisher nur der Gradientenbetrag für jeden Bildpunkt berechnet. Im weiteren Verlauf von EdgeDetection wird jedoch auch die Richtung des Gradienten, bzw. sein Winkel zur Horizontalen benötigt.

Dieser lässt sich leicht über trigonometrische Beziehungen berechnen: In Abb. 9 sind die beiden Gradientenanteile Gx und Gy senkrecht als die Katheten eines rechtwinkligen Dreiecks aufgetragen. Im vorhergegangenen Abschnitt wurde der Betrag des Gradienten als die Summe der Beträge der Gradientenanteile berechnet. Anhand der Abbildung wird jedoch klar, dass dieses Vorgehen nur eine Näherung war. Der tatsächliche Gradient ist nämlich die Hypothenuse des Dreiecks und sein Betrag lässt sich exakter über den Satz des Pythagoras berechnen. Für Rembrand 2.0 genügt jedoch die Näherung.

Abbildung 9: Mit den beiden Anteilen Gx und Gy kann über den Tangens der Winkel α bestimmt werden.

Der Winkel α kann nun durch die Tangens-Beziehung zwischen Gx und Gy für jeden Pixel ermittelt werden. Für den Fall Gx=0 wird α=0° gesetzt, falls Gy ebenfalls 0 ist und α=90°, falls Gy ungleich 0 ist. Alle anderen Winkel können problemlos über die Formel in Abb. 9 errechnet werden.

In einem Pixelarray existieren jedoch nur vier Richtungen: horizontal, vertikal und zweimal diagonal (siehe Abb. 10). Deshalb muss der errechnete Winkelwert in eine dieser vier Kategorien eingeteilt werden. Befindet sich α im Intervall von -22.5° bis 22.5°, wird ihm der Winkel 0° zugeteilt. Ist α stattdessen zwischen 22.5° und 67°, wird die Richtung 45° vermerkt. Für α zwischen -22.5° und -67° beträgt der gemerkte Winkel -45° und für α < -67° und α > 67° wird es auf 90° gesetzt.

Abbildung 10: Mögliche Richtungen ausgehend von einem Pixel

Non-Maximum Suppression

Die im vorherigen Schritt berechneten Winkel werden nun für die Verdünnung der Kanten benötigt. Die Kanten aus dem bisher entstandenen Sobelbild sind noch sehr dick und teilweise unscharf. Ziel ist es aber, Kanten zu erhalten, die nur einen Pixel breit sind, sodass sich die Zeichenbefehle leichter extrahieren lassen.

Dafür wird Non-Maximum Suppression angewendet, eine Methode, in der die Kanten durchlaufen werden und alle Nicht-Maxima von der Kante gelöscht werden. Stellt man sich das Bild wieder als ein Gebirge vor, in dem nun die Kanten hohe Berge sind, läuft man in der Non-Maximum Suppression auf dem Bergrücken entlang und setzt alle Pixel links und rechts vom Bergrücken auf die Höhe 0.

In der Umsetzung bedeutet das, dass wieder alle Pixel durchlaufen werden und bei jedem Pixel die Richtung des Gradienten abgefragt wird. Dann wird der Grauwert des aktuellen Pixels mit denen der Nachbarn in Richtung des Gradienten verglichen. Ist der aktuelle Pixel der Hellste, behält er seinen Wert, ist aber einer der beiden anderen Nachbarn heller, wird der aktuelle Pixel auf 0 gesetzt.


Extrahierung der Zeichenbefehle

Das Teilprogramm sketchEdges nimmt das bearbeitete Bild aus edgeDetection entgegen und extrahiert daraus die Zeichenbefehle. Dafür wird das Bild auf der Suche nach einer Kante Pixel für Pixel durchlaufen. Wird ein Pixel gefunden, der auf einer Kante liegt, wie in Abb. 11 durch die rote Umrandung veranschaulicht, soll der Stift an die entsprechende Stelle auf dem Papier bewegt und abgesetzt werden. Dann werden alle acht Nachbarpixel in der vorgegebenen Reihenfolge betrachtet. Sobald einer gefunden wird, der sich ebenfalls auf der Kante befindet, wird der aktuelle Pixel von der Kante gelöscht, der Stift auf die Position des Nachbarpixels bewegt, wobei eine kleine Linie entsteht, und der Nachbarpixel wird der neue aktuelle Pixel. Gehört aber keiner der Nachbarpixel zu Kante, wird der aktuelle Pixel von der Kante gelöscht, der Stift hochgenommen und weiter nach der nächsten Kante gesucht.

Abbildung 11:

Übersichtlicher dargestellt funktioniert der Algorithmus wie folgt:

  1. durchlaufe das Bild Pixel für Pixel
  2. findest du einen Pixel A, der auf einer Kante liegt, dann:
    1. bewege den Stift zur entsprechenden Stelle und setze ihn ab
    2. betrachte die Nachbarpixel in einer zuvor festgeschriebenen Reihenfolge
    3. findest du einen Nachbarpixel B, der ebenfalls zur Kante gehört, dann:
      • bewege den abgesetzten Stift zur entsprechenden Position
      • lösche A von der Kante und mache B zum aktuellen Pixel A
      • gehe zurück zu Schritt 2b
    4. gehört keiner der Nachbarpixel zur Kante:
      • lösche den Pixel A von der Kante
      • hebe den Stift wieder an
      • gehe zurück zu Schritt 1

Obwohl die Grundstruktur klar und einfach erscheint, ist die Implementierung nicht ganz so leicht. Das Durchlaufen der Bildpunkte geschieht mithilfe zweier for-Schleifen (Schritt 1). Innerhalb dieser wird für jeden Pixel mit einer if-Bedingung abgefragt, ob er sich auf einer Kante befindet (Schritt 2). Ist dies der Fall werden die Befehle für die Bewegung des Stifts zur entsprechenden Stelle gegeben. Diese Befehle müssen nur einmal ausgeführt werden, sobald eine Kante gefunden wurde. Befindet sich der Stift bereits auf einer Kante, sind sie überflüssig. Deshalb wird im folgenden eine while-Schleife implementiert: Solange man sich auf einer Kante befindet, sollen für den aktuellen Pixel alle Nachbarn betrachtet werden. Dies wiederum geschieht ebenfalls mit einer while-Schleife. Solange kein Kanten-Nachbar gefunden wurde, wird der nächste Nachbar betrachtet, bis keiner mehr übrig ist. Wird kein Nachbar auf der Kante gefunden, werden beide while-Schleifen verlassen und der Stift hochgenommen. Der Algorithmus fährt fort mit der nächsten Iteration der for-Schleifen. Wird jedoch ein Nachbar gefunden, der sich ebenfalls auf der Kante befindet, kann der Stift dorthin bewegt werden, aktueller und Nachbarpixel können aktualisiert werden und die Abbruchbedingung für die Nachbar-while-Schleife aktiviert werden. Im folgenden ist eine stark vereinfachte Version des Codes aufgeführt, um seine Struktur zu verdeutlichen.

// SCHRITT 1:
for (all lines) {                       
    for (all columns) {
        //SCHRITT 2:
        if (current pixel is on edge) {
            // SCHRITT 2a: Stift zur Kante bewegen und absetzen 
            onEdge = true;
            while (onEdge) {
                onEdge = false;
                // SCHRITT 2b:
                // Liste mit Indizes der Nachbarpixel erstellen
                int i = 0;   // Nummer des Nachbarpixels
                while (i<8) {
                    //SCHRITT 2c:
                    if (neighbors[i] is on edge) { 
                        // Stift dorthin bewegen, aktuellen Pixel aus der Kante löschen, Nachbarpixel wird neuer aktueller Pixel                  
                        i=8;  // Abbruchbedingung für die while-Schleife
                        onEdge = true;
                        }
                    }
                    i+=1;
                }
            }
            // SCHRITT 2d: aktuellen Pixel von der Kante löschen, Stift hochnehmen
        }   
    }
}

Da der Roboter unerwartet lange selbst für das Zeichnen eines nur kurzen Strichs benötigt, wurde der Algorithmus weiter ausgefeilt: Anstatt stets nur kurze Linien bei der Bewegung von aktuellem Pixel zum Nachbarpixel zu malen, sollen dort, wo die Kante gerade ist, gleich die gesamte Länge des Abschnittas auf einmal gemalt werden. In Abb. 11 beispielsweise soll die lange horizontale Linie nicht schrittweise sondern in einem Zug aufs Papier gebracht werden.

Dies lässt sich mit einer weiteren if-Bedingung und ein paar zusätzlichen Variablen realisieren: Hat der Algorithmus eine Kante gefunden, merkt er sich in jedem Durchlauf der äußeren while-Schleife die vorherige und die aktuelle Bewegungsrichtung. Erst wenn sie nicht übereinstimmen, zeichnet er die Linie. Für horizontale oder vertikale Abschnitte wird der Roboter also erst eine Linie ziehen, wenn er am Ende des Abschnitts angelangt ist, wo die Kante einen Knick macht.

Die moveTo-Befehle zur Bewegung des Stiftes werden im folgenden Abschnitt erläutert.


Kommunikation zwischen Processing und Arduino

Die moveTo-Funktion übermittelt zwei Zahlen an den Arduino: Die erste gibt die Anzahl der Schritte und die Richtung an in die sich der Motor bewegen soll und die zweite Zahl gibt an welcher Motor bewegt werden soll. Es gibt drei Motoren:

  • um den Wagen vor und zurück fahren zu lassen
  • um den Wagen nach links und nach rechts fahren zu lassen und
  • um den Stift zu bewegen

Dazu muss die moveTo-Funktion die erhaltenen Daten verarbeiten: einmal eine Pixelkoordinate und einmal ob der Stift, wenn er bewegt wird, auf- oder abgesetzt sein soll. Erst berechnet die Funktion, wie viele Schritte die einzelnen Motoren machen müssen und in welche Richtung. Danach wird wenn nötig der Stift bewegt. Wenn der Stift oben ist, dann werden die einzelnen Motoren nacheinander bewegt, wenn der Stift aufgesetzt ist, werden die Motoren in kleinen Schritten nacheinander bewegt, so das eine kleine Treppe entsteht, deren Stufen so klein sind, dass sie wie eine schräge Linie wirkt.

Schwierigkeiten haben wir damit, herauszufinden wie viel Pause man zwischen den einzelnen Bewegungen braucht, damit der Arduino nicht überfordert ist. Außerdem wissen wir nicht ob und welche Daten beim Arduino ankommen, was am Anfang auch zu Schwierigkeiten geführt hat. Weitere Schwierigkeiten gab es bei der gezielten Ansteuerung der Motoren und das sie auch die richtige Anzahl an Schritten machen bzw. das das Verhältnis der Schritte stimmt, so dass wenn der Stift aufgesetzt ist eine Linie entsteht die zwei Punkte ziemlich genau verbindet.

Viele anfänliche Probleme konnten wir durch ausprobieren und durch austausch mit anderen Gruppen gelösen z.B. die Ansteuerung der Steppermotoren. Bisher ist bei den Steppermotoren noch das Problem, die Pausen besser anzupassen, damit das zeichen flüssiger funktioniert und trozdem keine Fehler auftreten. Dazu müsste man viele Tests durchführen und viel Ausprobieren.
Wir wissen war immer noch nicht was genau beim Arduino ankommt und was nicht aber wir wissen ob was ankommt, wenn die LED am arduino aufhört zu blinken.
Das größe Problem ist die kleinen Abweichungen der Motoren einzuschäzten und auszugleichen, nur dazu müssten eine Kontrolle eingebaut werden und dazu reicht die Zeit leider nicht mehr.


Diskussion

Im Grunde haben wir alle wichtigsten Ziele unseres Projektes erfüllt: Rembrandt 2.0 kann Fotos aufnehmen, darin die Kanten erkennen und bei einfachen Bildern diese auch halbwegs erkennbar aufs Papier übertragen. Er zeichnet dabei weitestgehend Linien und keine einzelnen Punkte wie bei einem Matrixdruck. Zudem hat er eine eigene Signatur, die er am Ende zum Bild hinzufügt. Bei komplexeren Bildern funktioniert er jedoch nicht einwandfrei. Dies ist auf mehrere Probleme zurückzuführen:

Im Teilprogramm edgeDetection funktioniert die Non-Maximum Suppression nicht hundertprozentig. Einige wenige Kanten der Bilder - vor allem in Bereichen großer Kontraste - bleiben breiter als ein Pixel. Das führt dazu, dass der Stift die Kante mehrmals hoch und runter fahren muss, bis sie vollständig gelöscht ist, was zu Ungenauigkeiten und teilweise auch zum Auslaufen des Stiftes führt. Grund dafür ist vermutlich, dass die mit Sobel berechneten Gradientenbeträge manchmal größer als 255 sind und dann einheitlich auf diesen Wert gesetzt werden. So entstehen größere Flächen mit weißen Pixeln, in denen die Non-Maximum Suppression dann kein Maximum finden kann.

Eine Idee wäre es, die Gradienten genauer zu berechnen, einerseits durch Nutzung des Satzes des Pythagoras statt Addition der Beträge (siehe Abb. 9), andererseits durch ein Teilen durch 4, denn der maximal erreichbare Wert für den Gradienten beträgt 1020=4*255. Teilt man also durch 4, kann erreicht werden, dass die Gradientenwerte 255 nicht überschreiten und somit ein eindeutiges Maximum in einem Bereich mit hohen Gradienten errechnet werden kann.

Ein weiteres Problem der edgeDetection ist, dass manche Linien nicht durchgezogen sind. Durch häufiges Ab- und Aufsetzen des Stiftes kann es aber leicht zu Ungenauigkeiten kommen. Um dem vorzubeugen müsste anstelle des simplen Binärfilters am Ende ein stabiles Hystere-Verfahren implementiert werden. Dabei handelt es sich um eine Methode, die jeden Pixel anhand von zwei Schwellenwerten auf Zugehöigkeit zu einer Kante prüft. Er kann dabei nicht, vielleicht oder definitiv zu einer Kante gehören. Wenn er nur vielleicht zur Kante gehört, muss überprüft werden, ob er (direkt oder über andere vielleicht zur Kante gehörenden Pixel) mit einem definitiven Kantenpixel verbunden ist. Würden die Schwellenwerte gut gewählt, könnte erreicht werden, dass alle Kanten durchgezogen sind.

Dafür, aber auch für den Binärfilter wäre es zudem sinnvoll, die Schwellenwerte an die Helligkeit des Sobelbilds anzupassen, damit nicht zu viele aber auch nicht zu wenige Strukturen sichtbar sind. Momentan obliegt diese Optimierung noch dem Nutzer, z.B. durch Ausleuchtung des Raumes.

Hinzu kommt noch, dass es manchmal zu Kommunikationsaussetzern zwischen Arduino und Processing kommt. Wir vermuten, dass dies mit den Pausen zwischen den einzelnen Befehlen, die an den Arduino gesendet werden, liegt. Eine dynamische Anpassung der Pausen wäre also eine Möglichkeit, dieses Problem anzugehen.

Ein weiter Fehler tritt auf, wenn sich der Arduino in y-Richtung bewegen soll. Häufig legt er dabei nur die Hälfte des Weges zurück. Es könnte ein Motorfehler sein, da wir bei dem Code keinen Fehler gefunden haben.

Dadurch, das die Schreibunterlage nicht gerade ist ist die Druckstärke des Stiftes nicht konstant, kommt es zu kleinen Abweichungen im Bild: Mal ist die Reibung zwischen Stift und Papier so groß, dass für kleine Bewegungen die Kraft der Motoren nicht reicht, mal schwebt der Stift einen Millimeter über dem Papier. Das liegt aber vermutlich nicht nur an der Unterlage sondern auch an der Ungenauigkeit des Steppermotors, der den Stift steuert. Man müsste zwischendurch immer wieder kontrollieren ob der Stift in der Position ist, in der er sein soll, und/oder eine Stifthalterung bauen, die sich an den Druck anpassen kann. Dadurch würde es zu besseren Ergebnissen kommen.
Durch kleine Ungenauigkeiten der Motoren und dadurch, dass die Wägen ein wenig Spielraum haben, kommt es zu kleinen Fehlern. Wieder wäre durch eine Kontrolle während des Zeichnens eine Verbesserung des Ergebnisses möglich.

Bisher werden die Bilder Spiegelverkehrt gezeichnet, was auch dazu führt das die Unterschrift bisher über dem Bild Kopfüber steht. Durch Umstecken der Motoren oder der Änderung der Vorzeichen im Arduinoprogramm und dann kleinen entsprechenden Anpassungen könnte man dies ändern.

Code

Quellen und Literatur

  • Andreas Cobet: Detektion und Erkennung von Text in Videos mit niedriger Qualität. Doktorarbeit der Ingenieurswissenschaften, TU Berlin (2015), Dokument
  • Wikipedia (Hrsg.) Canny Edge Detector. Seite, in der Version vom 23.05.2018 (letzter Zugriff: 15.07.2018, 22:36 Uhr)
  • Processing Foundation (Hrsg.): Processing Reference. Seite (lezter Zugriff: 15.07.2018, 22:35 Uhr)
  • MateMate-Projekt aus dem vergangenen WiSe 2017/18, siehe: Projektdokumentation
projektesose18/rembrandtpublic/start.txt · Zuletzt geändert: 2018/11/09 11:00 von d.golovko