Counting Bees with Pure OpenCV (No Neural Networks): What Fits on a Raspberry Pi

Zero neural networks. A bee counter that detects, tracks and counts honey bees in and out of a hive entrance, measured at 988 FPS on a desktop x86 CPU — with the target platform a ten-year-old Raspberry Pi 3B (4× Cortex-A53 @ 1.2 GHz, 1 GB RAM). The idea fit into one sentence. The implementation — architecture, code, parameter tuning, evaluation harness, even the scene-configuration proposals — came out of conversations with an AI agent (Gemini 4 Argon).
The idea and the rules of the game
Counting traffic at a hive entrance is a classic beekeeper’s problem. A strong summer colony sends out tens of thousands of foragers per day, and watching the entrance by eye is the traditional way to judge whether a colony is growing, shrinking or preparing to swarm. That doesn’t scale. The idea in one sentence: a camera over the entrance, a cheap board computer, and a live IN / OUT count.
The constraints were fixed before any code was written:
- Target hardware: Raspberry Pi 3B, headless, at least 15 FPS at 640×480. Not a Pi 5, not a GPU box — the kind of board that already lies in a drawer.
- No neural networks. No YOLO, no CNN, no accelerator. The underlying question: is classic computer vision still enough for a well-defined motion problem in 2026?
- No scipy, no filterpy, no lap. On a 1 GB RAM board, slow installs and heavy imports eat real budget. Everything had to run on OpenCV + NumPy alone.
The agent turned that into a five-module pipeline (capture, detector,
tracker, counter, main), one config.yaml per scene, an evaluation
script, and a decision log that records why every parameter has the value
it has.
One honesty note up front: the PoC was built and measured on an x86 machine. Everything Raspberry-Pi-specific in this post is an estimate derived from per-core performance ratios — getting it onto the board itself is the next milestone, and one reason the constraints above exist.
Test footage: two real YouTube videos of hive entrances, prepared with
ffmpeg. Video 1 had a white intro and slow-motion duplicated frames
(cut the first 17 s, sped up 2.5×); video 2 was cut from t=33 s of a
30-minute static shot, so t=0 in the clip matches the source timestamp:
# video 1: remove intro, undo 2.5x slow motion
ffmpeg -i src.mp4 -vf "setpts=0.4*PTS" -r 30 video1/vcely.mp4
# video 2: cut from t=33 s
ffmpeg -ss 33 -i src.mp4 video2/vcely.mp4
A live demo with both annotated videos and the full per-scene configs is on the vcely-poc project page.
The technology decision: classic CV, not deep learning
Three arguments drove the “no neural networks” decision — and they apply to many other “count moving things” problems:
- The hardware budget is tiny. MOG2 background subtraction plus contour analysis on a cropped ROI of ~100 000 pixels costs less than 0.6 ms per frame of pure math on x86. A Pi 3B’s Cortex-A53 core is roughly 8–12× slower single-threaded than a desktop core, which lands the whole pipeline at an estimated 6–10 ms per frame — a 3× headroom against the 33.3 ms budget of 30 FPS. A small YOLO model on the same ARM chip without an NPU simply does not fit that budget.
- The problem is motion, not identity. Nothing in the task requires recognizing “a bee” among 80 generic object classes. “Something moved in front of a static background — how big is it, what shape is it, where is it heading” is exactly what background subtraction was built for.
- Determinism beats black boxes on cheap hardware. Every parameter is a physical knob: thresholds in pixel units, memory in frames, hysteresis in pixels. When a count looks wrong, there is a concrete value to adjust and a logged reason for why it has that value. A neural network on hardware you cannot profile turns failure diagnosis into guesswork.
This is not an argument against deep learning in general — the future-work section below marks the exact spot where an NPU would genuinely help. For this problem, on this hardware, classic CV wins.
One pipeline, four stages

- Capture + ROI. The frame is resized and cropped to the entrance area. Swaying grass outside the ROI stops existing for the detector, and the crop saves over 70 % of the CPU budget compared to processing the full frame.
- Detector. MOG2 adapts to the static scene background; morphological opening removes salt-noise pixels, closing merges a bee’s striped body and translucent wings into one compact blob; the shape filter drops thin compression artifacts that survive the morphology.
- Tracker. Every detection becomes a track with an ID, a velocity estimate and a miss counter. Tracks survive short occlusions, crossed trajectories and bees standing still for half a second.
- Counter. A closed polygon around the entrance, not a line. The signed
distance to the polygon boundary (
cv2.pointPolygonTest) decides IN / OUT with ±6 px hysteresis, and a minimum total displacement per track filters out guard bees hovering in place.
Scene configuration is one config.yaml per video. The agent generates a
“scene proposal” — a frame overlaid with a 50 px grid — from which the ROI
rectangle and counting polygon are read off:
# video1/config.yaml (excerpt)
video:
roi: [140, 10, 360, 290]
detector:
var_threshold: 18.0
detect_shadows: false
learning_rate: 0.0012
counter:
line: [[150, 150], [250, 50], [350, 100], [250, 220]]
hysteresis_px: 6.0
min_displacement_px: 20.0

Tuning on real footage
Development was a normal iterative loop: run the pipeline, watch the annotated output and the binary mask, adjust a config value, run again. Each scene ended up with its own config — video 1 and video 2 differ in thresholds, ROI, tracker memory and even HUD position.
| Change | Reason (observed on real footage) |
|---|---|
detect_shadows: false, var_threshold 28→18–20, learning_rate 0.015→0.0012–0.002 | crawling dark bees on sunlit wood were classified as shadows and vanished from the mask |
| contour shape filters: aspect 0.28–3.5, extent ≥ 0.28 | MPEG compression noise on a sharp wood edge produced thin phantom contours |
damped velocity prediction (0.65^missed), max velocity 18 px/frame, max_missed_frames 15–20 | linear prediction overshot after a missed detection and jumped onto another bee; a bee pausing 0.5 s used to lose its ID |
closed 4-point polygon + pointPolygonTest instead of a line, ±6 px hysteresis, min displacement 20–22 px | bees leave the entrance straight up, around any line; hysteresis ignores hovering guard bees |
| scene-cut cooldown + duplicate-frame detection | intros and FPS-converted footage caused false detection bursts |
One pitfall is genuinely non-obvious and worth a paragraph. OpenCV’s MOG2 marks every pixel that darkens to 0.5–1.0× of the background brightness as shadow (mask value 127). A dark-brown bee body on sunlit wood sits exactly in that range — so with shadow detection enabled and a threshold applied to the mask, the detector deletes precisely the bees we want to count, and only the two fast fliers with glinting wings survive. Turning shadows off, lowering the variance threshold and slowing the background learning rate brought the crawling bees back into the mask:

Two robustness details worth copying into any background-subtraction project: a scene-cut guard (if more than 25 % of the ROI changes at once, or the frame goes white, relearn the background at a high learning rate and clear all tracks instead of emitting false crossings) and duplicate-frame detection (identical consecutive frames — the hallmark of FPS-converted video — return the previous detections, so the tracker doesn’t age its tracks against a frozen frame).
Results on two real scenes
60 seconds of each video, 1801 frames each, all numbers from summary.json:
| Metric | Video 1 — slanted view, dense traffic | Video 2 — frontal wide entrance |
|---|---|---|
| IN | 34 | 44 |
| OUT | 47 | 23 |
| Blue (detected, uncounted) | 249 | 355 |
| Confirmed tracks | 330 | 422 |
| Counted events | 81 | 67 |
| Pipeline speed (x86, incl. video decode + encode) | 988 FPS | 595 FPS |
The blue number deserves an honest explanation. In a bee crowd, 2D counting is inherently ambiguous: bees touch, cross and occlude each other, and many never cross the zone boundary with a clean, attributable trajectory. Instead of hiding that, the tracker keeps every confirmed track that never got counted and reports it live in the HUD as uncounted (blue). It doubles as a quality indicator: when blue far exceeds green + orange, the zone shape or thresholds need adjustment — not the count.


Original footage: video 1 · video 2 (from t=33 s).
Where it hits limits
- Blob merging. Two or three bees touching at a busy entrance become one contour with one ID, then split into a fresh ID. This under-counts in dense traffic — the main reason video 2’s blue number is high.
- Standing bees. A bee static for more than ~1 second gets adapted into the background model and temporarily disappears. Guard bees and ventilating bees at the entrance do exactly that.
- 3D flyovers. A single 2D view cannot tell a bee that crawled into the slot from one that flew 10 cm above the zone. No amount of CPU fixes geometry.
What we’d improve next
Cheapest first:
- Software (free). A Kalman filter with Hungarian assignment (SORT-style) instead of greedy nearest-neighbour matching, plus local optical flow for crossing trajectories. And a 60–90 FPS camera: fast arrivals currently smear 30–40 px between frames at 30 FPS; doubling the frame rate halves the displacement and removes most ID swaps at crossings.
- An NPU. A Hailo-8L, Google Coral or the RPi 5 AI Kit with a tiny YOLOv8n/YOLOv11n fine-tuned on bees would end the blob-merging problem (each bee detected separately even in a pile), see standing bees, shrug off shadow false-negatives — and open the door to classification: pollen baskets, drones, hornet alarms.
- Mechanics (best ROI). The strongest upgrade is not computational. Mount the camera looking straight down into a low transparent entrance tunnel (~12 mm high), where bees must walk in a single layer and cannot fly over each other. Under those conditions even this OpenCV pipeline on a Pi Zero 2 W should exceed 95 % accuracy.
Final thoughts
Classic computer vision is far from dead. For a well-defined motion problem on tiny hardware it beats deep learning on cost, latency and explainability — every wrong count traces back to a named, numbered, adjustable parameter. And the distance from a one-sentence idea to a measured, annotated, logged proof-of-concept has collapsed into a series of conversations with an AI agent. Next step: the tunnel geometry, real ground truth on video, and a Pi Zero 2 W on an actual hive.
Links:
- Live demo, configs and per-scene metrics: vcely-poc project page
- Source footage: 1 Hr Slow Motion Honey Bees · Over 30 Minutes of Bees Flying from Their Hive