Dreh dich im Kreis (Linux-Magazin, Februar 2022)

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!

Eiskalt verkehrt

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

Doppelt spiegeln

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.

Listing 1: rotate-180.go

    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 }

Beschreibbare Kopie

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.

Listing 2: imgmod.go

    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!

Rotieren um 90 Grad

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.

Aus X wird Y

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!

Listing 3: rotate-90.go

    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.

Listing 4: autorot.go

    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.

[1]

Listings zu diesem Artikel: http://www.linux-magazin.de/static/listings/magazin/2022/02/snapshot/

[2]

Michael Schilli, "Spuren verwischen": Linux-Magazin 09/20, S.XXX, <U>https://www.linux-magazin.de/ausgaben/2020/09/snapshot-29/<U>

Michael Schilli

arbeitet als Software-Engineer in der San Francisco Bay Area in Kalifornien. In seiner seit 1997 laufenden Kolumne forscht er jeden Monat nach praktischen Anwendungen verschiedener Programmiersprachen. Unter mschilli@perlmeister.com beantwortet er gerne Ihre Fragen.

POD ERRORS

Hey! The above document had some coding errors, which are explained below:

Around line 5:

Unknown directive: =desc