IoT and AI

YOLO Object Detection over MQTT: Publish Detections as JSON

Run a YOLO model on a webcam or RTSP stream in Python, turn each frame's detections into compact JSON, and publish counts and alerts over MQTT so any dashboard or automation can react.

YOLO object detection MQTT illustration with bounding boxes on a camera frame and JSON detection messages flowing to a broker
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

YOLO object detection over MQTT turns a camera into a sensor: instead of streaming video, you publish small JSON messages describing what the camera sees. In this tutorial you will run a YOLO model on a webcam or RTSP stream with Python, publish per-frame detections, maintain object counts and raise alerts that any MQTT client can consume.

Why Send Detections Instead of Video

Video is heavy, hard to search and needs a person to watch it. Detections are light, structured and machine-readable. A single message like "3 persons, 1 forklift in zone A" is a few hundred bytes, while the frame it came from is tens or hundreds of kilobytes.

Publishing detections over MQTT also decouples vision from everything else:

  • Dashboards subscribe to counts and show occupancy in real time.
  • An automation service subscribes to alerts and switches a warning light.
  • A historian stores counts per minute for reporting.
  • Another model, perhaps on a different machine, combines vision with other sensors.

None of these consumers need to know which camera, model or GPU produced the result. That is the same loose coupling that makes MQTT popular for sensors, now applied to computer vision on edge devices.

Architecture

camera (USB / RTSP) -> Python + YOLO -> MQTT broker -> dashboards, alerts, storage

connectmq/demo/<id>/site1/dock/cam1/detections   every processed frame (throttled)
connectmq/demo/<id>/site1/dock/cam1/counts       retained, current count per class
connectmq/demo/<id>/site1/dock/cam1/alerts       event when a rule triggers
connectmq/demo/<id>/site1/dock/cam1/status       retained, online/offline (Last Will)

Prerequisites

  • Python 3.9 or newer, a webcam or an RTSP camera
  • A CPU is enough for small models at a few frames per second; a GPU or Jetson makes it real time
  • Packages:
pip install ultralytics opencv-python "paho-mqtt>=2.0"

The first run downloads the pretrained yolov8n.pt weights (the nano model trained on the 80 COCO classes). See the Ultralytics documentation for other model sizes and newer versions; the API used below works across them.

The Detection Publisher

Save as yolo_mqtt.py. It opens the camera, runs YOLO, converts results to JSON, publishes detections at most twice per second, updates retained counts when they change, and raises an alert when people are present in a restricted zone.

import json
import time
import uuid

import cv2
import paho.mqtt.client as mqtt
from ultralytics import YOLO

SOURCE = 0  # webcam index, or "rtsp://user:pass@192.168.1.50:554/stream1"
PREFIX = "connectmq/demo/yolo-c83f/site1/dock/cam1"
CLASSES = {"person", "car", "truck", "forklift"}  # forklift only with a custom model
MIN_CONF = 0.5
PUBLISH_EVERY = 0.5  # seconds
ZONE = (0.6, 0.0, 1.0, 1.0)  # restricted zone: right 40% of the frame (x1, y1, x2, y2)

model = YOLO("yolov8n.pt")
names = model.names

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
                     client_id=f"yolo-cam1-{uuid.uuid4().hex[:6]}")
client.will_set(f"{PREFIX}/status", json.dumps({"online": False}), qos=1, retain=True)
client.connect("broker.connectmq.com", 1883, keepalive=30)
client.loop_start()
client.publish(f"{PREFIX}/status", json.dumps({"online": True, "model": "yolov8n"}),
               qos=1, retain=True)

cap = cv2.VideoCapture(SOURCE)
last_pub, last_counts, zone_alert = 0.0, None, False


def in_zone(cx, cy):
    x1, y1, x2, y2 = ZONE
    return x1 <= cx <= x2 and y1 <= cy <= y2


try:
    while True:
        ok, frame = cap.read()
        if not ok:
            time.sleep(1)
            cap.open(SOURCE)  # reconnect for RTSP drops
            continue
        h, w = frame.shape[:2]
        result = model(frame, conf=MIN_CONF, verbose=False)[0]

        objects, counts, people_in_zone = [], {}, 0
        for box in result.boxes:
            label = names[int(box.cls)]
            if label not in CLASSES:
                continue
            x1, y1, x2, y2 = box.xyxyn[0].tolist()  # normalized 0..1
            cx, cy = (x1 + x2) / 2, (y1 + y2) / 2
            objects.append({"label": label, "conf": round(float(box.conf), 2),
                            "box": [round(v, 3) for v in (x1, y1, x2, y2)]})
            counts[label] = counts.get(label, 0) + 1
            if label == "person" and in_zone(cx, cy):
                people_in_zone += 1

        now = time.time()
        if now - last_pub >= PUBLISH_EVERY:
            last_pub = now
            client.publish(f"{PREFIX}/detections", json.dumps(
                {"ts": now, "frame": [w, h], "objects": objects}), qos=0)

        if counts != last_counts:
            last_counts = counts
            client.publish(f"{PREFIX}/counts", json.dumps({"ts": now, **counts}),
                           qos=1, retain=True)

        if people_in_zone and not zone_alert:
            zone_alert = True
            client.publish(f"{PREFIX}/alerts", json.dumps(
                {"ts": now, "rule": "person-in-zone", "state": "raised",
                 "count": people_in_zone}), qos=1)
        elif not people_in_zone and zone_alert:
            zone_alert = False
            client.publish(f"{PREFIX}/alerts", json.dumps(
                {"ts": now, "rule": "person-in-zone", "state": "cleared"}), qos=1)
finally:
    cap.release()
    client.publish(f"{PREFIX}/status", json.dumps({"online": False}), qos=1, retain=True)
    client.loop_stop()
    client.disconnect()

Watch the output from another terminal:

mosquitto_sub -h broker.connectmq.com -p 1883 -i yolo-watch-c83f -t "connectmq/demo/yolo-c83f/#" -v

Walk into the right side of the frame and you will see a raised alert, followed by cleared when you leave.

Designing the Detection Payload

A detection message from the script looks like this:

{
  "ts": 1792140112.42,
  "frame": [1280, 720],
  "objects": [
    {"label": "person", "conf": 0.88, "box": [0.612, 0.204, 0.731, 0.958]},
    {"label": "car", "conf": 0.74, "box": [0.051, 0.402, 0.338, 0.811]}
  ]
}

Guidelines that keep YOLO object detection MQTT payloads useful:

Choice Recommendation Reason
Box coordinates Normalized 0 to 1 Consumers do not need the resolution; zones survive camera changes
Confidence Rounded to 2 decimals Saves bytes without losing meaning
Frames Never in the detections topic Keep images on a separate topic or out of MQTT entirely
Rate Throttle to 1 to 5 messages per second Consumers rarely need every frame
Counts Retained, published on change New dashboards see the current state immediately
Model info In the retained status topic Lets you trace results to a model version

Counting and Alert Rules

Counting objects per frame is easy; counting objects that pass by is harder because the same person appears in many frames. For throughput counting, enable tracking in Ultralytics (model.track(frame, persist=True)), which assigns an ID to each object, and count an ID once when its center crosses a line. Publish the crossing as an event rather than a count, and let a downstream service aggregate per minute.

Alert rules belong close to the camera when latency matters (a person entering a robot cell) and downstream when they are business logic (more than five trucks waiting at the dock for ten minutes). Because results are on MQTT, you can move a rule from one place to the other without changing the camera code.

Consuming Detections Downstream

The value of publishing YOLO object detection results over MQTT shows up on the consuming side. A few patterns that teams build in the first weeks:

  • Occupancy dashboard. Subscribe to the retained counts topic of every camera with a wildcard such as site1/+/+/counts. Each new dashboard session receives the current count of every camera immediately, then live updates.
  • Per-minute statistics. A small Python or Node-RED service aggregates counts per minute and writes them to a time-series database. Reports on peak hours or dock utilization come from there, not from video.
  • Interlocks and warnings. An automation service subscribes to the alerts topic and switches a warning light or sends a message to a supervisor. Keep safety-critical interlocks in certified hardware; camera alerts are a supplement, not a replacement.
  • Cross-checking with other sensors. A person alert combined with a light barrier or a door contact is far more reliable than either alone.

Because each of these is just another subscriber, adding one never slows down the camera. The broker fans the same message out to all of them.

Privacy and Data Minimization

Cameras that see people come with responsibilities. Publishing only labels, counts and normalized boxes, never faces or frames, is the strongest privacy measure you can take, and it fits naturally with this architecture. If you need snapshots for verification, store them locally with a short retention period or send them to a private topic that only authorized services can read. Document what the camera detects and why, and check local regulations before deploying in workplaces or public areas.

Troubleshooting

  1. Very low frame rate on CPU. Use the nano model, lower the input size with imgsz=416, or process every second frame.
  2. RTSP stream freezes. OpenCV buffers frames; read continuously in a separate thread and process the most recent frame only.
  3. Flickering alerts. Require a detection in several consecutive frames before raising, and several empty frames before clearing.
  4. Messages rejected or dropped. On the public broker, stay under 10 messages per second per client and 64 KB per message; throttling handles this.
  5. Wrong classes. The pretrained model knows COCO classes only. For forklifts, helmets or specific products, train a custom model on your own images.

Production Notes

For real cameras, publish to a private broker over TLS (port 8883) with credentials, and never send frames of people to a public topic. A managed MQTT broker such as ConnectMQ handles authentication, certificates and monitoring, and its live topic explorer is a quick way to confirm detections are flowing. Use the public MQTT test broker only for development with test scenes.

If your camera runs on an NVIDIA board, see NVIDIA Jetson with MQTT for GPU-accelerated inference, and for a low-cost variant on a microcontroller see ESP32-CAM person detection. The AI and IoT guide explains how vision fits with other edge AI workloads.

Get Started with ConnectMQ

Create a private ConnectMQ broker on the free Serverless plan at app.connectmq.com, no credit card needed, add client.username_pw_set("YOUR_USERNAME", "YOUR_PASSWORD") and client.tls_set(), switch to port 8883, and your camera's detections are flowing securely.

Frequently Asked Questions

Can I send camera frames over MQTT together with detections?

You can send JPEG snapshots on a separate topic within the broker's message size limit, but keep continuous video on a streaming protocol. Detections should stay small and frame-free.

Which YOLO model size should I use?

Start with the nano model for CPUs and small boards, and move up to small or medium only if accuracy is not good enough. Larger models need a GPU for real-time rates.

How do I count people instead of detecting them per frame?

Use the tracker to assign IDs and count each ID once when it crosses a line or enters a zone. Publish each crossing as an event and aggregate downstream.

Does the broker run the YOLO model?

No. The model runs on your camera computer and the broker only carries the resulting messages to whoever subscribes.

Related articles

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.