home     zurück
letzte Änderung am 05.03.2016

Umstellung auf np.int8 ?


np.uint8
Zunächst mal ein paar Überlegungen zu Bildern, die als np.uint8 vorliegen.
Wenn das Differenz-Bild gebildet wird, können Werte negativ werden. Weil np.uint8 negative Werte aber nicht darstellen kann, gibt es einen Nulldurchgang und das eigentlich negative Ergebnis wird zu einem hohen positiven Ergebnis.

Der Befehl np.absolute() liefert nicht immer das gewünschte Ergebnis (hier 10), wenn die Eingangsdaten als np.uint8 vorliegen.
>>> import numpy as np
>>> a=np.array([110], dtype=np.uint8)
>>> b=np.array([100], dtype=np.uint8)
>>> np.absolute(a-b)
array([10], dtype=uint8)
>>> np.absolute(b-a)
array([246], dtype=uint8)
>>>
>>> np.absolute(np.int8(a)-np.int8(b))
array([10], dtype=int8)
>>> np.absolute(np.int8(b)-np.int8(a))
array([10], dtype=int8)
Vielleicht ließe sich was machen mit np.minimum() oder np.maximum().
>>> np.minimum(a-b, b-a)
array([10], dtype=uint8)
>>> np.maximum(a-b, b-a)
array([246], dtype=uint8)
>>>
>>> a=np.array([250], dtype=np.uint8)
>>> b=np.array([1], dtype=np.uint8)
>>> np.minimum(a-b, b-a)
array([7], dtype=uint8)
>>> np.maximum(a-b, b-a)
array([249], dtype=uint8)

Funktioniert also auch nicht. Einmal liefert der np.minimum() das gewünschte / richtige Ergebnis, bei anderen Werten aber der np.maximum().
Nochmal hübsch als Tabelle:
Pixel Bild_1
Pixel Bild_2
Bild_1 - Bild_2
nach uint8
als uint8
Status
110
100
10
10 & 0xff 10
richtig
100
110
-10
-10 & 0xff 246
falsch
250
1
249
249 & 0xff 249
richtig
1
250
-249
-249 & 0xff 7
falsch

Was sicher funktionieren würde, wäre eine eigene Funktion, die pro Pixel entscheidet, welches Bild vom anderen Bild abgezogen wird:
diff=np.empty([480, 640], np.uint8)
for y in range(480):
  for x in range(640):
    if img1[y, x]<img2[y, x]:   diff[y, x]=img2[y, x]-img1[y, x]
    else:                       diff[y, x]=img1[y, x]-img2[y, x]

Aber in dieser Form ist das natürlich viel zu langsam.

np.int8
Würden die Bilder nun einfach durchgängig als np.int8 gespeichert werden, sähe die Rechnung so aus:
Pixel Bild_1
als int8
Pixel Bild_2
als int8
Bild_1 - Bild_2
als Pixel
status1
np.absolute()
status2
110
110
100
100
110 - 100 = 10
10
richtig
10 => 10 richtig
100
100
110
110
100 - 110 = -10
246
falsch
-10 => 10 richtig
250
-6
1
1
-6 - 1 = -7
249
richtig
-7 => 7
falsch
1
1
250
-6
1 - -6 = 7
7
falsch
7 => 7
falsch

Tja.....auch das liefert nicht immer die richtigen Werte.
Na gut, 250 zu 1 wird bei Differenz-Bildern eher selten vorkommen.
Aber eine Differenz von >128 sollte reichen, um falsche Ergebnisse zu erhalten. Mal schauen:
>>> 140-120   # erst der Gut-Fall
20
>>> np.uint8(np.absolute(np.int8(140)-np.int8(120)))
20
>>> np.uint8(np.absolute(np.int8(120)-np.int8(140)))
20
>>>
>>> 139-10    # und jetzt knapp drüber
129
>>> np.uint8(np.absolute(np.int8(139)-np.int8(10)))
127
>>> np.uint8(np.absolute(np.int8(10)-np.int8(139)))
127
>>>
>>> 200-10    # oder deutlich drüber
190
>>> np.uint8(np.absolute(np.int8(200)-np.int8(10)))
66
>>> np.uint8(np.absolute(np.int8(10)-np.int8(200)))
66

Ja. Deutlich! Passt nicht.

Nun will ich mal was prüfen:
if __name__=="__main__":
  cam=IP_Cam()

  img1=cam.getImageGrayscale()   # liefert jetzt np.int8
  time.sleep(0.1)
  img2=cam.getImageGrayscale()
  img1_u=np.uint8(img1)           # eine np.uint8-Version daraus bauen
  img2_u=np.uint8(img2)

  diff=np.absolute(img1-img2)

  diff2=np.empty([480, 640], np.uint8)
  for y in range(480):
    for x in range(640):
      if img1_u[y, x]<img2_u[y, x]: diff2[y, x]=img2_u[y, x]-img1_u[y, x]
      else:                         diff2[y, x]=img1_u[y, x]-img2_u[y, x]

  cam.saveImage(np.uint8(diff), "diff1")
  cam.saveImage(diff2, "diff2")

  cam.saveImage(img1_u, "img1")
  cam.saveImage(img2_u, "img2")

Das Script speichert vier Bilder. Im ersten Durchlauf sehen die so aus:
Größe
Name
17269 diff1.jpg
17269 diff2.jpg
39174 img1.jpg
39025 img2.jpg

Die Datei diff1.jpg ist ein Differenz-Bild, das aus zwei np.int8-Arrays gebildet wurde und bei dem negative Werte mittels np.absolute() in positive Werte gewandelt wurden.
Die Datei diff2.jpg ist ein Differenz-Bild von zwei np.uint8-Arrays, bei dem jedes Pixel dadurch gebildet wurde, dass das Pixel mit der geringeren Helligkeit von dem Pixel mit der höheren Helligkeit abgezogen wurde - also alle Werte jederzeit positiv sind und bleiben.

Die beide Differenz-Bilder sind identisch (selbe md5sum), weil keine Bewegung zwischen img1 und img2 stattfand - die Bilder also nur durch das Grundrauschen der Kamera voneinander abweichen.

Sorge ich nun aber für Bewegung zwischen img1 und img2, siehts so aus:
Größe Name
20705 diff1.jpg
20790 diff2.jpg
39271 img1.jpg
38620 img2.jpg

Die Bilder diff1.jpg und diff2.jpg sind jetzt unterschiedlich groß.
Da brauche ich die gar nicht erst per md5sum miteinander vergleichen.
In diff2.jpg gibt es zwei deutlich hellere Stellen, als in diff1.jpg.
An diesen Stellen war der Unterschied zwischen img1 und img2 offenbar größer als 127.
Hier mal beide Bilder übereinander (wie immer: anklicken zum vergrößern):
Abweichung von Differenz-Bildern

Das bestätigt bzw. visualisiert die eben erlangte Erkenntnis - und zeigt, dass eine Umstellung auf np.int8 auch nicht der Weisheit letzter Schluss ist.

Abgesehen davon besteht ein Bild in np.int8 ja aus Werten zwischen -128 bis 127.
Dieser Umstand wird sicher spätestens dann richtig unerfreulich, wenn Helligkeiten verglichen werden müssen.
Auch nimmt der Image.fromarray() kein np.int8 an.
Obwohl....mit Parameter mode="L" dann doch. Aber eigentlich ist das auch egal, weil sich np.int8 quasi schon disqualifiziert hat.

Was kann man stattdessen tun?
Bilder könnten entweder als
- np.int abgelegt werden oder es könnte
- je nach Bild-Operation zwischen np.int8 und np.uint8 hin-und-her-konvertiert werden.

Der np.int hat auf meinem System 64 Bit. Das wäre m.E. ein bischen Overkill, wenn man nur 8 Bit braucht...
Also vielleicht np.int16. Damit würde ein Bild nur doppelt so viel Speicherplatz belegen, wie es eigentlich braucht.
Der np.int16 nimmt Werte zwischen -32768 und 32767 auf. Also genug Platz für Werte von -255 bis 255.

Wahrscheinlich wird eine Konvertierung von np.int16 nach np.uint8 aber deutlich länger dauern, als von np.int8 nach np.uint8.
Aber sowohl np.int8 als auch np.uint8 funktionieren nicht, um 8 Bit samt Vorzeichen aufzunehmen.
Das bräuchte 9 Bit. Die gibts aber nicht. Also wenn schon wandeln, dann nach int16 ... und int8 komplett vergessen.

Mal testen:
def differenceImageV1(img1, img2):   # img1 und img2 sind vom Typ np.uint8
  diff=np.empty_like(img1)
  h, w=img1.shape
  for y in range(h):
    for x in range(w):
      if img1[y, x]<img2[y, x]: diff[y, x]=img2[y, x]-img1[y, x]
      else:                     diff[y, x]=img1[y, x]-img2[y, x]
  return(diff)

def differenceImageV2(img1, img2):
  return(np.uint8(np.absolute(np.int16(img1)-np.int16(img2))))

def differenceImageV3(img1, img2):   # nur als Referenz - liefert falsches Ergebnis
  return(img1-img2)

V1 und V2 liefern sowohl identische als auch korrekte Bilder. Unabhängig von Bewegung innerhalb der Eingangsbilder.
Bei vielfachem Aufruf kommen folgende Zeiten raus:
  10x: 1893.99 ms
1000x:  435.62 ms
1000x:   26.56 ms
Die V1 ist in dieser Hinsicht eindeutig unbrauchbar.
Die V2 geht schon.
Die V3 ist richtig schön schnell, liefert aber Blödsinn und war auch nur als Referenz gedacht.

Eine entsprechende Nachfrage auf stackoverflow.com hat eine noch deutlich bessere Version ergeben:
def differenceImageV6(img1, img2):
  a=img1-img2
  b=np.uint8(img1<img2)*254+1
  return(a*b)


Liefert die selbe Summe wie meine V2, ist aber deutlich flotter:
1000x:  401.61 ms  np.sum=26108157
1000x:  179.71 ms  np.sum=26108157

Damit könnte jetzt die Differenz von zwei uint8-Bildern schnell und korrekt gebildet werden.
Ansonsten bliebe alles auf np.uint8. Der einzige Nachteil an der Sache wäre, dass ich bei zukünftigen Funktionen immer auf die korrekte Verarbeitung des möglichen Nulldurchgangs achten muss.
Wären Bilder immer und durchgängig auf np.int16, müsste ich das nicht.

Also braucht es noch ein paar Messungen zum Vergleich von np.uint8 und np.int16.

Erstmal werde ich die bisher genutzten Operationen sammeln.
Operation
Funktion / Syntax
Summe bilden
np.sum()
Mittelwert bilden
np.mean()
Differenz bilden
arr1-arr2
Kopie erstellen
arr2=1*arr1 oder arr2=np.array(arr1, dtype=xxx, copy=True)
Bereich ausschneiden
arr[y1:y2, x1:x2]
Wertebereich ändern arr[arr<x]=y
Datentyp ändern
np.uint8(), np.int16(), ...

In der folgenden Tabelle sind die Ausführungszeiten für die unter "Test..." aufgeführten Funktionen angegeben.
Die Funktionen wurden jeweils 2000 mal ausgeführt und die Zeiten ganz profan mit time.process_time() gemessen.
Operation
Test int16
Test uint8
Zeit int16
Zeit uint8
Summe bilden
tmp1=np.sum(img1_s16)
tmp2=np.sum(img2_s16)
tmp1=np.sum(img1_u8)
tmp2=np.sum(img2_u8)
830.78 ms
980.18 ms
Mittelwert bilden
tmp1=np.mean(img1_s16)
tmp2=np.mean(img2_s16)
tmp1=np.mean(img1_u8)
tmp2=np.mean(img2_u8)
877.64 ms
1127.37 ms
Differenz bilden tmp1=np.absolute(img1_s16-img2_s16)
tmp1=differenceImageV6(img1_u8, img1_u8)
599.29 ms
328.10 ms
Kopie erstellen V1
tmp1=1*img1_s16
tmp2=1*img2_s16
tmp1=1*img1_u8
tmp2=1*img2_u8
158.14 ms
157.75 ms
Kopie erstellen V2 tmp1=np.array(img1_s16, copy=True)
tmp2=np.array(img2_s16, copy=True)
tmp1=np.array(img1_u8, copy=True)
tmp2=np.array(img2_u8, copy=True)
153.03 ms
70.92 ms
Bereich ausschneiden tmp1=img1_s16[10:100, 10:100]
tmp2=img2_s16[10:100, 10:100]
tmp1=img1_u8[10:100, 10:100]
tmp2=img2_u8[10:100, 10:100]
1.11 ms
1.10 ms
Wertebereich ändern tmp1=1*img1_s16
tmp2=1*img2_s16
tmp1[tmp1<128]=0
tmp2[tmp2<128]=0
tmp1=1*img1_u8
tmp2=1*img2_u8
tmp1[tmp1<128]=0
tmp2[tmp2<128]=0
6545.11 ms
6779.61 ms
Datentyp ändern img1_s16=np.int16(img1_u8)
img2_s16=np.int16(img2_u8)
tmp1=np.uint8(img1_s16)
tmp2=np.uint8(img2_s16)
202.66 ms
135.99 ms

Dabei muss man nun hoffen, dass dort nicht irgendwo eine Optimierung von numpy die Messwerte verändert.
Wenn ich statt 2000 Iteration 1000 einstelle, kommen sämtlich die halben Zeiten heraus.
Das deute ich mal als gutes Zeichen, dass numpy keine Iterations-Schritte wegoptimiert hat.

Interessant sind solche Zeilen, bei denen die Zeiten für int16 und uint8 deutlich voneinander abweichen.
Bei der Summenbildung liegt int16 leicht vorn, beim Mittelwert ebenfalls.
Bei der Differenz hat die optimierte V6 tatsächlich die int16-Version geschlagen.
Interessant ist noch der Unterschied zwischen Kopie V1 und V2 für uint8.
Die Änderung des Datentyps von int16->uint8 geht sonderbarerweise schneller vonstatten, als der Rückweg.

Die Klein-Computer brauchen schon etwas länger, obwohl hier nur 1000 Iterationen pro Funktion durchlaufen wurden.
Operation
RaspberryPi int16
RaspberryPi uint8 BananaPi int16
BananaPi uint8
Summe bilden 21092.51 ms
17896.24 ms
6576.92 ms
6092.12 ms
Mittelwert bilden 44651.76 ms
40972.37 ms
10746.21 ms
10717.93 ms
Differenz bilden 23580.92 ms
37739.59 ms
12088.47 ms
18823.55 ms
Kopie erstellen V1 19371.14 ms
12048.09 ms
9640.56 ms
5632.14 ms
Kopie erstellen V2 7161.44 ms
2049.26 ms
6034.22 ms
1240.02 ms
Bereich ausschneiden 133.63 ms
133.59 ms
28.06 ms
27.03 ms
Wertebereich ändern 71613.67 ms
70013.32 ms
33331.61 ms
32231.65 ms
Datentyp ändern 13658.56 ms
14429.18 ms
6008.72 ms
3367.94 ms

Hier ist interessant, dass die Differenz-Bildung auf beiden Rechnern mit 16 Bit schneller läuft.
Ansonsten hat hier uint8 eher die Nase vorn.


Ja....was mache ich nun mit diesen Erkenntnissen?
Die Unterschiede sind bei keiner der untersuchten Operationen extrem.

Bei uint8 muss ich immer aufpassen, ob bei einer Operation ein Überlauf auftreten könnte und dies dann entsprechend berücksichtigen.
Aber was könnte ich neben der Differenz-Bildung schon noch mit den Bildern machen wollen, wobei es zu einem Überlauf kommen könnte?
Helligkeit verdoppeln vielleicht - also quasi Bild*2. Auf uint8 könnte das einen Überlauf geben: 200*2=400,  400&0xff=144. Damit hätte man das Gegenteil von dem erreicht, was man erreichen wollte. Bei int16 bliebe es erstmal bei 400 und man könnte dann noch einen arr[arr>255]=255 drüber laufen lassen oder sogar einfach mit der 400 weitermachen, sofern man das Bild nicht anzeigen oder speichern will.

Bei int16 belegen die Bilder den doppelten Platz im RAM-Speicher und müssen vor der Verarbeitung erstmal von uint8 nach int16 konvertiert werden. Dafür kann man ab dann leichter damit rechnen.

Oder könnte man vielleicht einfach immer beide Versionen im Speicher halten und mit der jeweils schnelleren arbeiten... !?
Hmm...dagegen sprechen die unterschiedlichen Zeiten-Verhältnisse auf den unterschiedlichen Rechnern. Dann müsste man ja eine Tabelle führen, wo was jeweils schneller läuft - und das für so doch eher geringe Laufzeit-Unterschiede. Das lohnt den Aufwand nicht.
Aber wenn man statt zwei Versionen, gleich drei Versionen im Speicher hält, macht es schon wieder eher Sinn.
Also Farbbild + Graubild_uint8 + Graubild_int16.

So werde ich das mal probieren und dabei gleich das gewachsene Sammelsurium an Funktionen etwas ausdünnen.

Jetzt kommt also schon die vierte Version der Bewegungs-Nachführung.