Benutzer-Werkzeuge

Webseiten-Werkzeuge


Seitenleiste

projektewise18:icontactpublic:start

Dokumentation

Download our Code here: icontact_code.zip


Welcome to iContact !

Hier findet ihr die Dokumentation unseres Semesterprojekts iContact - einem Setup für eine Eingabe ohne Hände und Stimme.

Inspiriert wurde unser Projekt durch den Künstler Jason Becker, einem Musiker und Komponisten, der seit seinen 20. Lebensjahr an ALS leidet und er seitdem nur äußerst schwer seiner Leidenschaft weiter folgen konnte. Mithilfe einer Zeichensprache, die Freunde und Familie von seinen Augen ablesen können, ist er allerdings trotzdem immer noch in der Lage zu komponieren.

Unser Ziel ist es, ein Programm zu entwickeln, dass es dem Benutzer ermöglicht, mithilfe von Augenbewegungen Text einzugeben. Dies soll Menschen mit ALS (Krankheit, die zu einer Ganzkörperlähmung und zum Stimmverlust führt) helfen zu kommunizieren. Die Erfassung der Bewegung soll also automatisch geschehen.

Dazu soll mittels einer WebCam die Position der Augen ermittelt und die Bewegung der Pupillen aufgezeichnet und ausgewertet werden. Durch eine spezielle Zeichensprache sollen die Bewegungen in Buchstaben bzw. Befehle übersetzt werden. Optional können Bewegungen mit z.B. den Augenbrauen oder dem Mundwinkeln mit Sensoren wahrgenommen werden. Das werden wir bei ausreichender Zeit hinzufügen.

WebCam und Sensor anschließen, Programm starten, auf Kalibrierung warten und es kann losgehen. Der Nutzer blickt in verschiedene Richtungen und gibt mit jeweils einem Paar aus zwei Augenbewegungen einen Buchstaben ein. Eine zusätzliche Bildschirm-„Tastatur“ für Zahlen und Symbole, die Möglichkeit Dateien zu speichern (und Öffnen) sowie einige Optionen zur Konfiguration erweitern die Flexibilität. Programm abgestürzt, oder versehentlich beendet? Kein Problem, denn eine automatische Sicherung kann den Verlust wiederherstellen. Bei einer Schreibgeschwindigkeit von nur einigen Wörtern pro Minute kommt es auf jeden Absatz an!

Übrigens:

Diese Zeile wurde mit unserem Programm geschrieben (und hierher kopiert)!



Viele Probleme haben wir erst auf die eine, dann auf eine andere Weise gelöst. Am Anfang haben wir z.B. noch keine Gesichtserkennung von OpenCV verwendet, später nur noch Augenerkennung, dann sogar die Nase und zuletzt gar nicht mehr. Mittlerweile arbeitet unser Programm fast gänzlich ohne externe Libraries, außer Serial und Video für das Einlesen der Daten vom USB-Port und der WebCam.

Stattdessen hat es sich als nützlich erwiesen, selbst einige Klassen und eine Library zu schreiben - eigentlich sogar unbedingt notwendig, da unser Code schon nach anderthalb Monaten unglaublich lang und unübersichtlich zu werden drohte. Besonders kritisch sind natürlich die Aktionen in setup() und draw(), da manche Einstellungen hier kollidieren können und beide Methoden mittlerweile wahrscheinlich weit über 300 Zeilen hätten.

Ein paar Klassen sind hauptsächlich Wrapper für die vereinfachte und abgeschlossene Bedienung der Kamera und der Kommunikation mit dem Arduino. Andere, wie GData, der ECScreen (Eye-Control-Screens) und der EyeTracker sind viel umfangreicher: Sie dienen der Echtzeitbearbeitung der Kameradaten, der Darstellung der Eingabeanweisungen bzw. dem gewaltigen Prozess der Augenkalibrierung und -erkennung sowie dem Auswerten der resultierenden Daten. Die eigene Library ProcessingGUI ist entstanden, weil existierende GUI-Libraries uns nicht ausgereicht haben und generell zum Teil eher weniger zufriedenstellend und flexibel sind (unten mehr dazu).

Unser Projekt lässt sich zwar deutlich in verschiedene Phasen gliedern, aber an vielen Dingen haben wir parallel gearbeitet und einige Aspekte nebenher weiterentwickelt. So sind die Übergänge in den Projektphasen oft eher fließend.

Hier ein genauerer Überblick in unseren Arbeitsprozess und die einzelnen Schritte und Maßnahmen:

    Navigation:
  1. Gesichts- und Augenerkennung
  2. Live-Bewegung
  3. Biegemessstreifen
  4. Eye-Language
  5. Interface erstellen
  6. Extra - Automatische Wortvorgabe

Gesichts- und Augenerkennung

  1. Gesicht finden und erkennen
    • Als Erstes muss unser Programm ermitteln, ob jemand vor der Kamera sitzt und wenn ja, wo sich genau das Gesicht befindet. Zu Anfang haben wir dafür für OpenCV-Library verwendet. Diese (eigentlich für c++, aber von Greg Borenstein teilweise nach Java portiert) ist in der Lage, bestimmte Features aus einem Bild zu erkennen, z.B. Augen, Ohren, Nase und Gesichter.
    • Bald sind wir allerdings darauf umgestiegen, direkt nach den Augen zu suchen.
    • Später haben wir uns dann aber entschieden, OpenCV ganz aus dem Projekt zu verbannen, weil die Ergebnisse für unsere Zwecke nicht verlässlich und präzise genug waren (siehe weiter unten).
  2. Augenerkennung
    • Unser Plan war bereits von Anfang an, die Iris- (oder Pupillen-) position zu bestimmen und in Relation zur Augenposition zu setzen. Dafür verwenden wir die sog. Hough-Transformation, eine klassische (nicht auf Maschine Learning basierte) Methode für die Suche nach einfachen Strukturen in einem Bild. Besonders einfach sind Linien: an jeden Punkt des Bildes wird im Grunde eine Gerade mit allen möglichen Winkeln angelegt und geprüft, ob sie mit Teilen des Bilds übereinstimmt. Ähnlich funktioniert dies mit Kreisen, allerdings ist der Rechenaufwand deutlich höher, wenn die Größe des Kreises unbekannt ist.
    • Für diesen Test muss von dem Bildframe ein Gradientenbild erzeugt werden. Das ist quasi ein Kontrastbild, das an Stellen, wo der Kontrast (Helligkeitsübergang) im Originalbild hoch war, hell ist und an kontrastarmen Stellen dunkel ist. Mathematisch gesehen handelt es sich um eine Differenzierung (Ableitung) nach den zwei Veränderlichen x und y. Dafür gibt es verschiedene Methoden mit Matrizen, die auf jeden Punkt in Berücksichtigung auf seine Nachbarn angewendet werden. Die besten Ergebnisse lieferten hier der sog. Laplace-Operator und ein besonders einfacher (und rechenzeitsparende) Vergleich mit nur einem Nachbarpixel in x-Richtung.
  3. Abstände und Verhältnisse einrichten
    • Ein Problem ist ganz offensichtlich: Wenn man den ganzen Kopf zur Seite bewegt, kann das Programm dies nicht erkennen und meint natürlich, man hätte das Auge bewegt. Es ist also eine Referenz im Gesicht notwendig, mit der die Werte verglichen werden können. Als erstes haben wir versucht, die Augen oder Nase mittels OpenCV zu verfolgen, aber beides war zu ungenau und unterlag starken Schwankungen, was zu großen Fehlern geführt hat. Im Moment kleben wir uns einen farbigen Klebepunkt ins Gesicht, der diese Aufgabe übernimmt. Auch er wird durch die Hough-Transformation erkannt. Wenn das Programm gestartet wird, muss eine Kablibriering durchgeführt werden, bei der zuerst der Referenzpunkt gesucht und seine genaue Position und Größe in mehreren Wiederholungen bestimmt werden. Daraus lässt sich die ungefähre Augenposition errechnen, sodass Fehlerkennungen stark reduziert werden. Im nächsten Schritt muss der Abstand in x- und y-Richtung durch eine Reihe von Messungen sehr präzise ermittelt werden, da diese Relation die Basis für die Erkennung wärend der restlichen Laufzeit bildet (eine Neukalibrierung kann auch zu späterem Zeitpunkt gestartet werden).

Live Bewegung

  1. Pupillenposition dauerhaft überprüfen
    • Hier haben wir die oben genannten Varianten einfach auf jedes Bild angewendet um zu sehen, welche sich am besten verhält. Bei jeder Version fiel uns auf, dass das Programm manchmal die Augen komplett verliert und danach alle möglichen Richtungen angibt, was natürlich unerwünschte Ergebnisse produziert. Wenn man tatsächlich nur mit den Augen arbeiten kann, kann man einen Reset nicht durchführen, welcher aber unbedingt nötig ist. Wir gaben einfach dem Sensor die Funktion, dass ein sehr langes Biegen eine Rekalibrierung aufruft, womit man selbst im schlimmsten Fall immer noch mit dem Programm arbeiten kann, ohne es neu zu starten.
  2. Position der Pupille auf Bildschirm anzeigen
    • Zuerst wollten wir uns immer die Position des Auge anzeigen lassen. Aber diese Idee haben wir schnell verworfen, da wir mithilfe eines 3x3 Rasters arbeiten und nicht die richtige Position ermitteln, sondern nur eine relative Position innerhalb des Rasters berechnet wird. Zum Debugging haben wir uns immer zwei Zahlen ausgeben lassen. Die erste Zahl entsprach dem letzten besuchten Feld und die zweite Zahl den aktuellen Feld, z.B. für den Buchstaben t sind es 7 7 und bei a 0 1. Jedoch sind diese Zahlen nicht sonderlich hilfreich für den Verbraucher ;), sodass wir sie nicht mehr anzeigen.

-> gaze-plot.png

Biegemessstreifen

  1. Signale lesen und einbringen
    • Zunächst haben wir überprüft, wie sich der Biegemessstreifen überhaupt verhält, indem wir uns die gemessene Spannung mithilfe des seriellen Plotter anzeigen haben lassen. Dabei fiel uns auf, dass sich der Biegemessstreifen in beide Richtungen relativ gleich biegen ließ und auch entsprechend ähnliche Werte ausgab. Somit ist die Richtung egal. Beim Start wird eine einsekündige Eichung durchgeführt, und dann nimmt der Arduino Messwerte auf und sobald sich die Spannung vom Eichwert um die Toleranz unterscheidet, wird gezählt, wie lange dieser Zustand anhält. Es gibt drei Schwellenwerte, anhand deren unterschiedliche Signallängen registriert werden und entsprechend verschiedene Codes an den PC gesendet werden.
  2. Sensor am Gesicht anbringen
    • Zuerst wollten wir den Sensor am Mundwinkel befestigen (so wie bei Stephen Hawking), aber da der Sensor konkav oder konvex gebogen werden muss, reicht die doch recht schwache Bewegung nicht aus, um ein differenziertes Signal zu produzieren. Zudem war die Befestigung auch ein Problem, da es meistens unangenehm oder ineffektiv war. Ein Körperteil, das besser für diese Bewegung geeignet ist, ist der Finger und auf den sind wir dann ausgewichen.
  3. Arduino mit Processing verbinden
    • Über das serialEvent() kommunizieren der Arduino und das Processing-Programm. Zu Anfang wird ein sogenannter (hier eher einseitiger) Handshake durchgeführt, weil Processing nicht erkennt, ob an einem USB-Port wirklich der Arduino hängt und nicht zum Beispiel die Maus. Wenn der Arduino gestartet wird, sendet er einen String, über den Processing ihn identifizieren kann. Während der restlichen Laufzeit sendet der Arduino Bytes, die über den Status und Signale informieren. In die andere Richtung kann auch das Hauptprogramm seinerseits Aufrufe an den Arduino schicken und ihn eine Rekalibrierung, Änderung der Signalmindestlängen oder Pausierung durchführen lassen. Ein Standby wird durch eine rote LED angezeigt, Während der Kalibrierung blinkt die rote LED und die grüne LED zeigt Bereitschaft an.
  4. Schutzhülle
    • Leider ist der Biegemessstreifen recht sensibel. Die spezielle Beschichtung kann leicht abgerieben werden, allein schon, wenn er häufig gebogen wird. Um ihn nun zu schützen, haben wir den Biegemesstreifen und die Kabel durch einen Schnürsenkel gezogen, der nicht steif, aber lang genug ist und sich auf der Haut nicht unbequem anfühlt.

Eye-Language

  1. Eye-Language definieren
    • Ursprünglich wollten wir das Projekt für Menschen wie Jason Becker erstellen. Deswegen haben wir uns auch die für ihn entwickelte Zeichensprache angesehen und uns an dem Buchstabenraster orientiert.

      In dem Program gibt es verschiedene "Tabs" oder Screens. Der Hauptscreen ist für die Navigation zwischen den übrigen zuständig. Die einzelnen Aktionen, Befehle und Zeichen befinden sich in einem 3x3 Raster und man kann die Buchstabentastatur, Zahl-/und Symboleingabe, Dateifunktionen zum Öffnen und Speichern und die Einstellungen auswählen, indem man in die entsprechende Richtung guckt und den Sensor betätigt. Bei den Tastaturtabs erfolgt die Eingabe über eine Zwei-Komponenten-Aktion, bei der man zuerst in das Rechteck blickt, in dem sich der gewünschte Buchstabe befindet und dann in die Richtung, die der Position des Buchstabens in diesem Rechteck entspricht. So kann eine relativ große Menge an Zeichen auf einem Bildschirm umgesetzt werden.

Interface erstellen

  1. Zuerst haben wir die in Processing verfügbaren Methode text(x,y,String) verwendet, um die jeweilige Ausgabe darzustellen. Allerdings wollten wir gerne mehrzeiligen, umbrechenden und scrollbaren Text zeigen. Es gibt zwar Libraries für GUIs in Processing, aber sie sind ein wenig starr im Design und Textboxen, die in der Lage sind mehrzeiligen Text darzustellen gibt es überhaupt nicht. Aus dem Wunsch das ausführlicher zu machen ist dann die ProcessingGUI entstanden. Es gibt momentan Buttons, Menüs, Container, ScrollArrangements, Textboxen, Listen, Slider, Checkboxen und einige mehr. Die Komponenten lassen sich sehr frei designen und mittlerweile sogar automatisch animieren. Eventhandler wie mouse-enter / exit / press / release / scroll / drag / move und keypress können zu jedem Objekt hinzugefügt werden. Man kann nach wie vor die gewöhnlichen Zeichenfunktionen aus Processing mit der Oberfläche kombinieren.

    Die Textbox verhält sich schon fast wie man es erwartet: Es gibt einen blinkenden Cursor, der sich mit den Pfeiltasten verschieben lässt; mit der Maus kann der Cursor ebenfalls gesetzt werden und Text kann wie gewohnt ausgewählt werden. Copy/Paste/Cut Funktionen dürfen auch nicht fehlen...

    Für optimale Performance werden nur die Komponenten neu gezeichnet, die sich geändert haben (und ihre Eltern-Objekte), die Grafik aller Objekte wird stets in einer PGraphics gespeichert und beim Rendern abgerufen. Der Hauptcontainer, der sogenannte Frame, indem sich alle Objekte befinden müssen (nicht unbedingt direkt, denn durch Container sind auch Verschachtelungen möglich), verwaltet das Zeichnen, Aktualisieren, automatische Animieren und alle Events für die GUI. Alle Container Rendern wiederum ihre enthaltenden Objekt und leiten Maus- und Tastatur-Events an ihre Items weiter usw...

    Eine genauere Beschreibung der Funktionsweise würde allerdings hier den Rahmen sprengen. Bei Interesse einfach mal schreiben.

Extra - Automatische Wortvorgabe

  1. Für die Wortvorgabe hatten wir vor, eine Baumstruktur zu verwenden. In jedem Node (Knoten) steht genau ein Zeichen, sodass ein Pfad ein Wort ergibt. Jeder Node enthält außerdem eine Zahl, die eine Wertung für die Häufigkeit angibt. So werden häufige Wörter eher vorgeschlagen als selten verwendete.

    Ein Pfad kann auch mehrere Wörter enthalten: Eisenbahn → “Eis”, “Eisen”, “Eisenbahn”. Die Nodes mit den Zeichen ‘i’, ‘e’, ‘b’ etc. haben die Wertung 0 und zählen daher nicht als Wort (so wird zum Beispiel “Eisenb” nicht vorgeschlagen). Beispiel: Durch diese Struktur können wir “Eis”, “Eisen”, “Eisenbahn”, “Eisenach” und “Eisbein” mit nur (theoretisch) 16 Zeichen speichern. Außerdem wird das Suchen durch diese Ordnung stark beschleunigt, vor allem wenn sehr viele Wörter gespeichert sind. Da auch Wörter durch den Nutzer automatisch hinzugefügt und die Häufigkeiten reguliert werden, muss das Vokabular in eine Datei gespeichert werden. Die einfachste Idee ist, beim Speichern einfach alle Wörter zu bilden und als Zeilen zu speichern. Beim Öffnen des Programms wird die Datei eingelesen und alle Wörter neu zum Baum hinzugefügt. Es erweist sich aber als schwierig dabei effizient auch die Häufigkeiten zu berücksichtigen.

    Der zweite Ansatz baut auf dem Prinzip von xml-Dateien auf. XML-Dateien sind zum Beispiel so aufgebaut:
    <abschnitt>
      <part>
      </part>
    
      <part>
        <inhalt>
        </inhalt>
      </part>
    </abschnitt>
    
    Damit lassen sich sowohl Listen als auch Baumstrukturen realisieren, nur sind bei den vielen <>-Tags mit nur wenig Inhalt die ‘<’, ‘>’-Zeichen sehr verschwenderisch, daher sieht unser Ansatz deutlich anders aus: Aus der langen Struktur (hier ohne "Eisenach")...
    |<E> 
    |   <i> 
    |      |<s>
    |      |   <e> 
    |      |      <n> 
    |      |         <b> 
    |      |            <a> 
    |      |               <h> 
    |      |                  <n> 
    |      |                  </n> 
    |      |               </h> 
    |      |            </a> 
    |      |         </b> 
    |      |      </n> 
    |      |   </e>
    |      |   <b> 
    |      |      <e> 
    |      |         <i> 
    |      |            <n>
    |      |            </n>
    |      |         </i>
    |      |      </e>
    |      |    </b>
    |      |</s>
    |   </i>
    |</E>. 
    ...wird:
     E i s e n b a h n ////// b e i n /////// 
    Dabei schließt ein ‘/’ je einen Tag. Die Zahlen werden je hinter den Buchstaben geschrieben.
     E0 i0 s1 e0 n1 b0 a0 h0 n1 ////// b0 e0 i0 n1 /////// 
    Auch das lässt sich noch weiter komprimieren, zum Beispiel können alle Leerzeichen weggelassen werden, wenn die Zahlen durch Steuerzeichen ersetzt werden, die man nicht in normalem Text verwendet (So bleit die Möglichkeit erhalten, auch Ziffern in den "Wörtern" mitzuspeichern). Auch die Nullen (sind am häufigsten) können weggelassen werden, wobei unser Parser stets eine 0 annimmt, wenn keine Zahl dasteht.
    Sowohl das Abspeichern als auch das Einlesen und Suchen haben wir rekursiv gelöst, was hier übersichtlicher ist, als iterativ zu arbeiten.

    In einem kleinen Programm (auch im Code oben zu finden) haben wir das implementiert (Achtung, es wird wieder unsere GUI-Library verwendet). Beim Eingeben werden in der Liste unten Wörter vorgeschlagen, die beim Anwählen die Eingabe vervollständigen.


  2. Behälter für den Arduino
    • Nach einer Brainstorming Session standen wir vor zwei Optionen. Entweder wir würden eine Box mithilfe von einem 3D-Drucker fertigen lassen, oder selbst eine Box aus Holz bauen. Da die Kosten beim 3D-Druck allerdings voraussichtlich zu hoch werde würden, haben wir uns für die zweite Option entschieden. Die Box sollte leicht und klein sein, weshalb wir relativ dünne Holzplatten verwendeten. Diese sägten und schliffen wir passend und klebten sie mit Holzkleber daraufhin. Als Schließmechanismus entschieden wir uns für zwei Magneten, deren Befestigung aber ziemlich ermüdend war, weil die verschiedenen Kleber meistens nicht lange gehalten haben. Nach einigen Versuchen sind die Magnete aber an ihrem Stellen geblieben.
FM0   FC010000000:zzzzzz11d e3 00  0  09364fc fa14e1040 c3010d25f 55  0260 eb24c
 0  0  0  0 0 0 0 021361186110  032 020161186110  033 01ef61186110  0 c
01dd61186110  072 01cb61186110  033 01b961186110  033 01a761186110  033
019561186110  033 018361186110  033 017161186110  033 015f61186110  033
014d61186110  033 013b61186110  033 012961186110  033 011761186110  033
010561186110  033 0 f361186110  033 0 e161186110  033 0 cf61186110  033 0
bd61186110  033 0 ab61186110  033 0 9961186110  033 0 bf61186110  033 0
c761186110  034 0 cf61186110  034 0 d761186110  034 0 df61186110  034 0... AC Bscale(2.0) Dscale(2.0) enhenced_level(0) isOutdoor(0) result(1)   FM0  
FC001001100:zzzzzz01b f8 00  0  05f040045042c1040 c3010d25f 55  028a115276  0  0
 0  0 0 0 0 02765136511d  0 2 026551355118  0 2 0254512e510f  0 0 0243512d510e 
090 023251275103  0 2 022151295101  0 2 021051355101  0 2 01ff513a50ff  0 2
01ee514f5106  0 2 01dd51535102  0 2 01cc51745115  0 2 01bb518c5121  0 2
01aa51d9513e  0 2 0199520d515e  0 2 018852415175  0 2 017752915191  0 2
016652c551ad  0 2 0155532351dc  0 2 0144532851da  0 2 0133531c51e2  0 2
0155534e51e7  0 2 015d531d51de  0 2 016553b4522e  0 2 016d53c7523e  0 2...

Ausblick

  1. Ursprünglich hatten wir noch vor eine Sprachausgabe mit einzubringen, jedoch hat die Zeit nicht gereicht.
  2. Außerdem wäre es sehr interessant, noch einmal einen ganz anderen Ansatz zu probieren und mit maschinellem Lernen, statt mit Hard-Coding bei dem Erkennungsproblem zu arbeiten. Das würde sich bei der Sicherheit und Stabilität der Ergebnisse bei der Positionsauswertung sicher auszahlen.
projektewise18/icontactpublic/start.txt · Zuletzt geändert: 2019/04/08 12:32 von d.golovko