Irgendwie scheine ich mein Mobiltelefon beim Fotografieren falsch zu halten, denn es kommt total oft vor, dass ein Bild, das in der Fotosammlung auf dem Handy zwar richtig aussieht, dann aber plötzlich auf dem Kopf oder der Seite steht, wenn ich es mit Whatsapp an Freunde senden will. Ein Blick auf die Metadaten des betreffenden Fotos offenbart, was schiefläuft:
$ exiftool pic.jpg | grep Rotate
Orientation: Rotate 180
Offensichtlich speichert das Telefon das Bild falsch ausgerichtet ab, spart sich aber die Korrektur und zeigt im EXIF-Header des JPG-Formats an, wie es eigentlich richtig gedreht gehört. Ich weiß nicht, wer sich diesen Schmarrn ausgedacht hat, aber anzunehmen, dass jede beliebige App erst in den EXIF-Header schaut, und das Bild richtig herum rotiert, scheint mir ein grundlegender Denkfehler zu sein. Das sollte doch die erzeugende, originale Kamera-App erledigen, statt die Arbeit flussabwärts wieder und wieder einer unüberschaubaren Vielzahl von verarbeiteten FotoApps aufzubürden!
Abbildung 1 illustriert, dass das Mobiltelefon in seiner Photos-App das Bild noch richtig herum anzeigt (obwohl es falsch abgespeichert wurde), aber die Desktop-Version des WhatsApp-Client im Browser offensichtlich den zugehörigen Exif-Header ignoriert und das Bild eiskalt auf dem Kopf schicken würde. Was der Empfänger wohl dächte, wenn das Foto zu der Nachricht meines Surfausflugs an den Ocean Beach in San Francisco auf dem Kopf ankäme?
Abbildung 1: Das Handy zeigt das Bild noch richtig herum an ... |
Abbildung 2: ... doch WhatsApp würde das Foto allen Ernstes auf dem Kopf schicken. |
Höchste Zeit, ein Go-Programm zu schreiben, das ein vom Telefon geholtes Foto richtig ausrichtet und den Exif-Header für die erst falsche und dann richtige Orientierung löscht. Ganz so, wie es der Gimp (Abbildung 3) macht, wenn er ein verdrehtes Foto sieht. Ganz nebenbei lohnt es sich, einige Algorithmen zur Bildrotation um 90 oder 180 Grad zu studieren, schließlich könnte jemand beim nächsten Vorstellungsgespräch danach fragen!
Abbildung 3: Gimp merkt, dass etwas nicht stimmt. |
Ein digitales Foto ist letztlich eine NxM-Matrix aus Pixelwerten, und um es auf den Kopf zu stellen, also eine Drehung von 180 Grad auszuführen, vertauscht ein Algorithmus einfach Pixelwerte ober- und unterhalb der Mittellinie. Die X-Werte der Pixel laufen traditionsgemäß von links nach rechts im Bild, während sich die Y-Werte von oben nach unten erhöhen. So lässt sich der Ursprung des Fotos links oben mit (0,0) ansprechen, während die rechte untere Ecke an den Koordinaten (w-1, h-1)
liegt, mit der Bildbreite w
und der Höhe h
(jeweils in Pixeln).
Abbildung 4: Koordinaten bei der 180-Grad-Rotation |
Aber Vorsicht, wer einfach die Y-Werte spiegelt, findet am Ende ein Spiegelbild des ursprünglichen Fotos vor. Vielmehr kommen kleine X-Werte (Pixel links oben) bei der 180-Grad-Drehung unten rechts zu liegen, also muss der Rotieralgorithmus nicht nur die Y-Werte spiegeln, sondern auch noch die X-Werte. Abbildung 4 zeigt das Verfahren. Ein in der oberen Bildhälfte ansässiger Pixel an den Koordinaten (x0, y0)
kommt so in der unteren Bildhälfte an der Stelle x1, y1
zu liegen. Der Abstand von y0
zur Mittellinie ist dabei gleich dem Abstand von y1
zur Mittellinie. Gleichzeitig ist x0
so weit vom linken Bildrand entfernt wie x1
vom rechten Bildrand.
Listing 1 zeigt den Algorithmus, der ein Jpeg-Foto auf den Kopf stellt. Die zwei verschachtelten for
-Schleifen ab Zeile 13 hangeln sich durch die Y-Werte von 0 bis zum unteren Bildrand, und durch die X-Werte von 0 bis zur rechten Kante. Diese Koordinaten entsprechen den in Abbildung 4 als x0
und y0
in Grün eingezeichneten Positionen des Original-Pixels. Innerhalb der Doppelschleife holt Zeile 15 den Original-Pixelwert mit jimg.At(x,y)
ab, und speichert ihn an der gespiegelten Position im neu angelegten, modifizierbaren Foto dimg
an der Position, die in Abbildung 4 mit x1, y1
vermerkt ist. Sie errechnet sich aus dem Abstand von x0
vom rechten bzw. y0
vom unteren Bildrand. So spiegelt der Algorithmus das Bild effizient sowohl an der horizontalen wie an der vertikalen Mittellinie, und stellt es damit wie gewünscht auf den Kopf.
01 package main 02 03 import ( 04 "image" 05 ) 06 07 func rot180(jimg image.Image) *image.RGBA { 08 bounds := jimg.Bounds() 09 width, height := bounds.Max.X, bounds.Max.Y 10 11 dimg := image.NewRGBA(bounds) 12 13 for y := 0; y < height; y++ { 14 for x := 0; x < width; x++ { 15 dimg.Set(width-1-x, height-1-y, jimg.At(x, y)) 16 } 17 } 18 19 return dimg 20 }
Wenn Go ein Jpeg-Foto von der Platte liest, kommt es üblicherweise in einem Pixel-Array zu liegen, der sich nicht modifizieren lässt. Da der Algorithmus aber damit herumfuhrwerken möchte, legt Zeile 11 in Listing 1 mit NewRGBA()
zunächst ein beschreibbares Foto mit den gleichen Maßen wie das Original an, lässt es aber leer. Anschließend kann die Funktion rot180
mit jimg.At()
die Pixel des Originals auslesen, und sie mit Set(x, y, wert
im Zielfoto einzeln gespiegelt übertragen. Zeile 24 gibt das fertig gedrehte Bild als Pointer an das aufrufende Hauptprogramm zurück.
01 package main 02 03 import ( 04 "image" 05 "image/jpeg" 06 "log" 07 "os" 08 ) 09 10 func imgMod(srcFile string, dstFile string, cb func(image.Image) *image.RGBA) { 11 f, err := os.Open(srcFile) 12 if err != nil { 13 log.Fatalf("os.Open failed: %v", err) 14 } 15 16 jimg, _, err := image.Decode(f) 17 if err != nil { 18 log.Fatalf("image.Decode failed: %v", err) 19 } 20 21 dimg := cb(jimg) 22 if err != nil { 23 log.Fatalf("Modifier failed") 24 } 25 26 f, err = os.Create(dstFile) 27 if err != nil { 28 log.Fatalf("os.Create failed: %v", err) 29 } 30 err = jpeg.Encode(f, dimg, nil) 31 if err != nil { 32 log.Fatalf("jpeg.Encode failed: %v", err) 33 } 34 }
Soweit der Algorithmus zum Rotieren eines Fotos im Speicher um 180 Grad, doch wie kommt das Bild, das ja im Jpeg-Format komprimiert auf der Platte liegt, anfangs überhaupt als Pixel-Matrix in den Speicher? Die Funktion imgMod()
in Listing 2 nimmt dazu den Namen der Fotodatei in srcFile
entgegen, im zweiten Parameter den Namen der Zieldatei, und im dritten eine Callback-Funktion, die die gewünschte Rotation des Bildes im Speicher durchführen wird. Funktionen sind in Go vollwertige Datentypen und lassen sich problemlos an andere Funktionen mitgeben, mit dem Auftrag "hier ist der Algorithmus, den du auf die Daten anwendest". Listing 2 öffnet in Zeile 11 die Originaldatei zum Lesen, dekodiert die Jpeg-Daten mit Decode()
aus dem Standardpaket image/jpeg
und legt sie in der Variablen jimg
ab, falls kein Fehler aufgetreten ist. Zeile 21 ruft dann die vorher in der Variablen cb
als Parameter hereingereichte Funktion auf, übergibt ihr die Bilddaten in jimg
und empfängt die modifizierten Daten des dann rotierten Bildes in dimg
. Bleibt nur noch, eine neue Zieldatei dstFile
anzulegen und in Zeile 30 die Jpeg-codierten Daten für das modifizierte Foto hineinzuschreiben. Fertig ist der Lack!
Doch nicht alle falsch gespeicherten Bilder stehen auf dem Kopf, manchmal liegen sie auch auf der Seite, und müssen um 90 Grad gedreht werden. Der Aufruf der Utility exiftool
auf die Jpeg-Datei zeigt in diesen Fällen dann etwas wie "Orientation: 90 CW" an, zum Zeichen dafür, dass das Bild um 90 Grad im Uhrzeigersinn ("clockwise") gedreht werden muss, um es richtig herum anzuzeigen. Abbildung 5 zeigt die Desktop-App Gimp, die feststellt, dass ein Foto des wenig bekannten Parks "Billy Goat Hill" in San Francisco um eine Vierteldrehung nach rechts rotiert gehört und bietet den entsprechenden Service auch gleich an.
Abbildung 5: Gimp merkt, dass das Bild um 90 Grad gedreht gehört |
Wie funktioniert eine Vierteldrehung von Pixeln nun in einer 2D-Matrix? Abbildung 6 zeigt schematisch, wie die erste Pixelreihe mit den Werten (1, 2, 3, 4) durch eine 90-Grad-Drehung der Matrix im Uhrzeigersinn als rechteste Spalte im Ergebnis landet.
Abbildung 6: Die Matrix-Transformation beim Rotieren um 90 Grad. |
Während der Algorithmus so Reihen in Spalten umwandelt, ändert sich auch die Dimension des Zielbilds: Handy-Fotos sind typischerweise nicht quadratisch, sondern rechtwinklig, und ein um 90 Grad gedrehtes Foto ändert nicht nur seine Pixelwerte sondern vertauscht auch Länge und Breite des resultierenden Gesamtbildes. Listing 3 trägt dem Rechnung, in dem Zeile 10 die in bounds
liegenden Dimensionen in X- und Y-Richtung vertauscht, sodass das in Zeile 12 mit NewRGBA()
generierte modifizierbare Zielbild bereits die Dimensionen des rotierten Rechtecks und nicht mehr die des Originals hat.
Die doppelte for
-Schleife ab Zeile 14 iteriert zeilenweise durch das Ausgangsbild, nimmt mit jimg.At()
aktuelle Pixelwerte entgegen und speichert sie spaltenweise mit dimg.Set()
von rechts nach links in die Zielmatrix ein. So einfach geht das!
01 package main 02 03 import ( 04 "image" 05 ) 06 07 func rot90(jimg image.Image) *image.RGBA { 08 bounds := jimg.Bounds() 09 width, height := bounds.Max.X, bounds.Max.Y 10 bounds.Max.X, bounds.Max.Y = bounds.Max.Y, bounds.Max.X 11 12 dimg := image.NewRGBA(bounds) 13 14 for y := 0; y < height; y++ { 15 for x := 0; x < width; x++ { 16 org := jimg.At(x, y) 17 dimg.Set(height-y, x, org) 18 } 19 } 20 21 return dimg 22 }
Mit den beiden in Code gegossenen Algorithmen kann nun das Hauptprogramm in Listing 4 ein Foto von der Platte lesen, die Exif-Header auslesen, feststellen, ob sich darin ein "Orientation"-Tag findet, und die Rotation ins korrigierte Format einleiten. Wer bislang Exif-Header von Jpg-Fotos mit dem Tool exiftool
ausgelesen hat, wird sich wundern, dass der wahre Wert eines Orientation-Tags in den Exif-Headern keineswegs ein String mit der Gradangabe ist, sondern ein Integerwert, der die Werte 6 (90 Grad im Uhrzeigersinn), 3 (180 Grad) oder 8 (90 Grad entgegen dem Uhrzeigersinn) annimmt. Weitere Werte des Standards, der auch gespiegelte Fotos unterstützt, zeigt Abbildung 7, sie kommen in der Praxis aber seltener vor.
01 package main 02 03 import ( 04 "flag" 05 "fmt" 06 "github.com/rwcarlsen/goexif/exif" 07 "os" 08 "path" 09 ) 10 11 func main() { 12 flag.Usage = func() { 13 fmt.Printf("Usage: %s jpg-file\n", path.Base(os.Args[0])) 14 os.Exit(1) 15 } 16 17 flag.Parse() 18 if len(flag.Args()) != 1 { 19 flag.Usage() 20 } 21 22 jpgFile := flag.Arg(0) 23 24 f, err := os.Open(jpgFile) 25 if err != nil { 26 panic(err) 27 } 28 29 data, err := exif.Decode(f) 30 if err != nil { 31 panic(err) 32 } 33 34 orient, err := data.Get(exif.Orientation) 35 if err != nil { 36 fmt.Printf("No orientation header found.\n") 37 os.Exit(0) 38 } 39 40 val, err := orient.Int(0) 41 if err != nil { 42 panic(err) 43 } 44 45 switch val { 46 case 3: 47 imgMod(jpgFile, jpgFile, rot180) 48 case 6: 49 imgMod(jpgFile, jpgFile, rot90) 50 default: 51 panic("Unknown orientation") 52 } 53 }
Abbildung 7: Integer-Werte des Exif-Headers geben die Orientierung es Fotos an. |
Die in einem Jpeg-Foto verbuddelten Exif-Tags sind nicht einfach auszulesen, aber zum Glück bietet die Go-Community auf Github einige Libraries an, die den Job auf den Aufruf einer Funktion Decode()
reduzieren. Listing 4 zeigt das Hauptprogramm autorot
, das die Library goexif
von Github-User rwcarlsen
nutzt. Eine Snapshot-Ausgabe vor einiger Zeit hat ein ähnliches Produkt verwandt ([2]).
Mit data.Get()
holt Listing 4 in Zeile 34 den Wert des Tags "Orientation" aus dem Exif-Salat, und falls es sich nicht findet, gibt es nichts zu tun, denn das Bild ist bereits richtig rotiert. Wird das Programm allerdings fündig, holt Zeile 40 den ersten Integerwert des Tags heran (Index 0) und das Switch-Konstrukt ab Zeile 45 ermittelt, welche Art von korrigierender Rotation, 90 oder 180 Grad, das Bild benötigt, und ruft die Modifiziererfunktion imgMod
mit der entsprechenden Algorithmusfunktion als Parameter auf. Ausgangs- und Zieldatei benennen die Aufrufe von imgMod()
in Listing 4 jeweils identisch, also überschreibt autorot
einfach die Originaldateien. Wem dies zu gefährlich erscheint, kann die Zieldatei in .bak
umbenennen, dann passiert garantiert kein Malheur.
Wenn Gos image
-Library das Jpeg-Bild wieder auf die Platte zurückschreibt, schließt es die ursprünglichen Exif-Daten komplett aus, also steht dort auch kein Orientation-Header mehr.
Um das Binary autorot
zu erzeugen, braucht es folgende Kommandosequenz:
go mod init autorot
go mod tidy
go build autorot.go rotate-90.go rotate-180.go imgmod.go
So holt der Compiler go
die notwendigen Libaries von Github, compiliert sie und bindet sie mit ein. Es entsteht wie immer ein Executable, das bereits alle Abhängigkeiten enthält, also problemlos auf einen anderen Rechner kopiert werden kann und dort klaglos läuft, ein ähnliches Betriebssystem und Prozessorarchitektur vorausgesetzt. Perfekt ist das Programm noch nicht, es fehlt noch die Rotierung entgegen des Uhrzeigersinns sowie Algorithmen für exotischere Exif-Werte, aber das lässt sich mit den beschriebenen Grundlagen leicht hinzufügen. Wie immer sind dem Hobbyhandwerker keine Grenzen gesetzt.
Listings zu diesem Artikel: http://www.linux-magazin.de/static/listings/magazin/2022/02/snapshot/
Michael Schilli, "Spuren verwischen": Linux-Magazin 09/20, S.XXX, <U>https://www.linux-magazin.de/ausgaben/2020/09/snapshot-29/<U>
Hey! The above document had some coding errors, which are explained below:
Unknown directive: =desc