RuZman

2073

Illustration

Abstract

Im vorherigen Artikel Leap Motion Java - Hand Tracking (1) wurde die Hand in der vertikalen Ebene getrackt. In diesem Teil wird die dritte Dimension hinzugefügt und die TrackingBox eingeführt. Jede Hand kann eine eigene TrackingBox besitzen und anhand dieser wird die Bildschirmposition berechnet. Das hat den Vorteil, dass bei mehrhändigen Applikationen sich die Hände nicht kreuzen müssen, um von der einen Seite des Bildschirms auf die andere zu gelangen. Die in diesem Teil erarbeiteten Klassen stehen unter dem Link HandTracking zum Download zur Verfügung.

TrackingBox

Die TrackingBox ist mit der Klasse InteractionBox vergleichbar. Im Gegensatz zu dieser muss die TrackingBox nicht das komplette Sichtfeld des Leap Motion Controllers einnehmen. Zudem kann die Position und der Schwerpunkt der Box frei gewählt werden. Der Standard-Konstruktor wird eine äquivalente Funktionalität wie die InteractionBox bieten. Applikationen, für welche mehrere Hände benötigt werden, waren der Grund für die Box und die Verschiebung des Ursprungs. Der Schwerpunkt wird gesetzt, um Zonen zu definieren. Bewegt sich die Hand zum Beispiel von der vorderen Zone (front) in die hintere (back), wird ein Event ausgelöst, welches für gewöhnlich als Klick interpretiert wird.

Neben den Standard-Konstruktor wird es noch zwei weitere geben. Bei einem wird nur die Dimension der Box angegeben und der Ursprung automatisch ermittelt. Lediglich die Interaktionshöhe ist mit 10cm fest im Programmcode hinterlegt. Beim anderen kann der Ursprung frei gewählt werden und das Setzen der Interaktionshöhe bleibt dem Entwickler überlassen. Der Umrechnungsfaktor step (Pixel pro zurückgelegte Millimeter) wird automatisch an die Bildschirmauflösung angepasst. Zusätzlich wird der Schwerpunkt centroid in die Mitte der Box gesetzt. Die obige Illustration stellt den Vektor centroid gestrichelt dar. Um sich das besser vorstellen zu können, folgen die Visualisierungen der beiden Konstruktoren:

TrackingBox Default-Konstruktor TrackingBox Default-Konstruktor

Wegen der hohen Abhängigkeit der einzelnen Instanzvariablen muss die Klasse TrackingBox sicherstellen, dass niemand auf die Referenzen der Vektoren zugreifen kann. Aus diesem Grund werden in den Settern eine Kopie der übergebenen Vektoren gespeichert. Normale Getter wird es nicht geben, da man bei jedem Aufruf eine neue Kopie erzeugen müsste. Stattdessen wird die TrackingBox Methoden zur Verfügung stellen, welche einen übergebenen Parameter mit den gewünschten Werten befüllen. Das folgende Klassendiagramm bietet einen groben Überblick über den Funktionsumfang der TrackingBox:

UML Klassendiagramm Tracking Box

TrackingBox - calcScreenPosition()

Für die Überführung von der Hand-Position in Bildschirmkoordinaten werden drei Vektoren benötigt. Der Hand-Position point, der Ursprung der Box origin und der Umrechnungsfaktor step. Außerdem muss die Y-Achse an das Koordinatensystem des Bildschirms angepasst werden. Die vier Abbildungen stellen den Ablauf der Transformation grafisch dar:

Erste Transformation des Koordinatensystems Erste Transformation des Koordinatensystems Erste Transformation des Koordinatensystems Erste Transformation des Koordinatensystems

Als Java-Implementierung:

	public void calcScreenPosition(Vector point, Vector position) {
		position.setX((point.getX()-origin.getX())*step.getX());
		position.setY(DISPLAY_HEIGHT - ((point.getY()-origin.getY())*step.getY()));
		position.setZ((point.getZ()-origin.getX())*step.getZ());
	}

TrackingBox - calcZone()

Mit der calcZone()-Methode kann die Zone bestimmt werden, in welcher sich die Hand befindet. Diese soll ein Intervall von [-1, 1] zurückliefern, wenn sich die Hand in der TrackingBox befindet. Dazu muss das Koordinatensystem in den Schwerpunkt der Box gelegt werden. Der Wert wird dann von der Hand-Position abgezogen. Am Ende wird das Ganze durch die doppelte Box-Größe geteilt, um das Intervall von [-1, 1] zu erhalten. Dadurch lässt sich relativ einfach bestimmen, auf welcher Seite die Hand liegt. Werte außerhalb des Intervalls bedeuten, dass sich die Hand nicht in seiner TrackingBox befindet. Auch hierfür gibt es eine Bilderreihe, welche die Transformation nochmals verdeutlichen soll:

Erste Transformation des Koordinatensystems Zweite Transformation des Koordinatensystems Dritte Transformation des Koordinatensystems Vierte Transformation des Koordinatensystems Fünfte Transformation des Koordinatensystems Sechste Transformation des Koordinatensystems

Um die Anzahl der Rechenoperationen nicht unnötig in die Höhe zu treiben, wurden einige Rechenoperationen zusammengefasst. Hier nochmal als Java-Implementierung:

	public void calcZone(Vector point, Vector zone) {
		zone.setX((point.getX()-centroidOrigin.getX())/halfBox.getX());
		zone.setY((point.getY()-centroidOrigin.getY())/halfBox.getY());
		zone.setZ((point.getZ()-centroidOrigin.getZ())/halfBox.getZ());
	}

TrackingBox - Potential

Die TrackingBox besitzt die Einfachheit der InteractionBox und kann zudem wesentlich flexibler genutzt werden. Für die ersten Programme sollte sie jedenfalls ausreichen. Verbesserungspotential gibt es hingegen bei der Fehlerbehandlung. Liegt die TrackingBox nicht im Sichtfeld des Leap Motion Controllers, wäre es angebracht, dass eine Exception geworfen wird. Zudem skalieren sich die Vektoren step und centroid nicht, wenn sich der Ursprung oder die Box-Größe ändern. Diese Punkte werden zu einem späteren Zeitpunkt umgesetzt.

PointListener

Neben der TrackingBox wird nun auch der erste Grundstein für Events gelegt. Ziel dieses Teils ist die Funktionalität anzubieten, dass beim Wechseln einer Zone ein Event ausgelöst wird und alle Listener benachrichtigt werden. In folgenden Artikeln wird das EventHandling erweitert und dynamischer gestaltet.

Über die MotionRegistry werden globale Events angemeldet. D.h. sobald sich eine Hand in das Sichtfeld des Leap Motion Controllers bewegt, werden bereits bekannte PointListener bei der neuen AbstractPoint-Instanz angemeldet. Jede AbstractPoint-Instanz besitzt seinen PointEvent. Sobald im PointEvent feststellt wird, dass sich die Hand in eine andere Zone bewegt, benachrichtigt die AbstractPoint-Instanz seine Listener. Der grobe Aufbau der Klassen sieht folgendermaßen aus:

UML Klassendiagramm - Ausschnitt EventHandling

PointEvent

Unabhängig von der Anzahl der Listener, hat jede AbstractPoint-Instanz nur eine PointEvent-Instanz. Da jeder Listener das selbe PointEvent bekommt, muss sichergestellt werden, dass keine Instanzvariablen verändert werden können. Des Weiteren müssen sich AbstractPoint und PointEvent den Vektor zone teilen. Für bestimmte Anwendungsfälle ist es interessant, was die vorherige Zone war. Diese wird zusätzlich gespeichert. Die Zonen werden in einem EnumSet gehalten, um leichter zwischen den Zonen differenzieren zu können. Zum Beispiel kann man nach dem vorderen und hinteren Teil abgefragen, ohne die Höhe oder Seite zu berücksichtigen. Ob eine Änderung stattgefunden hat, wird mit der update()-Methode ermittelt. Dazu wird der Vektor zone in ein EnumSet umgewandelt und mit dem aktuellen EnumSet verglichen. Sind beide identisch, passiert nichts. Ansonsten gibt die update()-Methode ein true zurück. So sieht Java-Implementierung aus:

public class PointEvent {
	public enum Zone {
		UP, DOWN, LEFT, RIGHT, FRONT, BACK, OUTSIDE;
	}

	private Vector zone;

	private EnumSet<Zone> buffer;
	private EnumSet<Zone> prevZones;
	private EnumSet<Zone> zones;

	private AbstractPoint source;

	public PointEvent(AbstractPoint source) {
		this.source = source;

		zone = new Vector();
		prevZones = EnumSet.of(Zone.OUTSIDE);
		zones = EnumSet.of(Zone.OUTSIDE);

		source.setZone(zone);
	}

	public boolean update() {
		buffer = EnumSet.of(
			getZone(zone.getX(), Zone.LEFT, Zone.RIGHT),
			getZone(zone.getY(), Zone.DOWN, Zone.UP),
			getZone(zone.getZ(), Zone.BACK, Zone.FRONT));

		if(!buffer.equals(zones)) {
			prevZones = zones;
			zones = buffer;
			return true;
		} else {
			return false;
		}
	}

	private Zone getZone(float value, Zone zone0, Zone zone1) {	
		switch( (int) (value + 1)) {
			case 0: return zone0;
			case 1: return zone1;
			default: return Zone.OUTSIDE;
		}
	}

	public boolean isInsideTrackingBox() {	
		return !zones.contains(Zone.OUTSIDE);
	}

	public boolean isInZone(Zone zone1) {
		return zones.contains(zone1);
	}

	public boolean isInZone(Zone zone1, Zone zone2) {
		return zones.contains(zone1) && zones.contains(zone2);
	}

	public boolean isInZone(Zone zone1, Zone zone2, Zone zone3) {
		return zones.contains(zone1) && zones.contains(zone2)
				&& zones.contains(zone3);
	}

	/** gleiche nochmal für wasIn... */

	public AbstractPoint getSource() {
		return source;
	}
}

Refactoring

Um Abhängigkeiten und damit Fehlerpotential zu verringern, wurde ein Refactoring durchgeführt. Eine wesentliche Änderung ist, dass jetzt der Aufruf Environment.init() ausreicht, um das komplette Toolkit zu initialisieren. Die MotionRegistry kümmert sich dann eigenständig, um die Verwaltung der Listener.

PointListener - Beispiel

Eine Demonstration des PointListener liegt im Package de.ruzman.sample002. Die Applikation zeigt eine Box mit der jeweiligen Zone an, in welcher sich die Hand befindet. Das Program funktioniert nur mit einer Hand korrekt und die Box / das Koordinatensystem sind nur zur schöneren Visualisierung vorhanden. Die Klasse Slick2D nutzt die Slick2D-Library zum Rendern. Die Klasse Swing nutzt AWT / Swing. Mit Alt + F4 wird die Applikation beendet.