K.A.R.L
Header by Justin Kunimune - Own work, CC BY-SA 4.0
K.A.R.L ist eine WebApp die einem dabei hilft 360Grad Panoramas in Sektionen einzuteilen, (Himmel, Boden, Bäume usw…) und dann den Anteil der einzelnen Sektionen am Gesamtbild festzustellen.
Einleitung
Das Projekt ist aus der Zusammenarbeit mit zwei Freunden entstanden. Der eine steckt gerade mitten in der Konzeptionsphase seiner Bachelorarbeit (Geographie), die sich mit der Auswirkung von Vegetation auf das Stadtklima beschäftigt. Dazu hat er an verschiedenen Orten in Köln Albedo Messungen vorgenommen, also quasi “wieviel Licht kommt vom Himmel, und wieviel davon wird vom Boden reflektiert”. Um diese Messungen in den richtigen Kontext zu setzen hat er von jedem Messort 360 Panoramas angelegt, diese sehen ungefähr so aus:
Dazu brauchte er jetzt Angaben wieviel Prozent der Sicht zum Beispiel Vegetation, Himmel und Boden sind. Um das zu messen hat er in Gimp per Hand eine Segmentationsmap erstellt, eine Segmentationsmap sieht etwas so aus:
Problemstellung
Wenn wir jetzt einfach naiv hingehen und die Pixel der einzelnen Farben zählen und daraus eine prozentuale Verteilung machen kriegen wir das klassische Problem mit der Verzerrung das die Menschheit schon seit Jahrhunderten mit Karten hat. Undzwar lassen sich Kugeln nur sehr ungern zwei dimensional darstellen, dabei kommt es immer zu Verzerrungen, wie folgendes Bild visualisiert.
Zum Glück passiert diese Verzerrung nur in der Breite, wir brauchen also eine Formel die uns für die Höhe eines Pixels eine Gewichtung gibt um diese Verzerrung auszugleichen. Nach vielen Versuchen sind wir bei dieser Formel gelandet:
/*
height: height of the image in pixels
calibrationFactor: 1.333, somehow this works, dont ask why, we dont either
y: y position of the pixel
*/
const pixelValue = Math.cos(
(((360 / height ** 2) * y ** 2 + (-360 / height) * y + 90) / 360) *
(2 + calibrationFaktor) *
Math.PI
)
Diese Formel ist eigentlich dafür gedacht für einen bestimmten Breitengrad den Abstand zwischen zwei Längengraden zu berechnen, aber für unsere Zwecke funktioniert sie auch sehr gut.
Hier noch einiger der ersten Versuch in Desmos (fantastisches Tool btw):
Die Technologien
Ich hatte den Wunsch eine WebApp zu bauen die reintheoretisch auch komplett offline funktioniert. Deswegen werden nach dem ersten laden der Website keine weiteren Daten mehr verschickt, sondern alles wird im Browser ausgeführt. Dies ist bei einer Anwendung die sehr viele Pixel bearbeiten und so sehr viel Performance braucht schwierig da die Ressourcen des Browsers begrenzt sind. Folgende Technologien haben mir dabei geholfen die User Experience trotzdem halbwegs okay zu gestalten:
Canvas
Wenn es um irgendwas mit Pixeln im Browser geht kommt man an Canvas2D eigentlich gar nicht vorbei. Canvas ist durch seine programmatische Schnittstelle optimal für diesen Job geeignet.
Web Workers
Normalerweise werden im Browser alle Operationen einer Website in einem Thread ausgeführt. Dies führt dazu eine rechenintensive Aufgabe die komplette Website zum erliegen bringen kann. Dabei können WebWorker Abhilfe schaffen. WebWorker können beliebigen Code in einem seperaten Thread ausführen mit dem einzigen Hinderniss das die Kommunikation mit dem Haupt Thread etwas schwierig ist.
Bei K.A.R.L werden zwei WebWorker eingesetzt, der pixel-worker ist für das Analysieren der Segmentationsmap und für das Eimer Werkzeug verantwortlich und der ai-worker fürs ausführen des Tensorflow Codes.
Tensorflow
Warum per Hand malen nach zahlen machen wenn der Rechner das ganz automagisch kann? Hab ich mir auch gedacht und Tensorflow mit dem ade20k eingebaut. Dieses Netwerk ist super darin Bäume, Boden, Beton und Himmel zu erkennen. In der Editor Ansicht versteckt sich diese Funktion rechts unter dem “AI” Knopf.
IndexDB
Wenn es darum geht größere Mengen an Daten (vorallem Bilder), lokal im Browser zu speichern geht das eigentlich nur richtig mit IndexDB (ja, localStorage mit Base64 Bildern könnte auch gehen, ist aber in machen Browsern auf 5mb beschränkt). Da die IndexDB Api aber zu einer der verwirrensten Browser Api’s weit und breit gehört benutze ich idb, eine fantastische kleine Wrapperlibrary um IndexDB rum die auch noch Promises unterstützt.
Interessantes…
FloodFill Allgorithmus
Nachdem ich einige Panoramas per Hand mit den Tools segmentiert habe ist mir aufgefallen das man sehr oft einfach Regionen die eine ähnliche Farbe haben anmalt. Das brachte mich auf die Idee ein Fill Tool ähnlich wie in Photoshop zu bauen, aber vieeel besser.
Das Tool funktioniert ungefähr so:
- Der User klickt mit dem Fill Tool auf das Bild
- Die x/y Koordinaten des Klicks sowie alles Pixel des Bildes werden an einen WebWorker gesendet
- Dieser erstellt ein Graustufen Bild, bei dem der Wert jedes Pixels der Abstand (räumlich und farblich) zu dem Bereich ist der geklickt wurde.
- Das Bild wird zurückgeschickt
- Im Canvas Code wird dann aufgrund des Wertes der einzelnen Pixel entschieden ob dies gefüllt werden oder nicht
Svelte Bindings
Ich fand es immer kompliziert State über mehrere Komponenten hinweg zu regeln. Ein Beispiel wäre der Editor, er besteht aus drei einzelnen Komponenten (Toolbar, Topbar, PaintArea) die alle wissen müssen welches Farbe und welches Werkzeug gerade aktiv sind. Man könnte diesen State in einen globalen Store schreiben und in jedem der einzelnen Komponenten darauf zugreifen, aber eigentlich ist das aktive Werkzeug nur in dem Editor Kontext wichtig. Also liegt dieser State jetzt in dem Editor Komponent der in per binding an seine Unterkomponenten weitergibt, das ganze sieht dan ungefähr so aus:
<!-- Editor.svelte -->
<script lang="ts">
import TopBar from "./TopBar.svelte";
let activeColor = "ff0000";
let _activeColor = localStorage.getItem("activeColor");
if (_activeColor) {
activeColor = _activeColor;
}
</script>
<TopBar bind:activeColor/>
<!-- TopBar.svelte -->
<script>
export let activeColor = "ff0000";
</script>
{{activeColor}}
Svelte Stores
Für einige andere Sachen kann man hingegen super Stores verwenden, zum Beispiel für Toasts/Modals. So kann man den State für die Toasts (Dass sind die Nachrichten unten Links) super in einem Komponent packen und muss sie nicht über mehrere hinweg verteilen.