home     zurück
letzte Änderung am 06.03.2016

Kamera-Nachführung bei Bewegungserkennung V4


Vor dem Aufräumen des Programms möchte ich erstmal noch eine bessere Erkennung von illegalen bzw. fehlerhaft empfangenen Bildern haben.
Wieder hat mich eine Anfrage auf stackoverflow.com der Lösung näher gebracht bzw. eine deutliche Verbesserung ergeben.
Noch nicht 100% Erkennungsrate, aber sehr viel besser als vorher.
Dafür ist es nun allerdings erforderlich, dass ImageMagick installiert ist und schneller Festplatten-Platz existiert. Vorzugsweise also vom Typ tmpfs - alias RAM-Disk. Also etwa so:
sudo mount -t tmpfs -o size=10% none /ramdisk
...um unterhalb von /ramdisk ein File-System im RAM zu erzeugen, auf das mit Benutzer-Rechten schreibend zugegriffen werden kann.

Nun kann ein neuer Datentyp CamImage erstellt werden, der einerseits mehrere Aufbereitungsformen des Bildes aufnimmt und zusätzlich eine Prüffunktion enthält, mit der fehlerhaft empfangene Bilder erkannt werden können.
class CamImage:
  def __init__(self):
    cam=IP_Cam()
    self.valid=None
    tst=None
    while tst is None:
      self.raw, self.pil=cam.getImage()
      try:
        tst=self.pil.convert('L')
      except:
        print("error - reload image")
        tst=None
    self.u8=np.asarray(tst, np.uint8)
    self.s16=np.int16(self.u8)

  # ###########################################################
  # Liefert False, wenn das Bild in "self.raw" fehlerhaft ist.
  def isValid(self):
    if self.valid is None:
      t=datetime.datetime.utcnow().strftime("%Y%m%d-%H%M%S-%f")
      fn="/ramdisk/%s__corrupt.jpg"%(t,)
      f=open(fn, "wb")
      f.write(self.raw)
      f.close()
      #if ord(self.raw[-2:-1])!=255 or ord(self.raw[-1:])!=217: # FF D9
      if self.raw[-2:]!=bytes([255, 217]):
        self.valid=False
        print("FF D9")
      else:
        rc=subprocess.call(["identify", "-verbose", fn],
                              stdout=subprocess.DEVNULL,
                              stderr=subprocess.DEVNULL)
        self.valid=(rc==0)
        if not self.valid:
          print("identify")
      if(self.valid): # temp. not remove corrupt images
        os.remove(fn)
    return(self.valid)

  # ###########################################################
  # Speichert "self.raw" unter dem Dateinamen "filename".
  def save(self, filename):
    f=open("/ramdisk/%s.jpg"%(filename,), "wb")
    f.write(self.raw)
    f.close()

Wobei isValid() final natürlich eher so aussehen sollte:
  def isValid(self):
    if self.valid is None:
      if self.raw[-2:]!=bytes([255, 217]):
        self.valid=False
      else:
        fn="/ramdisk/__tmpimg.jpg"
        f=open(fn, "wb")
        f.write(self.raw)
        f.close()
        rc=subprocess.call(["identify", "-verbose", fn],
                              stdout=subprocess.DEVNULL,
                              stderr=subprocess.DEVNULL)
        self.valid=(rc==0)
      os.remove(fn)
    return(self.valid)


Weitere Spielereien haben mich zu dem Schluss kommen lassen, dass ein Histogramm besser als die einfache Summe geeignet ist, um Bewegung im Gesamtbild zu detektieren. Ebenfalls zur Auswahl des bewegtesten Matrix-Elements, das in die Bildmitte gebracht werden soll, eignet sich das Histogramm besser.
Die neue Funktion ist trivial und areasByBrightness() ändert sich nur geringfügig.
# ###########################################################
# Liefert ein Histogramm von dem Bild "img" als Array mit
# vier Elementen. Vorne die Anzahl der hellen Pixel, hinten
# die Anzahl der dunklen.
def histogram(img):
  h, b=np.histogram(img, [0, 60, 120, 180, 255])
  return(h[::-1])

# ###########################################################
# Liefert 0 bis zu 35 Tupel aus (Histogramm, x, y) für das
# in eine 7x5-Matrix zerlegte Bild "arr" als Liste.
# Histogramm ist ein Array von vier Werten, bei der
# jeder Wert der Pixel-Anzahl einer bestimmter Helligkeit
# entspricht. Die hellsten Pixel stehen im ersten Wert, die
# dunkelsten Pixel im letzten.
# Die Rückgabe-Liste ist absteigend nach den ersten zwei
# Werten von Histogramm sortiert.
def areasByBrightnessV2(arr, cx=7, cy=5, w=91, h=96):
  ret_lst=list()
  for x in range(cx):         # über die Segmente auf der X-Achse
    for y in range(cy):       # über die Segmente auf der Y-Achse
      seg=arr[y*h:y*h+h, 1+x*w:1+x*w+w] # Segment ausschneiden
      br=histogram(seg)       # Histogramm erstellen
      if br[1]>0:              # nur zum Ergebnis zufügen, wenn hell genug
        ret_lst.append((br, x, y))
  f=lambda x:(x[0][0]*2+x[0][1])
  return(sorted(ret_lst, key=f, reverse=True))


Von areasByBrightnessV2() wird eine Liste von Tupeln geliefert, die z.B. so aussieht:
[(array([  10,  201,  458, 8067]), 4, 0), (array([   9,  189,  631, 7907]), 5, 0), (array([   0,   31,  197, 8508]), 4, 1), (array([   0,    5,   94, 8637]), 6, 0), (array([   0,    1,   11, 8724]), 5, 2)]
Das ist so zu interpretieren, dass im Element(4, 0) 10 Pixel oberhalb von 180 und 201 Pixel oberhalb von 120 (und unter 180) vorhanden sind.
Im nächsten Element(5, 0) kommen ebenfalls 9 sehr helle und 189 helle Pixel vor. In den drei restlichen Elementen sind es nur vereinzelte Pixel. Die 30 nicht gelisteten Elemente (7*5-5=30) enthalten ausschließlich Pixel mit einer Helligkeit unter 120.

Wegen der korrigierten bzw. jetzt erst korrekt arbeitenden Funktion differenceImage() müssen Schwellwerte angepasst werden.
Als da wären:
Das war schnell dadurch erledigt, dass ich mir ein paar Werte für ein unbewegtes Differenz-Bild und für ein bewegtes angesehen habe.
In Bildern ohne Bewegung gibt es nur das Grundrauschen der Kamera und das spielt sich primär unterhalb einer Helligkeit von 60 ab.
Auf das Gesamt-Differenz-Bild bezogen wird Bewegung angenommen, wenn mindestens 100 Pixel >120 oder 20 Pixel >180 sind.
Erst jetzt wird mittels isValid() geprüft, ob die beiden ursprünglichen Bilder überhaupt korrekt sind. Nur wenn beide Bilder nicht gestört sind, wird auf "Bewegung erkannt" umgeschaltet.
Bei erkannter Bewegung wird nun das Differenz-Bild in eine 7x5-Matrix zerlegt und die Helligkeitsverteilung der 35 einzelnen Elemente ermittelt.
Zu hohe Helligkeit in mehr als 28 Elementen wird als Störung gewertet (verwackelt oder Helligkeitsanpassung).
Dann wird die Helligkeitsverteilung im hellsten Element darauf hin geprüft, ob mindestens ein Pixel heller als 180 und 100 Pixel heller als 120 sind.
Ist das der Fall, wird dieses Element in die Bildmitte gebracht (mehr oder weniger).

Die neue main() sieht derzeit so aus:
if __name__=="__main__":
  DEBUG=False
  nervous=False
  cam=IP_Cam()

  img1=CamImage()

  while True:
    motion_detected=False # True, wenn Bewegung erkannt wurde
    movement_enable=True  # False, wenn nicht neu Positioniert werden soll
    get_new_image=False   # True, um das aktuelle Bild zu verwerfen
    time.sleep(0.1)
    img2=CamImage()

    diff=differenceImageV6(img1.u8, img2.u8)
    h=histogram(diff) # zur Bestimmung, ob im Bild genug Bewegung vorkommt

    if h[0]>20 or h[1]>100:
      # Wenn das Differenzbild mindestens 100 Pixel oberhalb von 120
      # oder mindestens 20 Pixel oberhalb von 180 enthält, dann hat
      # möglicherweise Bewegung stattgefunden.

      if img1.isValid() and img2.isValid():
        # Wenn keins der beiden Bilder defekt empfangen wurde, dann
        # könnte das tatsächlich Bewegung sein.
        motion_detected=True
      else:
        get_new_image=True
        if DEBUG: print("fehlerhaft empfangenes Bild")

    if motion_detected:
      t=datetime.datetime.utcnow().strftime("%Y%m%d-%H%M%S-%f")
      if DEBUG:
        cam.saveImage(img1.u8, "%s-1"%(t,))
        cam.saveImage(img2.u8, "%s-2"%(t,))
        cam.saveImage(insertRaster(diff), "%s-3"%(t,))
        #cam.saveImage(img2.pil, "%s-4"%(t,), raw=True)
        img2.save("%s-5"%(t,))
        print("\ndiff.hist=%s"%(h,), t)
      else:
        img2.save("motion-%s"%(t,))
   
      abb=areasByBrightnessV2(diff) # Bild in eine 7x5-Matrix zerlegen
      if len(abb)==0: # doch nicht genug Bewegung
        motion_detected=False

    if motion_detected:     
      if DEBUG: print("len(abb)=", len(abb), abb)
      hist, x, y=abb[0]

      if not nervous:
        if x in (2, 3, 4) and y in (1, 2, 3):
          # wenn neue Position in der Bildmitte liegt -> nicht neu positionieren
          movement_enable=False
          if DEBUG: print("bereits mittig")

      if movement_enable:
        if len(abb)>28: # mehr als 28 Elemente über Grundrauschen
          # Zu viele Bereiche über Grundrauschen bedeutet üblicherweise:
          # - ein Bild ist entweder verwackelt oder
          # - die Kamera hat die Helligkeit angepasst.
          # Das Differenzbild ist jedenfalls unbrauchbar.
          movement_enable=False
          get_new_image=True
          if DEBUG: print("verwackelt", "*"*40)

      if movement_enable:
        if not (hist[0]>1 and hist[1]>100):
          # im anzufahrenden Bereich ist nicht genug Helligkeit
          movement_enable=False

    if motion_detected and movement_enable:
      cam.setLight(0)
      cam.gotoMatrixElement(x, y)  # neue Position anfahren
      time.sleep(0.2)              # auf Stillstand der Kamera warten
      cam.setLight(2)
      get_new_image=True

    if get_new_image:
      img2=CamImage()              # Bild von der neuen Position holen
    img1=img2

Was mich noch ein bischen stört, ist, dass ich anhand des Differenz-Bildes noch nicht beurteilen kann, ob ein helles Element aus dem älteren oder neueren Bild entstanden ist. Könnte ich das unterscheiden, könnte ich die Richtung einer Bewegung besser beurteilen bzw. das hellste Element in die Bildmitte bringen, bei dem die Bewegung aus dem neueren Bild stammt.

...geht also noch weiter mit der Erkennung der Bewegungsrichtung.