Eine ESP32-CAM streamt das Kamerabild, ein AMG8833 mit 64 Thermoelementen liefert das Wärmebild an ein Display-Board, ein Browser legt beides übereinander. Hier steht dieselbe Rechenkette an einer gezeichneten Szene: aus Tasse, Glas und Wand entsteht die 8×8-Matrix, wie der Sensor sie sieht; das Display rechnet sie mit dem Code des Boards zu 35 × 28 Kacheln in der Eisenpalette; das Dashboard legt sie mit seinen Reglern über das Kamerabild. Aufgabe wie am Gerät: den warmen Fleck mit Größe, Position und Spiegelung auf die Tasse legen — und dann die Entfernung ändern.
Vorgehen wie am Gerät (Manuskript Abschnitt 8): zuerst die Spiegelung prüfen, dann Größe, dann Position, bis der warme Fleck auf der Tasse liegt; speichern. Danach die Entfernung verstellen — die Parallaxe wandert, weil die beiden „Augen“ nicht am selben Ort sitzen.
Bilineare Interpolation der 64 Werte auf 35 × 28 Kacheln à 8 px, Eisenpalette mit 256 Stufen in RGB565, Skala geglättet (Faktor 0,3) mit mindestens 4 K Spreizung; oben am Balken der Max-, unten der Min-Wert der Skala. Doppelt so groß dargestellt wie das Gerät, damit die Kacheln sichtbar sind.
Netzname, Kennwort und die Adressen im Heimnetz sind in dieser Veröffentlichung ersetzt: der Netzname durch <WLAN-Name>, das Kennwort durch genauso viele x, wie es Zeichen hat, die Adressen durch <IP-der-Kamera> und <IP-des-Display-Boards>. Der Erzeuger dieser Seite trägt die Werte nicht.
Kommerzielle Wärmebildkameras blenden das Wärmebild halbtransparent über ein normales Kamerabild ein — so lassen sich Wärmequellen unmittelbar Objekten zuordnen („welche Klemme ist heiß?“, „wo verliert das Gehäuse Wärme?”). Dieser Versuch baut genau diese Bildfusion aus einfachen Bastelkomponenten nach:
Daneben demonstriert der Versuch zwei übertragbare Ingenieur-Lektionen:
| Komponente | Details |
|---|---|
| ESP32-Display-Board („CYD”-Typ) | ESP32 mit fest verbautem ILI9341-TFT (320×240, SPI); MicroPython |
| AMG8833 Grid-EYE (Adafruit-Breakout) | 8×8-IR-Array, Blickfeld 60°, Auflösung 0,25 °C, I2C (Adresse 0x69), Versorgung 3,0–3,6 V, ~4,5 mA |
| AI-Thinker ESP32-CAM + MB-Board | OV2640-Kamera (VGA), WLAN; Versorgung 5 V über USB |
| PC im selben Heimnetz | Python (Standardbibliothek) + Browser |
Der AMG8833 hängt am Display-Board (dort läuft er nachweislich):
| AMG8833 | Display-Board |
|---|---|
| VIN | 3,3 V |
| GND | GND |
| SCL | GPIO22 |
| SDA | GPIO27 |
| INT, ADO | frei |
Die ESP32-CAM braucht keinerlei Zusatzverdrahtung — nur USB-Versorgung über ihr MB-Board. Wichtig für die Fusion ist die Mechanik: Der AMG8833 ist unmittelbar neben dem Kameraobjektiv montiert und blickt in dieselbe Richtung (siehe Foto). Je kleiner der Versatz der beiden „Augen”, desto besser gelingt die Deckung — den Rest erledigen die Ausrichtregler im Dashboard.
Interne Verdrahtung des Display-Boards (fest, zur Dokumentation): Display an SPI2 (SCK 14, MOSI 13, CS 15, DC 2, RST 4, Hintergrundbeleuchtung 21).
Alles läuft über HTTP im Heimnetz — bewusst ohne Feldbus, Livebild und Livedaten direkt („HTTP liefert”):
┌────────────────────────┐ ┌───────────────────────────┐
│ ESP32-CAM │ │ ESP32-Display-Board │
│ ESP32_CAM_Stream.ino │ │ main.py (MicroPython) │
│ │ │ - AMG8833 lesen (I2C) │
│ MJPEG-Stream │ │ - Wärmebild lokal zeigen │
│ http://<CAM>:81/stream │ │ - Webserver Port 80: │
└──────────┬─────────────┘ │ /thermo /einstellung │
│ │ /speichern │
│ │ - ausrichtung.json │
│ └────────────┬──────────────┘
│ (Browser lädt Stream direkt) │ (JSON, 2×/s)
▼ ▼
┌──────────────────────────────────────────────────────────────┐
│ PC: thermo_cam_dashboard.py → Browser http://localhost:8084│
│ - Stream als <img>, Wärmebild als Canvas darüber │
│ - Regler: Position X/Y, Größe, Transparenz, Spiegelung │
│ - Temperaturskala (Eisenpalette), Datum/Uhrzeit │
│ - „Ausrichtung speichern" → dauerhaft aufs Display-Board │
└──────────────────────────────────────────────────────────────┘
Die drei Datenflüsse im Einzelnen:
GET /thermo beim Display-Board an und erhält
JSON: die 64 Temperaturen in °C plus die aktuelle Ausrichtung.GET /einstellung?ox=..&oy=..&sk=..&al=..&sp=..
über das Dashboard-Programm ans Display-Board;
GET /speichern legt sie dort dauerhaft in der Datei
ausrichtung.json ab. Beim nächsten Start lädt das Board sie
selbst, und das Dashboard übernimmt sie in die Regler — einmal
ausrichten genügt also.Panasonic-Infrarot-Array mit 8×8 = 64 Thermoelementen; jedes Pixel liefert die Temperatur der Fläche, die es „sieht” (Blickfeld insgesamt 60°×60°). Auslesung per I2C (Adresse 0x69, alternativ 0x68 über den ADO-Pin): 128 Bytes ab Register 0x80, je Pixel ein 12-Bit-Wert im Zweierkomplement mit 0,25 °C je Digit — die Umrechnung ist also eine Multiplikation. Bildrate intern 10 Hz. Versorgung strikt 3,0–3,6 V (~4,5 mA); das Adafruit-Breakout verträgt am VIN auch 5 V (eigener Regler), das nackte Bauteil nicht.
main.py (Display-Board, MicroPython)Das umfangreichste Programm des Versuchs. Es vereint vier Aufgaben: lokale Wärmebildanzeige, Sensor-Auslesung, Webserver und Ausrichtungs-Speicher.
| Konstante | Vorgabe | Bedeutung |
|---|---|---|
DYNAMISCHE_FARBSKALA |
True | Skala folgt geglättet den Messwerten; False = feste Grenzen |
FESTE_MIN_TEMP / FESTE_MAX_TEMP |
20 / 36 | Grenzen bei fester Skala (°C) |
MIN_SPANNE |
4.0 | kleinste Skalen-Spreizung (K) — verhindert Rausch-Konfetti bei gleichmäßig warmem Bild |
GLAETTUNG |
0.3 | Trägheit der dynamischen Skala (0 = starr, größer = nervöser) |
FARBSKALA_ANZEIGEN / SKALA_BREITE |
True / 40 | Farbbalken rechts ein/aus, Streifenbreite in Pixeln |
FELD_PIXEL |
8 | Kachelgröße des Wärmebilds in Pixeln |
FARBPALETTE |
“eisen” | “eisen” (schwarz→violett→rot→gelb→weiß) oder “regenbogen” |
UHR_ANZEIGEN |
True | Datum/Uhrzeit-Leiste unten |
WLAN_SSID / WLAN_PASS |
(eintragen!) | Heimnetz-Zugang — nötig für Webserver und NTP-Uhr |
ZEITZONE_OFFSET_H |
2 | Sommerzeit 2, Winterzeit 1 |
HTTP_PORT |
80 | Port des Mini-Webservers |
DISPLAY_IP = "…"), damit
sie ohne Thonny ins Dashboard übernommen werden kann. Ohne WLAN läuft
das Gerät als reine Lokalanzeige weiter.GLAETTUNG), eine Mindest-Spreizung
(MIN_SPANNE) verhindert, dass Rauschen bei einheitlicher
Szene zu wildem Farbflimmern wird. Der Farbbalken rechts trägt oben den
Max-, unten den Min-Wert der Skala.| Endpunkt | Antwort/Aktion |
|---|---|
/thermo |
JSON
{"t":[64 Temperaturen °C], "einst":{ox,oy,sk,al,sp}} |
/einstellung?ox=&oy=&sk=&al=&sp= |
Ausrichtung setzen (mit Grenzen: sk 25–400 %, al 0–100 %, sp Bit 0/1) |
/speichern |
Ausrichtung in ausrichtung.json schreiben
(Flash-Dateisystem, neustartfest) |
ESP32_CAM_Stream.ino (ESP32-CAM, Arduino/C++)Die Kamera ist bewusst auf ihre Kernaufgabe reduziert: Sie ist eine reine Netzwerkkamera. Der Sketch initialisiert die OV2640 (VGA 640×480, JPEG-Qualität 10, PSRAM-Doppelpuffer), verbindet sich mit dem WLAN und liefert einen MJPEG-Livestream über den eingebauten HTTP-Server:
http://<Kamera-IP>:81/stream
(Multipart-JPEG — jeder Browser zeigt ihn direkt an)Alternativ tut es der funktionsgleiche
ESP32_CAM_Thermo_Fusion.ino (er streamt identisch und
ignoriert den fehlenden Sensor) — für den Regelbetrieb ist der schlichte
Stream-Sketch die sauberere Wahl: so wenig Software wie nötig auf jedem
Gerät.
thermo_cam_dashboard.py (PC, Python)Reines Python ohne Zusatzpakete; startet einen lokalen Webserver und öffnet die Bedienoberfläche im Browser.
| Konstante | Vorgabe | Bedeutung |
|---|---|---|
ESP32CAM_IP |
(eintragen) | IP der Kamera — nur für die Stream-URL |
DISPLAY_IP |
(eintragen) | IP des Display-Boards — Wärmedaten und Ausrichtung |
STREAM_PORT |
81 | Streamport der Kamera |
THERMO_INTERVALL |
0.5 | Sekunden zwischen zwei Wärmedaten-Abfragen |
WEB_PORT |
8084 | Port der Bedienoberfläche (http://localhost:8084) |
Der eigentliche Fusions-Trick ist bewusst einfach und nutzt den Browser als Grafikprozessor:
<img>-Element in einer 640×480-Bühne.left/top = Offset),
gespiegelt (transform: scale) und teiltransparent gemacht
(opacity).| Regler | Bereich | Wirkung |
|---|---|---|
| Position X / Y | ±250 px | Wärmebild verschieben |
| Größe | 25–400 % | Wärmebild skalieren (100 % = Bildbreite; Sensor ist quadratisch) |
| Transparenz | 0–100 % | Sichtbarkeit des Wärmebilds (45 % = guter Start) |
| horizontal/vertikal spiegeln | an/aus | Einbaulage des Sensors ausgleichen |
| „Ausrichtung speichern” | — | Einstellung dauerhaft aufs Display-Board |
Dazu: Statuspunkt (Verbindung zum Display-Board), Temperaturskala mit Max/Min (geglättet), Datum/Uhrzeit, Fehlerband bei Störungen. Beim ersten Verbinden übernimmt das Dashboard die auf dem Board gespeicherte Ausrichtung in die Regler.
Physikalische Grenzen, ehrlich benannt: Kamera und Sensor sitzen wenige Zentimeter auseinander (Parallaxe) — eine Ausrichtung gilt daher streng nur für eine Entfernung; bei sehr nahen Objekten wandert die Deckung. Für Szenen ab etwa einem Meter ist der Fehler mit 8×8 Wärme-Pixeln praktisch unsichtbar. Auch unterscheiden sich die Blickfelder (Sensor 60° quadratisch, Kamera ~65° in 4:3) — Offset und Skalierung gleichen das im Rahmen der groben Wärmebild-Auflösung vollständig aus.
main.py
WLAN-Zugangsdaten eintragen; Datei mit Thonny als main.py
aufs Board. Neustart → das Display zeigt 5 s die Zeile
DISPLAY_IP = "…", danach das Wärmebild.ESP32_CAM_Stream.ino
geflasht, USB-Versorgung. Kontrolle im Browser:
http://<CAM-IP>:81/stream zeigt das Livebild.thermo_cam_dashboard.py beide
IPs eintragen; python thermo_cam_dashboard.py; Browser auf
http://localhost:8084.Danach ist der Betrieb zweigleisig: Das Display-Board arbeitet autark als Wärmebildgerät (mit Skala und Uhr), das Dashboard zeigt zusätzlich die Fusion — beides gleichzeitig, beliebig ein- und ausschaltbar.
Der erste Entwurf schloss den AMG8833 direkt an die freien Pins der ESP32-CAM an (GPIO13/14, SD-Bus). Es folgte eine lehrreiche Fehlersuche, deren Ergebnis hier festgehalten ist, weil es für alle ESP32-CAM-Projekte gilt:
POWERON_RESET), sobald die I2C-Leitungen
angeschlossen sind; Zeichensalat in der seriellen Ausgabe; Sensor wird
nie gefunden — obwohl Sensor (Kreuztest am Display-Board), CAM-Pins
(Drahtbrücken-Loopback-Test), Kabel (Tausch) und Versorgung einzeln
nachweislich in Ordnung waren.ESP32_CAM_I2C_Test.ino (Pin-Suchlauf)
und ESP32_CAM_Pin_Loopback.ino (Brückentest) bleiben im
Projektordner für künftige Fälle.| Symptom | Ursache / Abhilfe |
|---|---|
| Dashboard: „Keine Verbindung zum Display-Board” | DISPLAY_IP falsch/veraltet (Display-Neustart zeigt sie 5 s an); Board ohne WLAN (Startbildschirm „KEIN WLAN!“) |
| Kein Kamerabild | Stream-URL direkt testen
(http://<CAM>:81/stream); CAM-Versorgung (kräftiges
USB); nur ein Betrachter je Stream |
| Wärmebild ruckelt im Dashboard | normal ist 2 Hz (THERMO_INTERVALL); kleiner stellen belastet das Display-Board kaum |
| Regler wirken nicht | Verbindung zum Display-Board prüfen (Statuspunkt); Browserseite neu laden |
| Ausrichtung nach Neustart weg | „Ausrichtung speichern” wurde nicht gedrückt (Regler wirken sofort, gespeichert wird nur auf Knopfdruck) |
| Display zeigt Wärmebild, aber Dashboard nichts | PC und Boards im selben Netz? Firewall-Freigabe für Python? |
| Uhrzeit falsch | NTP beim Start ohne WLAN fehlgeschlagen;
ZEITZONE_OFFSET_H prüfen (Sommer 2 / Winter 1) |
| Datei | Gerät | Rolle |
|---|---|---|
main.py |
Display-Board | aktiv: Wärmebild + Webserver + Ausrichtungs-Speicher |
ESP32_CAM_Stream.ino |
ESP32-CAM | aktiv: MJPEG-Stream |
thermo_cam_dashboard.py |
PC | aktiv: Fusions-Dashboard (Port 8084) |
ausrichtung.json |
Display-Board (automatisch) | gespeicherte Ausrichtung |
main_waermebild_v1.py / v2.py |
Archiv | frühere Display-Stände (v1 = Original, v2 = ohne Webserver) |
ESP32_CAM_Thermo_Fusion.ino |
Archiv | Variante mit Sensor an der CAM (siehe Abschnitt 10) |
ESP32_CAM_I2C_Test.ino,
ESP32_CAM_Pin_Loopback.ino |
Werkzeug | Diagnose-Sketche aus der Fehlersuche |
VTermo1.jpg, VTermo2.jpg |
Doku | Fotos des Versuchsaufbaus |
/alarm setzen oder die
RGB-LED des Boards schalten./thermo-Quellen;
das Dashboard könnte zwischen ihnen umschalten.Die Seite Waermebild_<Fassung>.html enthält den
Versuch ohne Gerät: eine gezeichnete Szene, aus der die 64 Sensorwerte
entstehen, das Display-Board mit dem Code von main.py und
das Dashboard mit dem Code von thermo_cam_dashboard.py. Was
am Gerät gerechnet wird, wird hier mit denselben Zeilen gerechnet; nur
die Szene und der Sensor sind Modell. Alle Zahlen sind aus der Geometrie
abgeleitet und stehen hier zum Nachrechnen.
Vor einer Wand mit Umgebungstemperatur steht ein Tisch ( K), darauf eine warme Tasse (, Henkel K, Henkelloch ) und ein kaltes Glas (). Die Größen sind für 1 m festgelegt (Tasse 90 × 130 px im Kamerabild von 640 × 480 px) und skalieren mit , wenn die Entfernung verstellt wird. Die Temperaturkarte der Szene hat 160 × 120 Zellen à 4 px.
Die Kamera bildet 65° in 4:3 auf 640 px ab; ihre Brennweite in Pixeln ist Der Sensor sieht 60° quadratisch; im Kamerabild sind das Deshalb liegt die richtige Größe im Dashboard bei , was der Erfahrungswert „grob 90–110 %“ aus Abschnitt 8 bestätigt.
Die Sensorachse ist gegen die Kameraachse versetzt: um den Montagewinkel (in der Seite einstellbar, Vorgabe 1,5°) und um die Parallaxe aus dem Abstand der beiden „Augen“ (Vorgabe 3 cm): Der Winkelanteil ist eine feste Verschiebung, die der Regler „Position X“ ein für alle Mal ausgleicht. Der Parallaxenanteil hängt von der Entfernung ab: bei cm sind es 15 px bei 1 m, aber 50 px bei 0,3 m — das ist die in Abschnitt 8 benannte Grenze, und die Seite zeigt sie, wenn nach dem Speichern die Entfernung verstellt wird. Ein Sensorpixel misst 72,5 px; oberhalb von etwa einem Meter bleibt der Parallaxenfehler darunter.
Jedes der 64 Pixel sieht und mittelt über sein Feld (10 × 10 Stützstellen der Temperaturkarte; außerhalb des Kamerabilds gilt die Wandtemperatur). Dazu kommt gaußsches Rauschen mit einstellbarer Streuung (Vorgabe 0,25 K) und die Quantisierung auf 0,25 °C je Digit wie im 12-Bit-Wert des Bausteins. Ein um 180° gedrehter Einbau vertauscht Zeilen und Spalten — dann sind beide Spiegelungs-Häkchen nötig, wie in Abschnitt 8 beschrieben.
Das Display-Board rechnet in der Seite mit den Funktionen aus
main.py, Zeile für Zeile übertragen:
interpoliere_zeile (bilinear 8 × 8 → 35 × 28),
baue_palette (256 Stufen, Eisenpalette, RGB565), die
geglättete Skala mit GLAETTUNG = 0.3 und
MIN_SPANNE = 4.0, der Farbbalken mit Max oben und Min
unten, die Uhrzeile. Das Dashboard verwendet den
<script>-Teil von
thermo_cam_dashboard.py unverändert: farbe,
malen, die Regler und deren Grenzen; „Ausrichtung
speichern“ legt die Werte im Browser ab, wo am Gerät
ausrichtung.json liegt.
Als Hilfe beim Ausrichten nennt die Seite die Deckung: den Schwerpunkt der Sensorpixel oberhalb der Mitte zwischen Min und Max, mit der eingestellten Lage in Bühnenkoordinaten gebracht und mit der Tassenmitte verglichen. Der Knopf „Rechnerische Ausrichtung übernehmen“ setzt die aus 14.2 folgenden Werte — für die eingestellte Entfernung.
Die Seite wird im echten Browser geprüft
(seite/pruefe_seite.mjs, Ergebnisse in
seite/PRUEFPLAN_Seite.md): keine Fehler beim Laden,
Bedienelemente auf dem ersten Bildschirm, die Interpolation und die
Paletten-Indizes gegen eine unabhängige Nachrechnung mit dem Python-Code
aus main.py (seite/referenz_anzeige.py), Min
und Max der Matrix gegen die eingestellten Temperaturen bei
abgeschaltetem Rauschen, die Deckung nach rechnerischer Ausrichtung
besser als ein Sensorpixel, die Parallaxe nach Entfernungswechsel, die
Dokumentation in der Seite.