# imageZMQ: pushing OpenCV frames off a Raspberry Pi farm without a broker

> imageZMQ is a MIT-licensed Python package that transports OpenCV images between computers over ZeroMQ, with a message format of a text header plus an image and two messaging patterns, blocking REQ/REP and non-blocking PUB/SUB. The judgement is that the design is a deliberate choice to avoid a broker, and that the package is a thin transport that leaves compression, ordering and backpressure to you.

**jeffbass/imagezmq** — A set of Python classes that transport OpenCV images from one computer to another using PyZMQ messaging.

- Repository: https://github.com/jeffbass/imagezmq
- Stars: 1,072 · Forks: 159
- Language: Python
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/jeffbass-imagezmq

## Eleven lines on the Pi, eight on the hub

imageZMQ is a set of Python classes that transport OpenCV images from one computer to another using PyZMQ messaging. The README's opening example is a screen on a Mac showing simultaneous image streams from 8 Raspberry Pi cameras, and the whole point of the package is the size of the code.

The receiver on the display machine is short:

```python
import cv2
import imagezmq
image_hub = imagezmq.ImageHub()
while True:  # show streamed images until Ctrl-C
    rpi_name, image = image_hub.recv_image()
    cv2.imshow(rpi_name, image) # 1 window for each RPi
    cv2.waitKey(1)
    image_hub.send_reply(b'OK')
```

The sender on each Pi is similar:

```python
import socket
import time
from picamera2 import Picamera2
import imagezmq

sender = imagezmq.ImageSender(connect_to='tcp://jeff-macbook:5555')

rpi_name = socket.gethostname() # send RPi hostname with each image
picam = Picamera2()
picam.start()
time.sleep(2)  # allow camera sensor to warm up
while True:  # send images as stream until Ctrl-C
    image = picam.capture_array()
    sender.send_image(rpi_name, image)
```

The structure of the two programs is the shape of the whole design. The sender captures, names itself with `socket.gethostname()`, and calls `send_image`. The hub receives a name and an image pair, opens a window per name, and sends back a short reply.

The two-line sleep after `picam.start()` is the kind of detail that only appears in code that has been run on real hardware. A camera sensor needs time to warm up before the first frame is usable, and skipping that wait gives you a dark or noisy first frame.

The package also compresses. The feature list includes the ability to send jpeg compressed OpenCV images to lighten network loads, and the `examples/` directory has `faster_jpg_hub.py` and `faster_jpg_send.py` for that path.

## The message is a text header plus an image

Each imageZMQ message is a `(text_message, image)` tuple. The text portion identifies the source and carries other information about the image.

In the example above, that text portion is the hostname of the sending Raspberry Pi, which is what lets the receiver put each unique stream into a specific window. The hub calls `cv2.imshow(rpi_name, image)`, so the name is not decoration, it is the key that maps a frame to a window on the display machine.

The reason to separate the two parts rather than just shipping bytes is that a receiver almost always needs to know something about a frame before it can process it. Which camera sent it, when it was captured, whether it has been processed already. Putting that in a text header means it travels with the frame and does not have to be re-derived, and it means the image payload stays a plain image with no envelope bolted onto it.

The API surface is small. `ImageSender` takes a `connect_to` address and has a `send_image` method. `ImageHub` has `recv_image` and `send_reply`. That is the whole contract, and the examples directory reflects it with files named for the pattern they use, such as `with_ImageHub.py` and `with_ImageSender.py`.

There is also a less common surface, because PyZMQ exposes the underlying socket, and the examples directory includes `advanced_http.py`, `advanced_pub.py` and `advanced_send.py`. The hub's reply is the other half of that: in REQ/REP the sender waits for it, in PUB/SUB the hub provides no reply at all.

## REQ/REP and PUB/SUB differ in what you lose

Two ZeroMQ messaging patterns are implemented, and the README describes the difference in terms of what the sender waits for.

With REQ/REP, each sender sends an image and waits for a reply from the central image hub, sending a new image only when the reply is received. The documentation calls it a blocking pattern for the sender.

With PUB/SUB, each sender sends an image but does not expect a reply and can continue sending without awaiting any acknowledgement. The hub provides no reply. It is a non-blocking pattern for the sender.

REQ/REP is the default, and the choice has real consequences. In REQ/REP the sender is throttled by the hub: if the hub is slow, the sender waits, which is a form of backpressure that keeps memory bounded. In PUB/SUB there is no such coupling, and the images the hub never receives are simply lost, because publish-subscribe has no delivery guarantee. For a live video view where a dropped frame does not matter, that is exactly what you want. For a system where every frame counts, it is not.

The repository ships a dedicated document on this choice, `docs/req-vs-pub.rst`, and the examples directory has a matching pair of files, `pub_sub_broadcast.py` and `pub_sub_receive.py`, alongside `timeout_req_ImageSender.py` for the REQ/REP side, which suggests timeouts are the part of the blocking pattern that needs the most attention in practice.

The pattern is chosen at construction. `ImageSender(connect_to=...)` uses the default, and the alternatives are set on the hub and sender objects rather than being a runtime switch, because a sender and a hub have to agree on the pattern for the exchange to work at all.

## No broker, on purpose

The README has a section asking why ZMQ rather than another messaging protocol, and the reasoning is specific enough to be useful.

The author says he looked at MQTT, RabbitMQ, AMQP and ROS as alternatives, and chose ZMQ with its Python PyZMQ bindings for three reasons. ZMQ does not require a message broker, because it is a peer to peer protocol, so an image does not need to be passed first to a message broker and then to the image hub. That means fewer running processes and less double handling of images, and since OpenCV images are large compared to simple text messages, the absence of a broker is important. ZMQ is very fast for passing OpenCV images, enabling high throughput between senders and hubs. And ZMQ and its PyZMQ bindings are easy to install.

The broker argument is the substantive one, and it is worth thinking about in your own context. A broker-based design has genuine advantages, and they are not addressed here. It gives you persistence, so a hub that is down does not lose the images queued for it. It gives you fan-out to many consumers. And it gives you a place to put retry and rate limiting logic. What it costs is an extra process and an extra copy of every image, which for a stream of large frames is a large cost.

The design targets the case where the senders are cheap cameras and the hub is the expensive machine, and the images are consumed as they arrive rather than durably queued.

The evidence offered for the choice is the author's own deployment. The README states that imageZMQ has been transporting images from a dozen Raspberry Pi computers scattered around a farm to 2 Linux image hub servers for over 5 years, with the Pis capturing and sending dozens to thousands of frames a day, and that it has worked very reliably and is very fast.

## Installation is a virtual environment and a shared OpenCV

The installation section is short on commands and long on warnings, and the warnings are about environment mismatch.

The package is on PyPI as `imagezmq`, with a PyPI README at `PyPI_README.rst` that is shorter than the repository README, and a `HISTORY.md` for the changelog. The `setup.py` declares `python_requires=">=3.5"` and install requirements of `pyzmq>=16.0` and `numpy>=1.13`. The build itself is plain setuptools through `pyproject.toml`.

The compatibility matrix in the documentation is explicit. The package has been tested with Python 3.5 through 3.11, PyZMQ 16.0, 17.1, 19.0 and 26.0, NumPy 1.13, 1.16, 1.18 and 1.24, OpenCV 3.3, 4.0, 4.1 and 4.6, Raspberry Pi OS Bookworm and Bullseye using PiCamera2, and Raspbian Buster, Stretch and Raspbian Jessie using the legacy PiCamera.

The instruction that matters most is about matching environments. Be sure to install OpenCV, including NumPy, into a Python virtual environment, and be sure to install imageZMQ into the same virtual environment. A mismatch between the OpenCV build and the imageZMQ install is a common source of failure that is entirely avoidable.

The OpenCV install itself is called out as challenging, with many tutorials on the web, and for Raspberry Pi computers with current Raspberry Pi OS versions the Picamera2 documentation recommends installing OpenCV using apt. The apt route avoids compiling OpenCV from source on a Pi, which is the usual cause of the difficulty.

## A mature package with an old release tag

The version picture is worth a note, because it is where the packaging metadata and the release history disagree.

The tagged releases are v1.0.1 on 2020-02-12, described as the first production release, v1.1.1 on 2020-05-23 adding new methods, and v.1.2.0 on 2024-07-18, a production release. The tag spelling is inconsistent, with a dot before the 1.2.0 version, and that is a small signal of a project that was not fastidious about its own release tooling.

The `setup.py` classifier says `Development Status :: 5 - Production/Stable`, and the package version is read from `imagezmq/__version__.py` rather than being duplicated in the manifest. The last push to the repository was on 2026-06-09 and the repository is not archived, so the code continues to move even though the newest tagged release is from 2024.

The licence is MIT, recorded in the package metadata as `License :: OSI Approved :: MIT License` with the text in `LICENSE.txt` at the repository root. That is a permissive grant with no copyleft obligation, which is a licensing observation rather than legal advice, and `LICENSE.txt` governs. The repository also carries a `SECURITY.md`, a `CODE_OF_CONDUCT.md`, a `CITATION.cff` and a `CONTRIBUTING.md`.

There is a `tests/` directory in the repository, and the `examples/` directory has a README of its own. The `docs/` directory holds the reStructuredText source of the documentation, including the `imagezmq-uses.rst` page on distributed computer vision projects and the `more-details.rst` page that explains the message tuples in detail.

## Conclusion

imageZMQ fits a builder with a cluster of small single-purpose computers, typically Raspberry Pis with cameras, that need to push frames to a more capable machine for heavier processing, and who wants peer-to-peer transport without standing up a broker. It does not fit a production system that needs guaranteed delivery, ordering, or a durable queue, because neither messaging pattern in the package provides those, and it does not fit a project that has to move large uncompressed frames over a slow link without doing its own compression. Verify first that the two messaging patterns match your loss tolerance, since PUB/SUB drops images the hub never receives and REQ/REP blocks the sender instead, that your sender and hub share the same Python environment for OpenCV and NumPy, which the installation notes call out explicitly, and that the version you install matches the Python and dependency versions the project lists as tested.

## FAQ

### What does imageZMQ do and how is it installed?

It is a MIT-licensed Python package that transports OpenCV images between computers using PyZMQ messaging, published on PyPI as `imagezmq`. It requires `pyzmq>=16.0` and `numpy>=1.13`, and the documentation asks you to install OpenCV and imageZMQ into the same Python virtual environment.

### What is the difference between REQ/REP and PUB/SUB in imageZMQ?

In REQ/REP, which is the default, each sender sends an image and waits for a reply from the hub before sending the next one, so the sender is blocked. In PUB/SUB the sender does not wait for any acknowledgement and the hub provides no reply, so frames the hub never receives are lost.

### How does imageZMQ keep track of which camera sent which image?

Each message is a `(text_message, image)` tuple. The text portion identifies the source, and in the Raspberry Pi example it is the hostname obtained with `socket.gethostname()`, which the hub uses as the window name in `cv2.imshow`.

### Can imageZMQ reduce the network load for image streams?

Yes. The feature list includes sending jpeg compressed OpenCV images to lighten network loads, and the examples directory contains `faster_jpg_hub.py` and `faster_jpg_send.py` for that path.

### Why did imageZMQ choose ZeroMQ over MQTT, RabbitMQ or AMQP?

The stated reasons are that ZMQ needs no message broker, so images are not passed first to a broker and then to the hub, which avoids extra processes and double handling of large images; that it is very fast for passing OpenCV images; and that it is easy to install.

## Sources

- [Issues](https://github.com/jeffbass/imagezmq/issues)
- [jeffbass/imagezmq on GitHub](https://github.com/jeffbass/imagezmq)
- [License: MIT](https://github.com/jeffbass/imagezmq/blob/master/LICENSE)
- [README](https://github.com/jeffbass/imagezmq/blob/master/README.md)
- [Releases](https://github.com/jeffbass/imagezmq/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/jeffbass-imagezmq
