Mein relativ neues Smartphone macht zumeist hervorragend scharfe Bilder, allerdings nicht immer. Warum bloß? In einer Smartphone-Kamera steckt ja nicht nur ein Fotochip, sondern auch ein leistungsfähiges KI-Gehirn, das den Autofokus der Kamera zum scharfzustellenden Bildbereich dirigiert.
Alte Fotofüchse wissen nun, dass kritische Betrachter bei Tierfotos erwarten, dass der Bereich ums Auge messerscharf ist. Der Rest des Tieres darf mangels Tiefenschärfe auch leicht unscharf werden, ohne dass der Gesamteindruck leidet. Nun trickst Apple (und auch andere Telefonkamerahersteller) mächtig herum, um den scharfzustellenden Bildbereich der Kamera möglichst batterieschonend zu ermitteln. Das scheint bei kaltblütigen Lurchen, wie dem in Abbildung 1 nicht zu funktionieren, denn so oft ich die freche kleine Eidechse auf einem Wanderweg auch knipste, kein einziges Foto der Serie wurde 100% scharf, so dass der Echsenkopf "poppt", wie der Fachmann sagt.
Daheim fragte ich ChatGPT, wo er denn den Autofokus ansetzen würde, und das Elektronengehirn antwortete zwar richtig, dass die Kamera dazu auf das Auge des Lurches zielen solle, versagte aber kläglich beim Auffinden desselben (Abbildung 1 links). Nun wollte ich kein voreiliges Siegesgeheul verhärmter KI-Kritiker hören, denn mir ist klar, dass die aktuell verfügbaren künstlichen Gehirne noch in den Kinderschuhen stecken. Deshalb fragte ich auch noch Grok aus dem Hause X (vormals Twitter), und der löste die Aufgabe aus dem Effeff (Abbildung 1 rechts).
|
| Abbildung 1: ChatGPT versagt kläglich beim Aufspüren der Auges, Grok trifft ins Schwarze. |
Bis das KI-Angebot aufgeholt hat, verpflichtet die Go-App dieser Ausgabe den User dazu, mit der Maus auf allen Bildern einer Serie jeweils eine rote Linie zu ziehen, die den kritischen Fokusbereich definiert. Entlang dieser Messlinie ermittelt die App dann die Lichtintensität jedes Pixels. Abbildung 2 zeigt das Ergebnis bei drei verschiedenen Fotos der Palme vor meinem Fenster in San Francisco. Das erste ist gestochen scharf, das zweite leicht unscharf und das unterste total unscharf. Die zugehörigen Messgraphen verraten, wie sich Schärfe in einem Bild manifestiert: Bei einem scharfen Bild ist der Graph zackig, denn zwischen hellen und dunklen Bereichen (zum Beispiel zwischen blauem Himmel und dunklen Palmenblättern) geht die Intensität ruckartig nach unten. Ein unscharfes Bild zeigt hingegen sanfte, harmonische Übergänge, die eher einer Sinuskurve als einem Dirac ähneln.
|
| Abbildung 2: Pixel-Intensität entlang der roten Messlinie: Scharf, leicht unscharf, total unscharf. |
Die App dieser Ausgabe zeigt nun alle Fotos einer Serie in einem Kontaktabzug an (Abbildung 3), lässt den User mit der Maus auf jedem Bild eine rote Line ziehen, und ermittelt den Intensitätsverlauf entlang dieser Linie. Aus dessen Zackigkeit errechnet sie einen Score, der bei vielen abrupten Wechseln hoch und bei sanften Übergängen niedrig ist. Sobald die App den Schärfegrad des Bildes errechnet hat, blendet sie den "Score" unten ein. Voran geht der Rang des Bildes in einer nach Schärfe sortierten Serie. Der Sieger bekommt außerdem einen blauen Rahmen spendiert. Den Pfad dieses Premiumbildes schreibt die App nach grep-Manier in die Standardausgabe, sobald sie mit "Q" abgebrochen wird. So können nachfolgende Tools das schärfste Bild weiterverarbeiten, und es zum Beispiel in ein Verzeichnis mit den besten Aufnahmen bugsieren.
|
| Abbildung 3: In jedem Foto wählt der User einen Wertebereich aus und erhält einen Score. |
Wie nun lässt sich die Zackigkeit einer Werteserie mathematisch bestimmen, und zum Beispiel erste Graph oben in Abbildung 2 höher bewerten als der zweite oder gar der dritte? Ein gängiges Verfahren ist es, die Deltas zwischen den Messpunkten zu quadrieren, aufzuaddieren, und das Ergebnis durch die Anzahl der Deltas zu teilen ([2]). Heraus kommt ein Score für die Rauigkeit des Messwertgraphen. Stark schwankende Messwerte bewirken dabei hohe Deltas, die Quadratur bewertet positive und negative Schwankungen gleich, und heraus kommen stark gewichtete Score-Werte. Flachere Messgraphen produzieren kleinere Deltas, die wegen der Quadratur deutlich niedrigere Scores liefern (Abbildung 4). Listing 1 implementiert den Algorithmus in der Funktion Roughness() ab Zeile 6. Sie errechnet den Score aus der Summe der quadratierten Deltas, geteilt durch die Anzahl der Messwerte.
|
| Abbildung 4: Ermittlung des Schärfe-Scores eines unscharfen bzw. scharfen Fotos |
01 package main
02 import (
03 "image"
04 "image/color"
05 )
06 func Roughness(img image.Image,
07 x1, y1, x2, y2 int) float64 {
08 values := SampleLine(img, x1, y1, x2, y2)
09 if len(values) < 2 {
10 return 0
11 }
12 var sum float64
13 for i := 0; i < len(values)-1; i++ {
14 d := values[i+1] - values[i]
15 sum += d * d
16 }
17 return sum / float64(len(values)-1)
18 }
19 func SampleLine(img image.Image, x1, y1, x2, y2 int) []float64 {
20 dx, sx := x2-x1, 1
21 if dx < 0 {
22 dx, sx = -dx, -1
23 }
24 dy, sy := y2-y1, 1
25 if dy < 0 {
26 dy, sy = -dy, -1
27 }
28 dy = -dy
29 err := dx + dy
30 out := []float64{}
31 for {
32 gray := color.GrayModel.Convert(img.At(x1, y1)).(color.Gray)
33 out = append(out, float64(gray.Y))
34 if x1 == x2 && y1 == y2 {
35 break
36 }
37 e2 := 2 * err
38 if e2 >= dy {
39 err += dy
40 x1 += sx
41 }
42 if e2 <= dx {
43 err += dx
44 y1 += sy
45 }
46 }
47 return out
48 }
Wie hell sind nun die Bildpunkte entlang der per Maus eingetragenen Geraden? Die Pixel in einem Farbbild liegen als Rot-, Grün, und Blauanteile vor. Alle drei tragen zur Helligkeitswahrnehmung bei, allerdings ist das menschliche Auge im Grün-Kanal fast fünfmal so empfindlich wie für Blautöne, und etwa doppelt so empfänglich wie für Rotanteile. Am einfachsten errechnet sich die Helligkeit eines Pixels mit der Funktion color.GrayModel.Convert (Zeile 32 in Listing 1), die ihn in einen Grauton umwandelt und dabei die drei Kanäle unterschiedlich gewichtet.
|
| Abbildung 5: Zur Messung der Schärfe durchwandert der Bresenham-Algorithmus das Bild entlang der Geraden. |
Zieht der User mit der Maus eine Gerade durchs Bild, gilt es, die Messwerte entlang dieser Geraden zwecks Analyse aus dem Bild zu fieseln. Dabei erstreckt sich die Gerade von den Koordinaten (x1, y1) nach (x2, y2). Wegen der endlichen Bildauflösung streift sie bei Steigungen, die nicht Vielfache von 45 Grad sind, manche Pixel nur zum Teil und es entsteht keine ideale Linie, sonden eine stufige Annäherung nach Abbildung 5. Ein schnelles Verfahren zum Ermitteln der gestreiften Pixel entlang der Geraden ist der 1962 von Jack Bresenham bei IBM erfundene Algorithmus ([3]).
Bei den Startkoordinaten (x1, y1) beginnend, arbeitet sich Listing 1 Pixel für Pixel in der "schnellen" X-Richtung nach rechts vor und macht hin und wieder einen Schritt in die "langsame" Y-Richtung nach oben, um der eingezeichneten Geraden zwischen Start- und Endpunkt zu folgen (Abbildung 5). Ob der Algorithmus einen Korrekturschritt in Y-Richtung einlegen muss, wird vom Wert einer Fehlervariablen err bestimmt. Die bekommt Strafpunkte im Wert von dy abgezogen, falls der Messpunkt nur in X-Richtung marschiert. Geht es aber in Y-Richtung nach oben, bekommt err den Wert dx aufgebrummt. Je nachdem, in welcher Richtung err den eingestellten Grenzwerte überschreitet, folgt ein Korrekturschritt in Richtung dx oder dy. Listing 1 verwendet übrigens die Variable e2 mit dem zweifachen Wert von err, aber nur deshalb, weil nach Bresenham dx/2 beziehungsweise dy/2 als Grenzwerte gelten und die Funktion Floating-Divisionen vermeiden will.
Wie stellt eine in Go geschriebene Fyne-GUI Linien dar, die der User mit der Maus aufzieht? Listing 2 definiert dazu ein Custom-Widget Linex, das, abgeleitet von Fynes widget.BaseWidget, drei zusätzliche Komponenten enthält: das darzustellende Foto in view, die gezogene rote Linie in line und einen eventuell erscheinenden blauen Rahmen frame, falls das Bild das schärfste aus der Serie ist.
Auf neumodischen Handys geschossene Fotos sind mit 4000x3000 schon recht groß, und eine Full-Frame-Kamera wie eine Sony A7 III liefert Monster im Format 6000x4000. In der GUI erscheinen die Bilder viel kleiner, und Fyne würde die notwendige Skalierung zwar ohne Murren vornehmen, aber dabei den Prozessor belasten und gewaltige Datenmengen zwischen RAM und Grafikkarte hin- und herschaufeln. Deshalb erzeugt Zeile 27 in Listing 2 ein 400 Pixel breites Abbild für die mühelose Darstellung, aber das Original lebt in orig fort und dient fürderhin bei der Schärfeanalyse.
001 package main
002 import (
003 "image"
004 "image/color"
005 _ "image/jpeg"
006 "os"
007 "fyne.io/fyne/v2"
008 "fyne.io/fyne/v2/canvas"
009 "fyne.io/fyne/v2/container"
010 "fyne.io/fyne/v2/widget"
011 "github.com/disintegration/imaging"
012 )
013 type Linex struct {
014 widget.BaseWidget
015 img image.Image
016 view *canvas.Image
017 line *canvas.Line
018 frame *canvas.Rectangle
019 dragging bool
020 onDone func(l *Linex, x1, y1, x2, y2 int)
021 x1, y1, x2, y2 float32
022 }
023 func NewLinex(path string, maxW int, cb func(l *Linex, x1, y1, x2, y2 int)) *Linex {
024 f, _ := os.Open(path)
025 defer f.Close()
026 orig, _, _ := image.Decode(f)
027 small := imaging.Resize(orig, maxW, 0, imaging.Lanczos)
028 frame := canvas.NewRectangle(color.Transparent)
029 frame.StrokeColor = color.White
030 frame.StrokeWidth = 4
031 lp := &Linex{
032 img: orig,
033 view: canvas.NewImageFromImage(small),
034 line: canvas.NewLine(color.Transparent),
035 frame: frame,
036 onDone: cb,
037 }
038 lp.view.FillMode = canvas.ImageFillContain
039 lp.line.StrokeWidth = 6
040 lp.ExtendBaseWidget(lp)
041 return lp
042 }
043 type lxLayout struct {
044 p *Linex
045 }
046 func (l *lxLayout) Layout(_ []fyne.CanvasObject, size fyne.Size) {
047 p := l.p
048 p.view.Resize(size)
049 s, ox, oy := p.geom()
050 sw := p.frame.StrokeWidth
051 b := p.img.Bounds()
052 w := float32(b.Dx()) * s
053 h := float32(b.Dy()) * s
054 p.frame.Move(fyne.NewPos(ox-sw/2, oy-sw/2))
055 p.frame.Resize(fyne.NewSize(w+sw, h+sw))
056 p.updateLine()
057 }
058 func (*lxLayout) MinSize(_ []fyne.CanvasObject) fyne.Size {
059 return fyne.NewSize(300, 200)
060 }
061 func (p *Linex) CreateRenderer() fyne.WidgetRenderer {
062 c := container.New(
063 &lxLayout{p},
064 p.frame,
065 p.view,
066 p.line,
067 )
068 return widget.NewSimpleRenderer(c)
069 }
070 func (p *Linex) geom() (s, ox, oy float32) {
071 z := p.Size()
072 b := p.img.Bounds()
073 iw := float32(b.Dx())
074 ih := float32(b.Dy())
075 s = z.Width / iw
076 if h := z.Height / ih; h < s {
077 s = h
078 }
079 ox = (z.Width - iw*s) / 2
080 oy = (z.Height - ih*s) / 2
081 return s, ox, oy
082 }
083 func (p *Linex) updateLine() {
084 s, ox, oy := p.geom()
085 p.line.Position1 = fyne.NewPos(ox+p.x1*s, oy+p.y1*s)
086 p.line.Position2 = fyne.NewPos(ox+p.x2*s, oy+p.y2*s)
087 canvas.Refresh(p.line)
088 }
089 func (p *Linex) Dragged(e *fyne.DragEvent) {
090 s, ox, oy := p.geom()
091 p.line.StrokeColor = color.RGBA{255, 0, 0, 255}
092 x := (e.Position.X - ox) / s
093 y := (e.Position.Y - oy) / s
094 if !p.dragging {
095 p.dragging = true
096 p.x1, p.y1 = x, y
097 }
098 p.x2, p.y2 = x, y
099 p.updateLine()
100 }
101 func (p *Linex) DragEnd() {
102 p.dragging = false
103 p.onDone(p, int(p.x1), int(p.y1), int(p.x2), int(p.y2))
104 }
Jedes handgestrickte Widget in Fyne benötigt einen Renderer, also eine Komponente, die es skaliert und in die GUI pflanzt. Die Zeilen 62 bis 66 packen Rahmen, Foto und die rote Linie mit New() in einen Container, dessen dynamisches Abbild Fyne zentral steuert. Das klappt auch, bis auf den blauen Rahmen, der das Foto luftdicht umschließen soll, und nicht den darumherum liegenden verfügbaren Platz.
Der Layout-Manager einer Fyne-GUI unterscheidet zwischen Widgets, deren Größe und Lage zentral gesteuert wird, und solchen, die ihren Auftritt selbst gestalten wollen. Zieht der User zum Beispiel mit der Maus das Applikationsfenster auf, erhält ein darin enthaltener VBox-Container das Signal, dass nun mehr Platz da ist, und er breitet sich sowohl horizontal als auch vertikal bis zum Rand aus. Liegen in diesem VBox-Container wiederum zum Beispiel zwei senkrecht übereinander gestapelte Label-Widgets, schickt er diesen ein Resize() Kommando, wobei er dafür sorgt, dass sie sich den vertikalen Freiraum brüderlich teilen.
Die einzelnen Kacheln der sharpgrep-App stellen nun jeweils Fotos mit einer bei Bedarf eingezeichneten roten Geraden dar. Diese sollte sich synchron zum Foto strecken, sobald der User das Applikationsfenster aufzieht. Allerdings müssen bei einem Foto immer die Proportionen zwischen Breite und Höhe stimmen, sonst sieht der Inhalt grotesk aus. Aus diesem Grund erhält das Image-Widget in Zeile 38 die Option ImageFillContain, die dafür sorgt, dass sich das Foto niemals verzerrt, sondern nicht weiter ausbreitet, falls entweder die Höhe oder Breite des verfügbaren Leerraums eine Beschränkung darstellt. Statt dessen bleibt entweder ein vertikaler oder seitlicher Rand.
Nun muss aber zum Beispiel das Linien-Widget wissen, wo die linke obere Ecke des Fotos nach einem Resize()-Event zu liegen kam, damit es sich relativ dazu im Bild positionieren kann. Auch der bei Schärfetestsiegern erscheinende blaue Rahmen um das Foto muss um dessen Lage im Container Bescheid wissen. Die Funktion geom() ab Zeile 70 in Listing 2 rechnet hierzu aus den Längen der Strecken in Abbildung 6 aus, an welchem Offset ox (oder oy im Fall von vertikalem Leerraum) der Ursprung des Fotos liegt. Der Wert z gibt dazu die Gesamtbreite des verfügbaren Raums für das Widget an, iw und ih die tatsächliche Breite und Höhe des darin unverzerrt hineingequetschten Fotos. Weiter rechnet geom() den Skalierungsfaktor s des Fotos aus, als Quotient aus den Maßen des dargestellten Fotos zu denen des Originalbildes.
|
| Abbildung 6: Damit das Foto seine Proportionen behält, fügt der Layout-Manager links und rechts Leerraum ein. |
Von Fyne zentral verwaltete Widgets mit Layout-Funktion definieren nur eine Funktion MinSize(), die festlegt, wie klein das Widget minimal werden darf. Alles andere bestimmt der zentrale Layout-Manager. Widgets ohne Layout-Funktion hingegen verlassen sich darauf, dass irgendjemand ihre Resize()-Methode aufruft, oder sie definieren ihr Layout selbst. Letzteres tut Listing 2 in der Layout()-Funktion ab Zeile 46, die sowohl das Foto an die vom Layout-Manager geforderte Größe size anpasst, als auch den luftdichten Rahmen frame zeichnet und die rote Linie maßgeschneidert verschiebt.
Außerdem muss das Linex-Widget wissen, welche Pixel im Originalbild die Endpunkte der eingezeichneten Geraden referenzieren, die ja im verkleinerten Foto der Darstellung erscheint. Nach zentrischer Streckung mit Offset (ox, oy) und einem Skalierungsfaktor s ist dies gleich erledigt und Dragged() ab Zeile 89 frischt die Koordinaten (x1,y1) bzw. (x2,y2) auf. Sie bestimmen die Endpunkte der Geraden als Pixelwerte im Originalbild und helfen später bei der Intensitätsmessung. Lässt der User beim Aufziehen der Geraden den Mausknopf los, springt Fyne die Funktion DragEnd() ab Zeile 101 an, und das Widget ruft den eingangs definierten Callback onDone auf, mit den Pixelkoordinaten im Originalbild.
Jedes Linex-Widget bekommt nun in Listing 3 ein Label mit nützlichen Textinformationen spendiert, sowie eine Funktion zum Aktivieren des blauen Rahmens, falls es sich um das Gewinner-Widget handelt. Lustigerweise ist der Rahmen immer da, nur erscheint er eingangs in Weiß, bleibt also unsichtbar. Die Select()-Funktion ab Zeile 47 färbt ihn bei Bedarf blau ein und der Aufruf von Refresh() in Zeile 53 lässt ihn aufleuchten (oder wieder verschwinden, falls ein neuer Gewinner feststeht).
01 package main
02 import (
03 "image/color"
04 "os/exec"
05 "fyne.io/fyne/v2"
06 "fyne.io/fyne/v2/container"
07 "fyne.io/fyne/v2/widget"
08 )
09 const ThumbnailWidth = 400
10 type Tile struct {
11 widget.BaseWidget
12 Path string
13 Linex *Linex
14 Label *widget.Label
15 Score float64
16 Rank int
17 con *fyne.Container
18 }
19 func NewTile(path string,
20 cb func(t *Tile, x1, y1, x2, y2 int)) *Tile {
21 label := widget.NewLabel(path)
22 label.Alignment = fyne.TextAlignCenter
23 tile := &Tile{
24 Path: path,
25 Label: label,
26 }
27 pl := NewLinex(path, ThumbnailWidth,
28 func(w *Linex, x1, y1, x2, y2 int) {
29 cb(tile, x1, y1, x2, y2)
30 })
31 con := container.NewBorder(container.NewPadded(), label, nil, nil, pl)
32 tile.Linex = pl
33 tile.con = con
34 tile.ExtendBaseWidget(tile)
35 return tile
36 }
37 func (t *Tile) TappedSecondary(*fyne.PointEvent) {
38 cmd := exec.Command("open", t.Path)
39 err := cmd.Start()
40 if err != nil {
41 panic(err)
42 }
43 }
44 func (t *Tile) CreateRenderer() fyne.WidgetRenderer {
45 return widget.NewSimpleRenderer(t.con)
46 }
47 func (t *Tile) Select(on bool) {
48 if on {
49 t.Linex.frame.StrokeColor = color.RGBA{0, 0, 255, 255}
50 } else {
51 t.Linex.frame.StrokeColor = color.White
52 }
53 t.Linex.frame.Refresh()
54 }
Weiter führt die Tile-Struktur Buch darüber, wie hoch der Schärfe-Score eines Fotos entlang der eingezeichneten Linie ist (score), und speichert den später im Hauptprogramm ermittelten Rang in der Hitparade aller Fotos. Drückt der User über einem Foto die rechte Maustaste, öffnet TappedSecondary() ab Zeile 37 das Foto in dem standardmäßig eingestellten Viewer.
Das Hauptprogramm in Listing 4 spannt das Applikationsfenster in Abbildung 3 auf und definiert ein zweispaltiges Grid-Widget, das alle Foto-Widgets aufnimmmt. Den Callback ab Zeile 21 springt die GUI an, falls der User eine Gerade in ein Bild gezeichnet und den Mausknopf losgelassen hat. Die Funktion Roughness() aus Listing 1 bestimmt dann die Zackigkeit des Intensitätsgraphen über die ausgewählten Pixel.
Der Aufruf von rankPhotos() in Zeile 22 springt die gleichnamige Funktion ab Zeile 41 an, die den ihr zugespielten Kachel-Array tiles durchforstet, die gewerteten Fotos nach ihrem Score sortiert und in jeder Tile-Struktur den Rang des Fotos in der Parade, von 1 aufsteigend, setzt. Die Reihenfolge in tiles bleibt dabei wegen der sonst chaotisch werdenden Darstellung gleich, statt dessen sortiert rankPhotos() mit einem Index-Hash. Beim Testsieger setzt Zeile 51 mit Select aus Listing 3 den blauen Rahmen und Zeile 53 zeigt Rang, Pfad und Score des Fotos im darunter liegenden Label-Widget an.
01 package main
02 import (
03 "flag"
04 "fmt"
05 "sort"
06 "fyne.io/fyne/v2"
07 "fyne.io/fyne/v2/app"
08 "fyne.io/fyne/v2/container"
09 )
10 func main() {
11 flag.Parse()
12 a := app.New()
13 w := a.NewWindow("Sharp Grep")
14 w.Resize(fyne.NewSize(1000, 700))
15 grid := container.NewGridWithColumns(2)
16 scroll := container.NewScroll(grid)
17 tiles := []*Tile{}
18 winner := ""
19 for _, ipath := range flag.Args() {
20 tile := NewTile(ipath,
21 func(t *Tile, x1, y1, x2, y2 int) {
22 t.Score = Roughness(t.Linex.img, x1, y1, x2, y2)
23 winner = rankPhotos(tiles)
24 },
25 )
26 tiles = append(tiles, tile)
27 grid.Add(tile)
28 }
29 w.SetContent(scroll)
30 w.Canvas().SetOnTypedKey(
31 func(ev *fyne.KeyEvent) {
32 key := string(ev.Name)
33 switch key {
34 case "Q":
35 w.Close()
36 fmt.Println(winner)
37 }
38 })
39 w.ShowAndRun()
40 }
41 func rankPhotos(tiles []*Tile) string {
42 idx := make([]int, len(tiles))
43 for i := range tiles {
44 idx[i] = i
45 }
46 sort.Slice(idx, func(i, j int) bool {
47 return tiles[idx[i]].Score > tiles[idx[j]].Score
48 })
49 for i, oidx := range idx {
50 tile := tiles[oidx]
51 tile.Select(i == 0)
52 tile.Rank = i + 1
53 tile.Label.SetText(
54 fmt.Sprintf("%d. %s %.1f", tile.Rank, tile.Path, tile.Score))
55 }
56 return tiles[idx[0]].Path
57 }
Der übliche Dreisprung mit go mod init/tidy, sowie ein nachfolgendes go build mit allen vier Listings erzeugt das Binary sharpgrep. Mit einer Reihe von Fotos von der Kommandozeile aus aufgerufen, stellt die Applikation diese in einer Matrix dar, und sobald der User in einem Foto die rote Gerade einzeichnet, findet die Messung statt. Bei mehreren so markierten Fotos erhält das schärfste den blauen Rahmen. Ein Tastendruck auf 'Q' bricht das Programm ab, nachdem es vorher noch schnell den Pfad zur Siegerdatei in die Standardausgabe geschrieben hat.
Listings zu diesem Artikel: http://www.linux-magazin.de/static/listings/magazin/2026/08/snapshot/
"Root Mean Square", https://en.wikipedia.org/wiki/Root_mean_square
"Bresenham-Algorithmus", https://de.wikipedia.org/wiki/Bresenham-Algorithmus