Willkommen zurück bei Heron Watch 🕵️♂️🐦#
Im letzten Beitrag habe ich meinen etwas absurden, aber durchaus ernst gemeinten Versuch vorgestellt, meinen Gartenteich vor einem gefiederten Fischdieb zu schützen. Kurz gesagt: KI erkennt Graureiher + Sprinkleranlage = niemand fasst die Koi an.
👈 Zuvor in der Reiher-Saga…
Diesmal gehen wir allerdings unter die Haube.
Dieser Blogeintrag dreht sich ganz um die Architektur – das System von Microservices, das hinter den Kulissen zusammenarbeitet, um den Reiher in Aktion zu erwischen. Es ist herrlich über-engineered und zugleich eine großartige Demonstration von verteiltem Systemdesign in einem realen (ok, Hinterhof-)Anwendungsfall.
Legen wir los.
🧩 Der Gesamtüberblick#
Hier ist die gesamte Architektur auf hoher Ebene:
(Home Assistant)"] %% ─────────────────────────────────────────── %% 4. Inference Orchestrator -->|POST image| Inference Inference["inference-svc"] Inference -->|JSON results| Orchestrator Orchestrator -->|publishes detections| detections Orchestrator -->|publishes predicted JPEGs| predicted_frames %% ─────────────────────────────────────────── %% 5. Clip extraction ClipExtractor["clip-extractor-svc"] detections --> ClipExtractor %% ─────────────────────────────────────────── %% 6. Clip buffer & storage ClipBuffer["clip-buffer-svc"] ClipBuffer -->|maintains ring| Buffer subgraph Storage["Storage"] direction TB Buffer["/buffer (ring segments)"] Clips["/clips (saved MP4s)"] end Buffer --> ClipExtractor ClipExtractor -->|writes clips| Clips Clips --> Dashboard %% ─────────────────────────────────────────── %% 7. Dashboard & user subgraph DashboardGrp["Dashboard"] direction TB predicted_frames --> Dashboard Dashboard["dashboard-svc"] Dashboard -->|serves live + clips| User User["User / Browser"] end
Das mag wie ein unübersichtliches Pfeilgewirr aussehen, aber keine Sorge. Wir zoomen in jeden Abschnitt hinein und erkunden die Services, die diese Hinterhof-Wachhund-Magie möglich machen.
📨 Warum RabbitMQ?#
Bevor wir uns den Service-Details widmen, lassen Sie uns über Message Broker sprechen. In einer Microservice-Architektur müssen verschiedene Komponenten effizient kommunizieren, ohne sich laut über den Raum zu rufen. Genau hier kommt RabbitMQ ins Spiel.
RabbitMQ ist ein leichtgewichtiger, bewährter Message Broker, der asynchrone Kommunikation zwischen Services ermöglicht. Ein Service kann eine Nachricht (z. B. einen neuen Frame) veröffentlichen, und ein anderer kann sie abholen und etwas Nützliches damit anstellen (z. B. Inferenz durchführen, speichern, ignorieren).
In diesem Projekt ist RabbitMQ die Verbindung zwischen:
- Dem Kameraframe-Grabber
- Der KI-Inferenz-Engine
- Der Erkennungs-Pipeline
- Der Clip-Extraktions-Logik
- Der Dashboard-UI
- Und allem anderen, das eventbasierte Kommunikation benötigt
RabbitMQ unterstützt Features wie persistente Queues, Routing-Keys und Fanout-Exchanges, was es ideal für diese Art von lose gekoppelter, ereignisgetriebener Architektur macht.
🛠️ Hätte ich stattdessen MQTT verwenden können? Ja, absolut. Tatsächlich könnte für kleinere Projekte oder reinen Smart-Home-Einsatz MQTT allein ausreichen – vor allem mit Lösungen wie MQTT Image. Aber ich sah hier eine großartige Gelegenheit, RabbitMQ als echten Message Broker mit Queues und Fanout zu demonstrieren und nicht nur als simples Pub/Sub-Rohr.
🚀 Was ist mit Kafka + MinIO? Wenn dies ein großmaßstäbliches Überwachungssystem mit tausenden Kameras und mehreren TB Video-Puffer wäre, würde ich zu Apache Kafka greifen und die Aufnahmen auf MinIO oder S3 speichern. Aber das hier ist ein Hinterhof-Projekt mit genau einer IP-Kamera und einem Vogel.
📸 frame-capture-svc: Das wachsame Auge#
Dieser Service hat eine Aufgabe – und erfüllt sie mit ungetrübtem Fokus (oder… Objektivfokus?). Er verbindet sich mit einer RTSP-fähigen IP-Kamera, extrahiert JPEG-Schnappschüsse bei konfigurierter FPS und wirft sie in die raw_frames
-Queue von RabbitMQ, damit der Rest der Pipeline sie verarbeitet.
Denken Sie an ihn als den Wachturm des Systems – stets wachsam, stets im Stream.
So ist es in der docker-compose.yml
konfiguriert:
frame-capture-svc:
build: ./frame-capture-svc
environment:
- RTSP_URL=rtsp://user:password@ip_address:554/live/ch1
- RABBIT_HOST=rabbitmq
- FPS=2
volumes:
- ./frame-capture-svc/frames:/app/frames
depends_on:
- rabbitmq
🔍 Einige Details#
RTSP_URL
zeigt auf den IP-Kamerastream.RABBIT_HOST
gibt an, wo RabbitMQ läuft.FPS=2
bedeutet, dass wir zwei Bilder pro Sekunde erfassen – mehr als genug für unseren gefiederten Einbrecher.- Das Volume-Mapping dient nur dem Debugging – so kann man sehen, welche Bilder tatsächlich erfasst wurden.
🧠 inference-svc: Das Gehirn#
Sobald ein Frame erfasst ist, leitet der orchestrator-svc
(dazu gleich mehr) ihn an den inference-svc
weiter, der ein YOLOv8 ONNX-Modell kapselt. Hier findet die Objekterkennung statt.
Der Service liefert ein sauberes JSON mit erkannten Objekten, Klassen, Bounding Boxes und Konfidenzwerten zurück. Er ist mit FastAPI + Uvicorn + ONNX für eine flotte Performance aufgebaut.
inference-svc:
build: ./inference-svc
volumes:
- ./inference-svc/model:/app/model
environment:
- RABBIT_HOST=rabbitmq
- MODEL_PATH=/app/model/best.onnx
- CONF_THRESHOLD=0.6
depends_on:
- rabbitmq
🔍 Einige Details#
MODEL_PATH
verweist auf die ONNX-Modell-Datei.CONF_THRESHOLD
ist der Konfidenzschwellenwert für Erkennungen (0,6 bedeutet 60 % Sicherheit).RABBIT_HOST
– na klar, den RabbitMQ-Host brauchen wir auch hier.- Das Volume-Mapping ist der Speicherort der Modell-Datei.
- Das Modell wird einmal geladen und für alle Anfragen wiederverwendet, was effizient ist.
Warum ein separater Service? Weil:
- wir die Inferenz unabhängig skalieren wollen (bei Bedarf weitere Instanzen)
- die Modell-Logik isoliert und testbar bleibt
- wir Modelle einfach austauschen können (z. B. YOLOv4, YOLOv7, YOLOv9 etc.)
🕹 orchestrator-svc: Der Puppenspieler#
Das Herz des Systems.
Er orchestriert alles, indem er:
- Frames aus der
raw_frames
-Queue konsumiert - die Frames an den
inference-svc
zur Erkennung schickt - die vorhergesagten Frames in die
predicted_frames
-Queue veröffentlicht - die Erkennungsergebnisse überprüft und wenn eine interessante Klasse erkannt wird:
- die Erkennungsergebnisse in die
detections
-Queue veröffentlicht - einen MQTT-Trigger an Home Assistant (oder ein anderes System) sendet
- die Erkennungsergebnisse in die
Betrachten Sie ihn als Dirigenten in unserem reiherjagenden Orchester.
orchestrator-svc:
build: ./orchestrator-svc
environment:
- RABBIT_HOST=rabbitmq
- INFERENCE_URL=http://inference-svc:8000/detect
- DETECTION_CLASSES=heron # or list: heron,person,dog
- MQTT_HOST=homeassistant.fritz.box
- MQTT_PORT=1883
- MQTT_USERNAME=admin
- MQTT_PASSWORD=redacted
depends_on:
- rabbitmq
🔍 Einige Details#
RABBIT_HOST
ist der RabbitMQ-Broker.INFERENCE_URL
ist die URL des Inferenz-Services.DETECTION_CLASSES
ist eine kommagetrennte Liste der zu erkennenden Klassen (z. B.heron,person,dog
).MQTT_HOST
ist der MQTT-Broker (bei mir Home Assistant).MQTT_PORT
,MQTT_USERNAME
undMQTT_PASSWORD
sind für die MQTT-Verbindung.depends_on
stellt sicher, dass RabbitMQ läuft, bevor der Orchestrator startet.- Der Orchestrator ist der einzige Service, der den MQTT-Broker kennt, weshalb nur er diese Zugangsdaten besitzt.
🌀 clip-buffer-svc: Der Ringmeister#
Dieser Service zeichnet fortlaufend kleine Segmente (z. B. 5 Sekunden HLS-Chunks) auf und hält sie in einem Ringpuffer in /buffer
vor.
Die Idee dahinter: Schon bevor ein Reiher erkannt wird, liegt das Filmmaterial bereits in einer Dauerschleife bereit. So verpassen wir nicht den „Anflug“-Teil der Aktion.
Ringpuffer + schneller Zugriff = saftige Pre-Roll-Clips.
clip-buffer-svc:
build: ./clip-buffer-svc
environment:
- RTSP_URL=rtsp://user:password@ip_address:554/live/ch1
volumes:
- buffer:/buffer
depends_on:
- rabbitmq
🔍 Einige Details#
Hier gibt es nicht viel zu sagen:
RTSP_URL
ist wie zuvor die RTSP-URL der Kamera./buffer
ist ein Docker-Volume, das die Ringsegmente speichert.- Es nutzt FFmpeg zur Verarbeitung des Videostreams.
- Jedes Segment erhält den Timestamp seiner Aufnahme als Namen, sodass man später das richtige leicht findet.
✂️ clip-extractor-svc: Der Editor#
Wenn der orchestrator
einen Reiher erkennt, veröffentlicht er eine Nachricht in der detections
-Queue. Der clip-extractor-svc
konsumiert Nachrichten aus dieser Queue und holt sich die relevanten Segmente aus dem Ringpuffer.
Er verwendet einen 10-Sekunden-Pre-Roll und einen 10-Sekunden-Post-Roll, um die gesamte Aktion aufzunehmen. Nach der Erkennung wartet er kurz, damit der Reiher verschwunden ist, bevor die Aufnahme beendet wird. Anschließend fügt er die Segmente mit FFmpeg zu einer einzigen MP4-Datei zusammen und speichert sie in /clips
. Nach dem Speichern wird für das Dashboard ein Thumbnail generiert.
clip-extractor-svc:
build: ./clip-extractor-svc
environment:
- RABBIT_HOST=rabbitmq
- BUFFER_DIR=/buffer
- OUTPUT_DIR=/clips
volumes:
- buffer:/buffer
- clips:/clips
depends_on:
- rabbitmq
- clip-buffer-svc
🔍 Einige Details#
RABBIT_HOST
ist der RabbitMQ-Broker.BUFFER_DIR
ist der Pfad zum Ringpuffer im Container.OUTPUT_DIR
ist der Pfad, um die Clips im Container zu speichern.- Die
volumes
mounten die Buffer- und Clips-Verzeichnisse. depends_on
stellt sicher, dass der Clip-Buffer läuft, bevor der Clip-Extractor startet.
Ja, der Reiher steht jetzt in seinen eigenen Überwachungsclips im Rampenlicht.
📊 dashboard-svc: Das Kontrollzentrum#
Ein Flask + Socket.IO Dashboard, das:
- Live-Frames streamt
- die neuesten Erkennungen zeigt
- Zugriff auf gespeicherte Clips bietet
- Logs und Status der Services anzeigt
Perfekt, um live mitzuerleben, wie der Reiher leicht in seiner Pläne gestört wird.
dashboard-svc:
build: ./dashboard-svc
ports:
- "5000:5000"
environment:
- RABBIT_HOST=rabbitmq
- CLIPS_DIR=/clips
volumes:
- clips:/clips
depends_on:
- rabbitmq
🔍 Einige Details#
RABBIT_HOST
ist natürlich erneut der RabbitMQ-Broker.CLIPS_DIR
ist der Pfad zu den gespeicherten Clips im Container.- Die
volumes
mounten das Clips-Verzeichnis. - Die
ports
legen das Dashboard auf Port 5000 frei. depends_on
stellt sicher, dass RabbitMQ läuft, bevor das Dashboard startet.
🧩 Erkenntnisse zum Systemdesign#
Sie fragen sich, wie das Setup funktioniert? Jeder Service läuft in seinem eigenen Docker-Container, definiert in einer docker-compose.yml
. Die Kommunikation erfolgt über RabbitMQ, und das Ganze ist modular aufgebaut – tauschen Sie ein Modell aus, fügen Sie weitere Kameras hinzu oder aktualisieren Sie einen Service, ohne den Rest anzufassen.
Umgebungsvariablen ermöglichen die Konfiguration ohne Codeänderungen – über eine .env
-Datei oder direkt in docker-compose
.
Während der Entwicklung sind keine Container nötig. Einfach die Services lokal mit Standardwerten starten. Nur für den RTSP-Stream benötigt man eine echte oder virtuelle Kamera, aber ich arbeite an einem gemockten RTSP-Setup mit einem Beispielvideo und zwei leichten Containern.
🧑💻 Benutzererfahrung#
Das alles erreicht den Benutzer – mich – schließlich über ein Web-Dashboard. Von hier aus kann ich Erkennungen überprüfen, das System debuggen oder einfach bei einer frischen Tasse Kaffee das Drama der Tierüberwachung genießen.

🧠 Warum Microservices?#
Sie fragen sich vielleicht: „Warum nicht einfach einen Monolithen schreiben?“
Gute Frage. Darum bin ich voll auf Microservices gesetzt:
- Modularität: Jeder Service ist einfach und fokussiert
- Skalierbarkeit: Führen Sie
inference-svc
bei Bedarf auf einer stärkeren Maschine aus - Fehlerisolation: Ein Ausfall betrifft nicht das gesamte System
- Asynchronität: RabbitMQ entkoppelt alles sauber
Außerdem… weil es Spaß macht.
Bleiben Sie dran für zukünftige Beiträge dieser Serie:
- Annotieren mit Stil (und Geschwindigkeit)
- Tipps für Live-Deployments
- Dashboard-UI- & Integrations-Tricks
Bis dahin: frühzeitig erkennen, verantwortungsvoll spritzen.
Fun Fact: Reiher hassen plötzliche laute Geräusche. Offenbar leben sie nicht nach YOLO – sie zucken zusammen.