home    zurück
letzte Änderung am 24.02.2016

Positionen anfahren (1)

Heute sollen Positionen mit der Kamera angefahren werden.
Zu Beginn werden erstmal ein paar Rahmenbedingungen bestimmt.

Ein voller Schwenk von maximal links nach maximal rechts dauert
Von Hand gemessen und natürlich ohne Stopp zwischendurch.
Weil mir eine Minute eigentlich schon viel zu lange ist, stelle ich PT-speed=1 ein.

Die Freiheitsgrade der Kamera habe ich mittels folgender Skizze ermittelt:
Skizze der Kamera-Winkel

Der Schwenkbereich umfasst ziemlich genau 270° bezüglich der Objektiv-Mitte (von a nach b).
Der vom Objektiv selbst abgedeckte Winkel beträgt etwa 40° (c bis d). Den habe ich dadurch ermittelt, dass ich ein Objekt ins Bild geschoben und die Position markiert habe, ab dem das Objekt auf dem "Live Video" sichtbar wurde.

Nun probiere ich es zunächst mal mit Start-Stopp-Betrieb.
Und da möchte ich wissen, wie viele Schritte ich von ganz links nach ganz rechts brauche, wenn ich die Kamera in der kleinsten Schrittweite bewege.
Wobei kleinste Schrittweite heißt, den Stopp-Befehl unmittelbar nach dem Start-Befehl zur Kamera zu senden.

Die Funktionen dafür sehen so aus:
class IP_Cam:
  def __init__(self):
    self.CAM_AUTH="camuser:pwd4Cam"
    self.snapshotURL="192.168.178.36/snapshot.cgi"
    self.ctrlURL="192.168.178.36/decoder_control.cgi?command=%d"

    self.http=urllib3.PoolManager()
    self.headers=urllib3.util.make_headers(basic_auth=self.CAM_AUTH)
    self.UP=0
    self.DOWN=2
    self.LEFT=4
    self.RIGHT=6

[...]

  # ###########################################################
  # Liefert einen Snapshot aus Grauwerten als NumPy-Array.
  def getImageGrayscale(self):
    try:
      img=self.getImage().convert('L')
    except:
      #print("error - retry")
      img=self.getImage().convert('L')
    return(np.asarray(img))

  # ###########################################################
  # Bewegt die Kamera "duration" Sekunden lang in die Richtung
  # "direction".
  def gotoDirection(self, direction, duration):
    r=self.http.request('GET', self.ctrlURL%(direction), headers=self.headers)
    if duration<0:
      return
    elif duration>0:
      time.sleep(duration)
    r=self.http.request('GET', self.ctrlURL%(direction+1), headers=self.headers)

  # ###########################################################
  # Bewegt die Kamera in Grundstellung.
  def gotoCenter(self):
    r=self.http.request('GET', self.ctrlURL%(25), headers=self.headers)
    while not self.hasStopped():
      time.sleep(0.5)

  # ###########################################################
  # Liefert True, wenn zwei Bilder, die 100ms nacheinander
  # von der Kamera gelesen wurden, annähernd gleich sind.
  def hasStopped(self):
    p1=cam.getImagePattern(cam.getImageGrayscale())
    time.sleep(0.1)
    p2=cam.getImagePattern(cam.getImageGrayscale())
    return(self.compareImagePattern(p1, p2))

Wenn das WebGUI der Kamera geöffnet ist, braucht ein kompletter Schwenk deutlich weniger Schritte. So etwa 500 zu 840 ohne WebGUI.
Bedeutet also, das die einzelnen Schritte offenbar größer sind. Das wiederum heißt, dass der Stopp-Befehl nach dem Pan-Befehl wohl später ankommen muss.
Macht Sinn. Mehr Verkehr auf der Leitung bremst die Zustell-Geschwindigkeit der Befehle.
Wird statt http://192.168.178.36/IPCameralive.htm die Seite http://192.168.178.36/videostream.cgi angezeigt, ändert sich nix an der Schritt-Anzahl.
Also wenigstens greift das WebGUI dann wohl nicht aktiv ein.

Das war jetzt bei LAN-Verbindung. Auf dem Switch meldet sich die Kamera übrigens mit "100M Full".
Dann jetzt mal den gleichen Spaß über WLAN und mit WebGUI. Meldet etwa 550 Schritte. Ohne WebGUI etwa 830 Schritte.

Die Kamera antwortet aufs Pings über LAN nach 0.373 ms bis 0.445 ms, Pings über WLAN brauchen zwischen 2.77 ms und 10.0 ms.
Etwas sonderbar. Da würde man doch eigentlich erwarten, dass die Schrittanzahl bei WLAN im Vergleich zu LAN deutlicher abweicht.


Ermittelt habe ich die Schrittzahl-Werte mit einem Script, dass zuerst maximal nach links schwenkt, ein Bild LM macht, dann maximal nach rechts schwenkt und noch ein Bild RM macht.
Danach wird die Kamera in Einzelschritten wieder solange nach links bewegt, bis das aktuelle Bild dem Bild von Bild LM entspricht. Dann wieder zurück nach rechts, bis das aktuelle Bild dem Bild von Bild RM entspricht.


Bildvergleich

Die Erkennung, wann ein Bild einem anderen Bild entspricht, ist mit zwei Funktionen implementiert. Sobald mindestens eine von Beiden Gleichheit erkennt, werden die Bilder als gleich angenommen.
Die erste dieser Funktion arbeitet mit der Grauwert-Summe des Differenz-Bildes (siehe Bewegungserkennung) und meldet Gleichheit bei einem Wert <5.000.000.
Bei der zweiten Funktion wird ein Bild in eine 8x8-Matrix zerlegt und der mittlere Grauwert jedes dieser 8x8 Bildausschnitte bestimmt. Diese 64 Grauwerte bilden zusammen ein Pattern, das von einer zweiten Funktion mit einem anderen Pattern verglichen werden kann. Gleichheit wird dann gemeldet, wenn alle 64 Grauwerte des einen Patterns um maximal einen definierbaren Wert von dem Grauwert des anderen Patterns im selben Bildausschnitt abweichen.
Bilder werden also dann als gleich erkannt, wenn die Helligkeitsverteilung in allen 64 Bildausschnitten ähnlich ist.

class IP_Cam:
[...]
  # ###########################################################
  # Liefert für ein Grau-Bild in "imgAsArray" eine Liste mit
  # 64 Werten als Pattern. Die Werte entsprechen dem Mittelwert
  # des jeweiligen Bildausschnittes.
  def getImagePattern(self, imgAsArray):
    pat=np.empty([8, 8], dtype=np.int8) # Array anlegen
    arrsh=np.hsplit(imgAsArray, 8)      # Bild horizontal splitten
    for h in range(len(arrsh)):         # über 8 Teilbilder
      arrsv=np.vsplit(arrsh[h], 8)      # Teilbild vertikal splitten
      for v in range(len(arrsv)):       # über 8 Teilbilder
        #self.saveImage(arrsv[v], "b%d%d"%(h,v))
        pat[v, h]=np.mean(arrsv[v])     # Bild-Mittelwert ins Array[y,x]
    return(pat)                         # Array als Pattern liefern

  # ###########################################################
  # Vergleicht die zwei Pattern "pat1" und "pat2" und liefert
  # True, wenn der mittlerer Grauwert jedes Bildausschnitts
  # in beiden Pattern maximal um "maxdelta" voneinander
  # abweicht.
  def compareImagePattern(self, pat1, pat2, maxdelta=2):
    return(np.max(np.absolute(pat1-pat2))<=maxdelta)


Intern wird ein Bild also erstmal in eine 8x8-Matrix zerlegt:































































Von jedem dieser 64 Teilbilder wird nun der mittlere Grauwert bestimmt.
Das Script sieht das Bild dann quasi folgendermaßen:































































Allerdings braucht das Script keine 64 Bilder mit jeweils einheitlich grauem Inhalt.
Der einzelne Grauwert langt vollkommen.
Intern sieht das Pattern zu obigem Bild von meinem Schreibtisch daher eher so aus:
200 234 195 118 118 169 245 226
239 246 195 150 154 145 225 127
231 238 187 155 112 79 189 120
155 196 158 84 99 58 148 136
78 111 109 65 85 52 86 69
71 105 97 58 68 57 76 47
66 94 90 66 68 61 85 133
56 64 51 100 97 134 125 136
Und eigentlich stimmt nicht einmal das, weil die Werte vom Typ np.int8 statt np.uint8 sind (damit der np.absolute() das gewünschte Ergebnis liefert, werden vorzeichenbehaftete Eingangs-Arrays gebraucht). Daher werden alle Werte >127 real als negative Zahlen dargestellt.


Positionen anfahren (2)

Jetzt bin ich aber von der eigentlichen Aufgabe abgedriftet.
Primär wollte ich ja Positionen anfahren - und nicht zwischen den linken und rechten Endpunkten hin- und herfahren.
Die bisherige Erkenntnis ist, dass
Daraufhin habe ich mal einen Test-Aufbau für Winkel-Fehler-Messungen eingerichtet.
Vor der Kamera befindet sich in 16cm Entfernung ein Zollstock - weitestgehend rechtwinklig zur Mittelachse der Kamera.
Den rechten Bereich vom Zollstock habe ich beim besten Willen nicht richtig scharf bekommen. Weder Abstandsänderung, mehr Licht oder hellerer Hintergrund haben zu einer Verbesserung geführt. Das muss eine Macke der Kamera sein.

Nun wird per Script ein Bild c1 aufgenommen, 0.1 Sekunden gewartet, noch ein Bild c1a aufgenommen, die Kamera dann drei Sekunden lang nach links geschwenkt, 0.1 Sekunde gewartet, die Kamera drei Sekunden nach rechts geschwenkt, 0.2 Sekunden gewartet und dann ein Bild c2 aufgenommen.

Test-Aufbau für
        Winkel-Fehler-Messungen
Bild c1

Bild c2
Bild c2
Wie man vage erkennen kann, beträgt die Veränderung knapp 2 Millimeter (nicht immer - das Script lief sicher 10x mit geringerer Abweichung).
Bei Bild c1 werden an den Bildrändern die Werte 88,9 und 100,5 angezeigt.
100,5-88,9=11,6 cm.
Der Abstand Zollstock-Objektiv beträgt 16 cm.
Um auf ein rechtwinkliges Dreieck zu kommen, wird die 11,6 halbiert.
a=5,8 cm
b=16 cm
tan(α)=a/b -> tan(α)=5,8/16=0,3625 -> α=tan-1(0,3625)=19,93°
Da war meine Skizze von oben mit 40° für den doppelten Winkel ja schon ziemlich nah dran.

Um den Fehler bei der Positionierung zu berechnen, wird a=2 mm bzw. a=0,2 cm eingesetzt.
tan(α)=0,2/16=0,0125 -> α=tan-1(0,0125)=0,72°
Wie gesagt: passiert nicht jedes mal, aber passiert.
Und 0.72° Abweichung nach einem kurzen vor- und zurück-Schwenk ist einfach ungenügend.

Weiterhin hat das Script noch die Differenz-Bilder von (c1 - c1a) und (c1 - c2) erzeugt und gespeichert.
Zwischen c1 und c1a wurde die Kamera nicht geschwenkt. Dieses Bild zeigt also das (geglättete) Grundrauschen.
Differenz-Bild mit Grundrauschen
Differenz-Bild (c1 - c1a) = Grundrauschen

Zwischen c1 und c2 wurde die Kamera geschwenkt. Dieses Bild zeigt den (geglätteten) Fehler bei der Positionierung.
Differenz-Bild nach
        links-rechts-Bewegung
Differenz-Bild (c1 - c2)
Ganz schön viel Action in dem Bild für 0,72°.

So weit....so traurig.
Mal sehen, wie genau die Kamera ihre Grundstellung wieder trifft.

Das zugehörige Script ruft gotoCenter() auf, wartet 0.2 Sekunden, holt ein Bild, fährt 3 Sekunden nach links, ruft wieder gotoCenter() auf, wartet 0.2 Sekunden, holt noch ein Bild und berechnet aus den beiden Bildern dann das Differenz-Bild.
Ein gotoCenter() benötigt übrigens 55 Sekunden, wenn in Grundstellung gestartet wird.
Auch hier ist das Differenz-Bild keine wahre Freude.
Differenz-Bild Grundstellung 2x
        anfahren
Differenz-Bild: Grundstellung 2x anfahren

Dieses Differenz-Bild hat deutlich mehr waagerechte helle Bereiche, als das vorige Differenz-Bild.
Daraus kann man ableiten, dass der Fehler mehr in der vertikalen, als in der horizontalen Achse auftritt.
Sehr schön erkennbar ist der Versatz der beiden Ausgangs-Bilder in der Bildmitte an den Aufklebern meiner Schubladen-Pappkartons: in der Vertikalen fast eine komplette Aufkleber-Höhe, in der Horizontalen aber nur ca. 10% der Aufkleber-Breite.
Die Pappkartons sind etwa 3 Meter von der Kamera entfernt, die Aufkleber sind 7x3,5 cm groß. Da könnte man jetzt die Winkel des horizontalen und des vertikalen Fehlers draus berechnen. Aber wozu....

Es nützt nix -> das Ding ist nicht präzise zu steuern.

Diese Erkenntnis ließ sich leider auch nicht mit ein paar Tests mit gespeicherten Positionen widerlegen. Das WebGUI der Kamera bietet die Möglichkeit, bis zu 15 Positionen zu speichern und diese später wieder anzufahren. Da die gespeicherten Positionen auch nach zwischenzeitlichem stromlos-machen der Kamera noch verfügbar sind, deutet das für mich darauf hin, dass die Kamera durchaus Möglichkeiten hat, eine definierte Position anhand von Koordinaten anzufahren. Nur leider liegen zwischen gespeicherter Position und wieder angefahrener Position gerne mal Welten bzw. gefühlt mehrere Grad Abweichung auf beiden Achsen. Ergo: selbst ein komplett unbrauchbar wäre gepralt.

Also werde ich jetzt mittels rumprobieren und Pi-mal-Daumen schauen, wie ich die 16 Bildausschnitte aus der Bewegungslokalisation jeweils in die Bild-Mitte bekomme.

Auf einem DIN A4-Blatt wird mit Bleistift-Strichen die 4x4-Matrix gezeichnet und deren jeweilige Mittelpunkte markiert.
Dann wird Blatt und Kamera so ausgerichtet, dass das "Live Video" das Blatt Bildschirm-füllend anzeigt.
Den Mittelpunkt des Bildes habe ich mir auf dem Bildschirm dadurch markiert, dass ich die Ecke meines Geany- bzw. Editor-Fensters über dem Web-Browser mit dem Kamera-WebGUI entsprechend platziert habe.

Dabei kam heraus, dass (bei geöffnetem WebGUI) für ein halbes Feld in der Waagerechten 0.86 Sekunden und für 1.5 Felder 2.3 Sekunden geschwenkt werden muss. Ein halbes Feld in der Senkrechten braucht 0.23 Sekunden, 1.5 Felder 0.57 Sekunden.
Praktischerweise können die Kombi-Bewegungen LEFT_UP, RIGHT_UP, LEFT_DOWN und RIGHT_DOWN mit den gleichen Werten gefüttert werden.

Nun werde ich mal die ganzen bisher entstandenen Funktionen zusammenführen, um daraus ein Script zu bauen, dass
Bewegungs-Nachführung