IoT and AI

Google Coral Edge TPU with MQTT: Classify Images on a Raspberry Pi and Publish Results

A hands-on tutorial that sets up a Google Coral Edge TPU on a Raspberry Pi, runs a quantized MobileNet classifier and publishes every prediction as JSON to an MQTT broker.

Coral Edge TPU USB accelerator plugged into a Raspberry Pi sending image classification results over MQTT to the cloud
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

The Google Coral Edge TPU lets a small board such as a Raspberry Pi run neural network inference far faster than its CPU alone, which makes real-time image classification practical at the edge. In this tutorial you will install the Coral Edge TPU runtime, run a quantized MobileNet classifier and publish each prediction as structured JSON over MQTT, so dashboards, alerting tools and other services can react to what the camera sees.

What You Need

The setup is deliberately simple so you can focus on the data path from accelerator to broker.

Item Purpose
Raspberry Pi 4 or 5 (64-bit Raspberry Pi OS) Host computer that runs Python and the MQTT client
Coral USB Accelerator Edge TPU coprocessor connected over USB 3
A camera or a folder of JPEG images Input for the classifier
Python 3.9 environment The Python version range supported by the official Coral Python packages
An MQTT broker Receives results; we use broker.connectmq.com for testing

Plug the accelerator into a blue USB 3 port. A USB 2 port works but noticeably slows down transfers between the Pi and the accelerator, which hides much of the benefit.

How the Coral Edge TPU Fits an IoT Pipeline

The Edge TPU is an ASIC that only executes TensorFlow Lite models that are fully integer-quantized and compiled with the Edge TPU compiler. Operations the compiler cannot map run on the CPU, so a well-compiled model keeps almost everything on the accelerator.

In an IoT architecture the accelerator is one stage of a loop:

  1. A camera frame or image arrives on the Pi.
  2. The Edge TPU classifies it in milliseconds.
  3. The Pi publishes a compact JSON result (labels, scores, latency, device ID) to an MQTT topic.
  4. Subscribers such as Node-RED, a database writer or an alerting script consume the result.

Only the inference result leaves the device, never the raw image. That keeps bandwidth low, avoids sending pictures of people or premises to the cloud and fits well within MQTT message limits. If you are still deciding where inference should run, our comparison of edge AI vs cloud AI covers the trade-offs in detail.

Step 1: Install the Edge TPU Runtime

Add the Coral package repository and install the standard-frequency runtime:

echo "deb [signed-by=/usr/share/keyrings/coral-edgetpu.gpg] https://packages.cloud.google.com/apt coral-edgetpu-stable main" | sudo tee /etc/apt/sources.list.d/coral-edgetpu.list
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo gpg --dearmor -o /usr/share/keyrings/coral-edgetpu.gpg
sudo apt-get update
sudo apt-get install -y libedgetpu1-std

Unplug and replug the accelerator after installing so the udev rules apply. Check that the device is visible:

lsusb | grep -Ei "global unichip|google"

Before the first inference the accelerator reports as "Global Unichip Corp."; after the runtime loads firmware it re-enumerates as "Google Inc.". Both are normal.

There is also a libedgetpu1-max package that runs the chip at a higher clock. It is faster but the accelerator gets hot, so start with the standard runtime and only switch if you have measured that you need it and have airflow.

Step 2: Create a Compatible Python Environment

The most common Coral problem is a version mismatch between Python, the TensorFlow Lite runtime and the Edge TPU library. The official Coral Python packages target Python 3.6 to 3.9, while current Raspberry Pi OS ships a newer Python. The reliable path is a dedicated Python 3.9 environment, for example with pyenv:

sudo apt-get install -y build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev libffi-dev liblzma-dev
curl https://pyenv.run | bash
# add the pyenv lines it prints to ~/.bashrc, then open a new shell
pyenv install 3.9.19
pyenv virtualenv 3.9.19 coral
pyenv activate coral
pip install --extra-index-url https://google-coral.github.io/py-repo/ "pycoral~=2.0"
pip install "paho-mqtt>=2.0" pillow "numpy<2"

Installing pycoral from the Coral package index also pulls in the matching tflite_runtime build, which is what our script imports. Pinning NumPy below 2 avoids binary incompatibility errors with these older wheels.

Download a compiled model and its labels from the Coral test data repository:

mkdir -p ~/coral-mqtt && cd ~/coral-mqtt
curl -LO https://github.com/google-coral/test_data/raw/master/mobilenet_v2_1.0_224_quant_edgetpu.tflite
curl -LO https://github.com/google-coral/test_data/raw/master/imagenet_labels.txt
curl -LO https://github.com/google-coral/test_data/raw/master/parrot.jpg

Step 3: Classify Images and Publish to MQTT

The script below loads the model with the Edge TPU delegate, classifies every image in a folder and publishes the top three predictions. It uses paho-mqtt 2.x with the version 2 callback API.

import json
import sys
import time
from pathlib import Path

import numpy as np
from PIL import Image
import paho.mqtt.client as mqtt
from tflite_runtime.interpreter import Interpreter, load_delegate

MODEL = "mobilenet_v2_1.0_224_quant_edgetpu.tflite"
LABELS = "imagenet_labels.txt"
BROKER = "broker.connectmq.com"
PORT = 1883
DEVICE_ID = "pi-coral-01"
# Replace 7f3a9c with your own random string so your topics are unique
TOPIC = f"connectmq/demo/7f3a9c/coral/{DEVICE_ID}/classification"
STATUS_TOPIC = f"connectmq/demo/7f3a9c/coral/{DEVICE_ID}/status"


def load_labels(path):
    labels = {}
    with open(path, encoding="utf-8") as f:
        for i, line in enumerate(f):
            parts = line.strip().split(maxsplit=1)
            if len(parts) == 2 and parts[0].isdigit():
                labels[int(parts[0])] = parts[1]
            else:
                labels[i] = line.strip()
    return labels


labels = load_labels(LABELS)
interpreter = Interpreter(
    model_path=MODEL,
    experimental_delegates=[load_delegate("libedgetpu.so.1")],
)
interpreter.allocate_tensors()
inp = interpreter.get_input_details()[0]
out = interpreter.get_output_details()[0]
_, height, width, _ = inp["shape"]


def classify(image_path, top_k=3):
    img = Image.open(image_path).convert("RGB").resize((width, height), Image.LANCZOS)
    data = np.expand_dims(np.asarray(img, dtype=np.uint8), axis=0)
    interpreter.set_tensor(inp["index"], data)
    start = time.perf_counter()
    interpreter.invoke()
    latency_ms = (time.perf_counter() - start) * 1000
    raw = interpreter.get_tensor(out["index"])[0].astype(np.float32)
    scale, zero_point = out["quantization"]
    probs = (raw - zero_point) * scale if scale else raw
    top = np.argsort(probs)[::-1][:top_k]
    results = [
        {"label": labels.get(int(i), str(i)), "score": round(float(probs[i]), 4)}
        for i in top
    ]
    return results, latency_ms


def on_connect(client, userdata, flags, reason_code, properties):
    print(f"Connected: {reason_code}")
    client.publish(STATUS_TOPIC, "online", qos=1, retain=True)


client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=f"{DEVICE_ID}-7f3a9c")
client.on_connect = on_connect
client.will_set(STATUS_TOPIC, "offline", qos=1, retain=True)
client.connect(BROKER, PORT, keepalive=60)
client.loop_start()

folder = Path(sys.argv[1] if len(sys.argv) > 1 else ".")
try:
    for image_path in sorted(folder.glob("*.jpg")):
        results, latency_ms = classify(image_path)
        payload = {
            "device": DEVICE_ID,
            "image": image_path.name,
            "top": results,
            "latencyMs": round(latency_ms, 2),
            "ts": int(time.time() * 1000),
        }
        info = client.publish(TOPIC, json.dumps(payload), qos=1)
        info.wait_for_publish()
        print(payload)
        time.sleep(1)  # stay well under the public broker's rate limit
finally:
    client.publish(STATUS_TOPIC, "offline", qos=1, retain=True).wait_for_publish()
    client.loop_stop()
    client.disconnect()

Run it against the downloaded parrot image:

python classify_mqtt.py ~/coral-mqtt

The first inference is slower because the model is loaded onto the accelerator; subsequent ones show the steady-state latency.

Step 4: Watch the Coral Edge TPU Results Arrive

Open a second terminal (on any machine) and subscribe with the Mosquitto clients:

mosquitto_sub -h broker.connectmq.com -p 1883 -i viewer-7f3a9c -t "connectmq/demo/7f3a9c/coral/#" -v

You should see the retained online status followed by one JSON message per image, for example a top label of "macaw" for the parrot test picture. Because broker.connectmq.com is a free public MQTT broker, anyone who guesses your topic can read it, so only send demo data there. The public broker allows up to 64 KB per message and 10 messages per second per client, which is plenty for classification results but a reason not to stream raw frames.

Moving From Test to Production

When the prototype works, switch to authenticated, encrypted connections. Create a broker in the ConnectMQ portal, copy the generated credentials and change the connection block:

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="YOUR_CLIENT_ID")
client.username_pw_set("YOUR_USERNAME", "YOUR_PASSWORD")
client.tls_set()  # uses the system CA store; the broker has a Let's Encrypt certificate
client.will_set(STATUS_TOPIC, "offline", qos=1, retain=True)
client.connect("YOUR_BROKER_HOST", 8883, keepalive=60)

A managed MQTT broker removes the work of patching, certificates and capacity planning, so the Pi only has to worry about inference. With ConnectMQ the client ID is bound to the issued credentials, which prevents another device from impersonating your camera node.

Design Tips for Edge TPU Projects

A few decisions make the difference between a demo and a system you can leave running:

  1. Publish events, not every frame. For a live camera, publish only when the top label changes or crosses a confidence threshold, plus a periodic heartbeat. This keeps traffic predictable.
  2. Include context in the payload. Device ID, model name and version, latency and timestamp make results debuggable months later.
  3. Use retained status with a last will. The retained online/offline topic lets dashboards show which accelerators are alive.
  4. Structure topics hierarchically. Something like site/area/line/camera/classification scales better than a flat namespace and maps cleanly to ISA-95 levels.
  5. Plan model updates. Compiling a new model is easy; rolling it out safely to many devices is not. Our guide on how to deploy ML models to edge devices shows how to trigger and track updates over MQTT.

Troubleshooting

Symptom Likely cause Fix
ValueError: Failed to load delegate from libedgetpu.so.1 Runtime not installed, device not detected or permission issue Reinstall libedgetpu1-std, replug the device, check lsusb, add your user to the plugdev group
Segmentation fault on invoke() Mismatched tflite_runtime and libedgetpu versions Use the Python 3.9 environment and install pycoral from the Coral package index
Inference is slow Model not compiled for the Edge TPU or USB 2 port Use a file ending in _edgetpu.tflite, move to a USB 3 port
numpy.core.multiarray failed to import NumPy 2 with old binary wheels pip install "numpy<2"
Messages never appear in the subscriber Different topic string or empty client ID Compare topics exactly; always set a non-empty, unique client ID

The official Coral documentation lists the supported platforms and the Edge TPU compiler requirements if you want to compile your own models.

Where to Go Next

Image classification is the simplest workload; the same publishing pattern works for object detection and pose estimation. If you need bounding boxes instead of labels, see YOLO object detection with MQTT. To compare the Coral accelerator with alternatives such as Jetson modules and microcontrollers, read the edge AI hardware overview, and for the bigger picture of combining models with connected devices, start from our AI and IoT guide.

Get Started with ConnectMQ

Your Edge TPU results deserve a reliable home. ConnectMQ gives you a cloud MQTT broker with TLS, WebSockets and a live topic explorer to watch classifications as they arrive. Create a free Serverless broker in a few minutes, no credit card required, and point your Raspberry Pi at it.

Frequently Asked Questions

Does the Coral Edge TPU work with Raspberry Pi 5?

Yes, the Coral USB Accelerator connects over USB and works on a Raspberry Pi 5 with a 64-bit OS. The main hurdle is the Python version, which is why this tutorial uses a separate Python 3.9 environment.

Can I run any TensorFlow model on the Edge TPU?

No. The model must be a fully integer-quantized TensorFlow Lite model compiled with the Edge TPU compiler. Unsupported operations fall back to the CPU and reduce the speed advantage.

Should I send camera images over MQTT?

Usually not. Publish inference results and send only occasional snapshots if needed; images quickly exceed sensible message sizes and raise privacy concerns.

Which QoS level should I use for classification results?

QoS 1 is a good default because it guarantees delivery at least once without the overhead of QoS 2. Include a timestamp so subscribers can ignore duplicates.

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.