Contents — 27 sections
Project Overview
A camera that counts how many people are waiting in line and turns it into live staffing signals — open another till before the queue gets ugly.
Long checkout queues are one of the biggest drivers of customer frustration and walked-away sales, and stores usually react to them too late — a supervisor notices a queue building, then scrambles to open another till by which point customers are already annoyed or gone. The missing ingredient is a live, objective measure of how many people are waiting. This project builds one: a camera that watches the queue area, counts the people in line with computer vision, and turns that count into a real-time staffing signal — open another checkout before the queue gets long, not after.
The core is people detection and counting in a defined region. A camera views the queue zone; a computer-vision model detects people in the frame, and the system counts how many are within the marked queue area, tracking the number over time. From the live count (and its trend) it derives the signals a manager actually needs: current queue length, whether it is growing, and when it crosses thresholds that should trigger opening or closing a till. Aggregated, it also yields analytics — queue lengths by time of day, peak periods, average wait — that inform staffing schedules.
The value is replacing gut-feel, too-late queue management with continuous, objective data: tills open in time, waits shorten, and staffing is planned from real patterns. It is honest about doing this responsibly — the goal is counting, not identifying anyone, so it should run on-device, count rather than recognise faces, and respect privacy and notice requirements — and about the vision limits (crowding, occlusion, defining where the queue is). But as an on-device people-counting queue monitor that produces live staffing signals and queue analytics, it gives retail an objective handle on one of its most visible customer-experience problems.
What this project does
- Counts people waiting in a defined queue area with vision
- Tracks live queue length and its trend over time
- Signals when to open or close a checkout
- Produces queue analytics (peaks, by time of day, waits)
- Runs on-device — counting, not identifying people
- Replaces too-late, gut-feel queue management with data
- Shortens waits and reduces walked-away sales
Real-World Applications
| Setting | How it is used |
|---|---|
| Checkout staffing | Live signals to open/close tills before queues build. |
| Queue analytics | Peak times and average waits to plan staffing. |
| Service counters | Any queue — banks, pharmacies, help desks. |
| Customer-experience KPIs | Objective wait/queue metrics for operations. |
Deployment contexts where a build of this kind earns its keep.
Features & Capabilities
- Computer-vision people detection + counting
- Region-of-interest queue counting
- Live count, trend and threshold signals
- Open/close-till staffing recommendations
- Queue analytics for scheduling
- On-device, privacy-respecting design
- Honest about crowding/occlusion and privacy
Difficulty, Time & Required Skills
| Attribute | Value |
|---|---|
| Difficulty level | Advanced |
| Estimated completion time | 14–22 hours |
| Indicative build cost | ₹6,000 – ₹10,000 |
| Primary discipline | Retail |
| Reference platform | Raspberry Pi 4 Model B (4 GB) |
Skills you should have (or will pick up)
- People detection with a CV model (e.g. lightweight detector)
- Region-of-interest counting and temporal tracking
- Threshold/trend logic for staffing signals
- On-device, privacy-respecting deployment
- Queue analytics aggregation
Bill of Materials
Every part below is commonly available from Indian and international hobby-electronics suppliers. Prices are indicative 2026 retail figures in Indian rupees and will drift — treat them as a budgeting guide, not a quotation.
| Component | Key specification | Qty | Approx. cost |
|---|---|---|---|
| Raspberry Pi 4 Model B (4 GB) Use an official 5 V 3 A supply — brown-outs from phone chargers corrupt SD cards. | Quad-core Cortex-A72 @ 1.8 GHz, 4 GB LPDDR4, Gigabit Ethernet, Wi-Fi 5, BT 5.0, 2× USB 3.0, 40-pin GPIO | 1 | ₹5,800 |
| Raspberry Pi Camera Module 3 Pi 5 uses a narrower 22-pin CSI cable — the old 15-pin ribbon will not fit. | 12 MP IMX708, autofocus, HDR, 1080p50, CSI-2 ribbon | 1 | ₹2,600 |
| 0.96″ SSD1306 OLED display Static images burn in — invert or scroll the screen periodically. | 128 × 64 monochrome, 1.3–3.3 V logic, 100 kHz–400 kHz I²C | 1 | ₹250 |
| Camera + edge compute On-device inference for privacy | Raspberry Pi (or Jetson) + camera viewing the queue area | 1 | ₹5,000 |
| Mount | Overhead/angled mount covering the queue zone | 1 | ₹400 |
| Staff signal | Light/screen or dashboard for open-till signals | 1 | ₹500 |
| Network | Wi-Fi/Ethernet for signals/analytics (not video) | 1 | ₹200 |
Estimated total: ₹14,750, excluding tools, shipping and consumables.
Tools and consumables
- Soldering iron (temperature controlled, 350 °C) with 0.8 mm 60/40 or lead-free solder
- Digital multimeter — continuity, DC volts and current ranges
- Wire strippers, flush cutters and a small set of precision screwdrivers
- Heat-shrink tubing and a heat gun (or a lighter, carefully)
- A laptop with a USB port and the toolchain listed above
Hardware Specifications
| Part | Specification | Supply | Interface | Reference |
|---|---|---|---|---|
| Raspberry Pi 4 Model B (4 GB) | Quad-core Cortex-A72 @ 1.8 GHz, 4 GB LPDDR4, Gigabit Ethernet, Wi-Fi 5, BT 5.0, 2× USB 3.0, 40-pin GPIO | 5 V / 3 A USB-C | GPIO, SPI, I²C, UART, CSI, DSI | Datasheet |
| Raspberry Pi Camera Module 3 | 12 MP IMX708, autofocus, HDR, 1080p50, CSI-2 ribbon | 3.3 V via CSI | CSI-2 | Datasheet |
| 0.96″ SSD1306 OLED display | 128 × 64 monochrome, 1.3–3.3 V logic, 100 kHz–400 kHz I²C | 3.3–5 V | I²C (0x3C) | Datasheet |
Consolidated electrical and interface specifications for every active part in the build.
Power Budget & Supply Sizing
Add up the typical active current of every part, then size the supply with at least 50 % headroom so transmit bursts and motor inrush never brown out the controller.
| Load | Supply rail | Typical current (mA) | Notes |
|---|---|---|---|
| Raspberry Pi 4 Model B (4 GB) | 5 V / 3 A USB-C | 1200 | Use an official 5 V 3 A supply — brown-outs from phone chargers corrupt SD cards. |
| Raspberry Pi Camera Module 3 | 3.3 V via CSI | 250 | Pi 5 uses a narrower 22-pin CSI cable — the old 15-pin ribbon will not fit. |
| 0.96″ SSD1306 OLED display | 3.3–5 V | 20 | Static images burn in — invert or scroll the screen periodically. |
Summed typical draw is 1470 mA. With a 1.5× design margin the supply should deliver at least 2300 mA continuously at the stated rail voltage.
Software Requirements & Development Environment
Reference toolchain: Raspberry Pi OS Bookworm (64-bit) + Python 3.11 + VS Code Remote-SSH. Anything newer normally works; anything older may lack the board definitions used here.
- Flash Raspberry Pi OS (64-bit) with Raspberry Pi Imager; pre-configure Wi-Fi, hostname and SSH in the Imager settings so the board comes up headless.
- Update first:
sudo apt update && sudo apt full-upgrade -y, then reboot. - Work inside a virtual environment —
python3 -m venv ~/venv && source ~/venv/bin/activate. Bookworm blocks system-widepip installby design. - Enable the buses you need with
sudo raspi-config→ Interface Options (I²C, SPI, Serial, Camera). - Develop over VS Code Remote-SSH so you edit on your laptop but run on the Pi.
Required libraries
| Library | Why it is needed | Install |
|---|---|---|
| Python 3.11+ | Runtime for the analysis, training and service code. | sudo apt install python3 python3-venv python3-pip |
| OpenCV 4.10+ | Frame capture, colour conversion, drawing and classical CV operators. | pip install opencv-python |
| TensorFlow / Keras 2.17+ | High-level model building and the TFLite converter. | pip install tensorflow |
| Ultralytics YOLO 8.3+ | Training and inference API for YOLOv8/v11 detectors. | pip install ultralytics |
| Picamera2 0.3.20+ | libcamera-based capture API for Pi Camera modules. | sudo apt install python3-picamera2 |
| Flask 3.0+ | Minimal HTTP API and dashboard server. | pip install flask |
Block Diagram
The block diagram shows the functional decomposition of the system — what senses, what decides, what acts, and where the data ends up.
Circuit Diagram & Wiring
Every signal line in the build is shown below, followed by a pin-by-pin connection table you can work through with a multimeter in hand.
| Peripheral | Peripheral pin | Controller pin | Signal |
|---|---|---|---|
| Camera | CSI/USB | — | Queue-area video (on-device) |
| Staff signal | GPIO/HDMI | — | Open/close till |
| Network | Wi-Fi/Eth | — | Counts/analytics (not video) |
| Local display | I2C | GPIO | Live count |
Wire one row at a time and tick it off — most "it does not work" reports trace back to a single swapped pair.
Wiring explanation
- Mount the camera overhead or angled to see the queue area clearly, minimising occlusion.
- Run inference on-device so raw video never leaves the unit — only counts/signals do.
- Drive a staff light/screen or dashboard with the open/close-till signal.
- Network carries counts and analytics, not video, for privacy and bandwidth.
- Define the queue region-of-interest in the camera view during setup.
System Architecture
Read the stack from the bottom up: physical hardware, the firmware that drives it, the transport that moves data off the device, and the software a human actually looks at.
Working Principle
Queue management fails for a simple reason: stores manage queues by human noticing, which is inconsistent and always a step behind. By the time a supervisor sees a queue has grown and reacts, customers have already been waiting too long, and some have abandoned their baskets. The fix is not more vigilance but a continuous, objective measurement of queue length that the store can act on the moment it starts to build — converting queue management from a reactive judgement call into a data-driven signal.
Producing that measurement is a computer-vision counting problem. A camera views the queue area, and a people-detection model finds the people in each frame. The key refinement is counting within a defined region of interest — the marked queue zone — so passers-by and shoppers elsewhere are not counted, only those actually waiting. Counting over successive frames (with light temporal smoothing/tracking) gives a stable live count and, importantly, its trend: a queue of five that is growing needs a different response from a queue of five that is clearing.
From the count and trend come the staffing signals that are the point of the system. Simple thresholds turn the number into action: above an open-till threshold (or when the trend shows a queue building toward it), signal staff to open another checkout before the wait becomes painful; below a close-till threshold, signal that a till can close, saving labour. Because the signal is early and objective, tills open in time and waits stay short. Aggregated over time, the same counts become analytics — queue length by hour and day, peak periods, estimated average waits — which let managers schedule staffing to demand rather than merely react to it.
What makes this system acceptable to deploy is doing it responsibly, and the design is explicit about it. The goal is strictly counting, not identification: the system needs to know how many people are waiting, never who they are. So it runs on-device (raw video never leaves the unit — only counts and signals do), it counts people rather than recognising faces, and it respects privacy expectations and any notice/consent requirements for cameras in the space. It is also honest about the vision limits: crowding and occlusion (people hidden behind others) make dense queues hard to count exactly, defining where "the queue" is can be ambiguous, and lighting and camera angle matter. Within those honest bounds — an approximate but continuous and objective count, kept private by design — it gives retail exactly what queue management has always lacked: a live, trustworthy handle on how many people are waiting, early enough to do something about it.
The maths behind it
Queue count in a region
For each frame: detect people {boxes}.
queue_count = | { p in people : center(p) ∈ ROI_queue } |
Count only inside the marked queue zone — ignore passers-by.
Smooth over frames to steady the number.
Trend (is it building?)
Track count over a short window:
trend = d(count)/dt (rising / falling)
A rising queue near the threshold warrants opening a till
BEFORE it crosses — act early, not late.
Staffing signal
if count ≥ OPEN_TH (or rising toward it): OPEN a till
if count ≤ CLOSE_TH for a while: CLOSE a till
Estimated wait ≈ count / service_rate
Set thresholds from acceptable wait times.
Program Flowchart
The firmware is a single cooperative loop. Nothing blocks for long, so networking, sensing and the user interface all stay responsive.
Assembly Instructions
Build on a breadboard first and only commit to solder once the whole system has run for an hour without a fault.
Set up on-device vision over the queue area
Mount the camera to see the queue clearly, run the people detector on-device, and define the queue region of interest.
Count in the region and track the trend
Count only people inside the queue ROI, smooth over frames, and track the trend so you can act before thresholds are crossed.
Produce signals and analytics
Turn count/trend into open/close-till signals with sensible thresholds, and log counts for queue analytics and staffing schedules.
Step-by-Step Implementation Guide
Work through these in order. Each step ends in something you can observe, so a failure is always localised to the step you just finished.
Count people in the queue region
Detect people on-device and count only those inside the queue ROI, smoothing for a stable live count.
pythoncount.pyimport numpy as np def queue_count(detector, frame, roi, history): people = detector.detect(frame, classes=["person"]) # on-device only n = sum(1 for p in people if point_in_poly(center(p), roi)) # in the queue history.append(n); del history[:-10] # keep last 10 return int(round(np.median(history))) # smoothed live countpeople = detector.detect(frame, classes=["person"]) # on-device onlyPeople are detected on the device itself, so raw video never leaves the unit — counting, not identification.n = sum(1 for p in people if point_in_poly(center(p), roi)) # in the queueOnly people inside the defined queue region are counted, so passers-by and other shoppers are excluded.return int(round(np.median(history))) # smoothed live countMedian smoothing over recent frames gives a steady count instead of a number that flickers frame to frame.Signal staffing and log analytics
Turn count and trend into open/close-till signals early, and log counts for peak-time analytics and staffing schedules.
Complete Source Code
The listing below is complete and compiles as written — there are no elided sections. Read the annotations under each block before you upload it.
#!/usr/bin/env python3
"""
Queue Length Counter — on-device people counting for staffing
Counts people within a defined queue region from a camera, tracks the
live count and trend, and signals when to open/close a checkout BEFORE
waits get long. Logs counts for queue analytics. Counts, never
identifies: inference on-device; only counts/signals leave the unit.
"""
import time, numpy as np
class QueueCounter:
def __init__(self, detector, roi, open_th=4, close_th=1, sink=None):
self.det = detector; self.roi = roi
self.open_th, self.close_th = open_th, close_th
self.hist = []; self.sink = sink
def count(self, frame):
people = self.det.detect(frame, classes=["person"]) # on-device
n = sum(1 for p in people if point_in_poly(center(p), self.roi))
self.hist.append(n); self.hist = self.hist[-10:]
return int(round(np.median(self.hist))) # smoothed
def trend(self):
return self.hist[-1] - self.hist[0] if len(self.hist) > 1 else 0
def step(self, frame):
c = self.count(frame); t = self.trend()
if c >= self.open_th or (c >= self.open_th-1 and t > 0):
sig = "OPEN_TILL" # early: before waits build
elif c <= self.close_th:
sig = "CAN_CLOSE_TILL"
else:
sig = "HOLD"
# ONLY the count + signal leave the device — never video or identities
if self.sink:
self.sink.publish("queue/status",
{"count": c, "trend": t, "signal": sig, "ts": time.time()})
return c, sig
def run(self, camera):
for frame in camera.frames(): # raw frames stay on-device
self.step(frame)
time.sleep(0.5) # counting needs only a few FPS
if __name__ == "__main__":
QueueCounter(PersonDetector(), QUEUE_ROI, sink=Dashboard()).run(Camera())
Configuration & Calibration
Configuration steps
- Configure the camera, on-device detector and the queue region of interest.
- Configure smoothing, open/close-till thresholds and trend sensitivity.
- Configure the staff signal and analytics logging (counts only).
- Configure privacy: on-device inference, no video egress, notice/consent.
Calibration procedure
An uncalibrated sensor produces confident, precise, wrong numbers. Do this once per physical unit and record the constants.
ROI + counting
Define the queue region and verify the count matches a manual count at several lengths.
Thresholds
Set open/close thresholds from acceptable wait times and the service rate.
Robustness
Check counting under crowding/occlusion and different lighting; accept approximate counts when dense.
Dataset, Model & Training
| Dataset | Size | Licence | Use here |
|---|---|---|---|
| Person-detection data (e.g. COCO person class) | Large | Varies (check terms) | Base people detector |
| Site queue-area frames (ROI-annotated) | Small–Medium | On-site | Tune ROI / thresholds |
| Crowd/occlusion samples | Medium | Varies | Robustness in dense queues |
| Time-stamped count logs | Growing | On-site | Queue analytics / scheduling |
| Layer / stage | Shape or configuration | Purpose |
|---|---|---|
| Detector | lightweight person detector (YOLO-class / MobileNet-SSD) | Runs on-device (Pi/Jetson) |
| ROI filter | count detections inside the queue region | Ignores passers-by |
| Tracker/smoother | temporal smoothing / light tracking | Stable count + trend |
| Signal logic | thresholds + trend on the count | Open/close-till signals |
| Privacy | on-device; counts out, video stays | Count, don't identify |
Hyperparameters
| Hyperparameter | Value | Why |
|---|---|---|
| Detector input size | ≈ 416–640 px | Speed vs small-person recall |
| Confidence threshold | ≈ 0.4–0.5 | Miss vs false people |
| Smoothing window | ≈ 3–10 s | Steady count vs responsiveness |
| Open/close thresholds | from wait targets | Site-specific |
Evaluation, Metrics & Deployment
| Metric | Value | What it tells you |
|---|---|---|
| Counting error (MAE) | Low at typical lengths | Rises with crowding/occlusion |
| Signal timeliness | Fires before waits get long | The operational goal |
| Privacy | No identification; video on-device | By design |
| FPS on-device | Real-time enough (few FPS) | Counting needs modest rate |
Figures from the reference training run described above — reproduce them before trusting your own changes.
Inference example
import numpy as np
class QueueCounter:
def __init__(self, detector, roi_polygon, open_th=4, close_th=1):
self.det = detector; self.roi = roi_polygon
self.open_th, self.close_th = open_th, close_th
self.history = [] # recent counts (smoothing/trend)
def count_frame(self, frame):
people = self.det.detect(frame, classes=["person"]) # on-device
# count only people whose center is inside the queue region
n = sum(1 for p in people if point_in_poly(center(p), self.roi))
self.history.append(n); self.history = self.history[-10:]
return int(round(np.median(self.history))) # smoothed
def signal(self, frame):
count = self.count_frame(frame)
trend = self.history[-1] - self.history[0] if len(self.history) > 1 else 0
if count >= self.open_th or (count >= self.open_th-1 and trend > 0):
return count, "OPEN_TILL" # act BEFORE it gets long
if count <= self.close_th:
return count, "CAN_CLOSE_TILL"
return count, "HOLD"
# NOTE: only 'count' + signal leave the device — never the video/faces.
Testing Procedure & Expected Output
Test from the bottom up. Confirm power, then each sensor in isolation, then the integrated loop — the first failing step tells you exactly where to look.
| Test | What you should see |
|---|---|
| Short queue forms | Accurate live count |
| Queue builds toward threshold | OPEN_TILL signal fires early (trend) |
| Queue clears | CAN_CLOSE_TILL signal |
| People pass by outside the ROI | Not counted |
| Dense, occluded queue | Approximate count — note the limit |
| Check data egress | Only counts/signals leave; no video/identities |
Bench-test checklist. If a row fails, stop and fix it before moving on.
Expected output
Live queue length, early open/close-till signals, and queue analytics — with only counts leaving the device.
{
"count": 5,
"trend": 2,
"signal": "OPEN_TILL",
"est_wait_min": 6,
"ts": "2026-07-28T17:12:00"
}
Five people and growing (+2), so the system signalled OPEN_TILL with an estimated 6-minute wait — the till is staffed before the queue becomes a problem; no video or identities ever left the device.
Troubleshooting: Common Errors & Fixes
Performance Optimisation
- Count within the queue ROI and smooth for a steady live number.
- Act on the trend so tills open before waits build.
- Run on-device at a few FPS — counting does not need high frame rates.
- Publish counts/signals only; keep video on the device.
- Pin the hot loop to one core with
tasksetand leave the others free for the OS. - Prefer MJPEG over raw YUY2 when capturing from USB cameras — the decode cost is far lower than the USB bandwidth cost.
- Log to a tmpfs RAM disk and flush to the SD card once a minute; per-sample SD writes are what kills cards.
- Run the service under
systemdwithRestart=alwaysso a crash never means a dead deployment. - Profile before optimising — print
micros()deltas around each stage and fix the slowest one first.
Safety Precautions
- Count, do not identify — run on-device, publish only counts/signals, keep video local.
- Respect privacy and notice/consent rules for cameras in the space.
- Do not repurpose the feed for surveillance/recognition — that breaks the design's premise.
- Treat counts as operational metrics, not judgements about individuals.
- Wear eye protection when soldering or cutting, and solder in a ventilated space — rosin flux fumes are a respiratory irritant.
- Power the circuit through a bench supply with a current limit while you are testing. A 300 mA limit turns a wiring mistake into a beep instead of a dead board.
- Disconnect power before changing any wiring. Hot-plugging a sensor onto a live bus is the fastest way to lose a controller.
Maintenance
- Re-verify the ROI and counting accuracy after camera/layout changes.
- Re-tune thresholds as service rates/targets change.
- Keep the model and lighting adequate for reliable detection.
- Audit that only counts/signals leave the device.
- Re-check every screw terminal and header after the first week — thermal cycling loosens connections that felt tight on day one.
- Recalibrate at the interval given in the calibration section, and keep the constants in a text file next to the firmware — not only in flash.
- Keep a short logbook of firmware versions and what changed. Six months later you will not remember why that constant is 1.083.
Future Improvements & Upgrades
A working v1 is a platform, not a finish line. These are the upgrades that add the most capability for the least rework.
- Estimate waits from count + measured service rate.
- Add multi-queue / whole-store coverage and load balancing.
- Add heat-map analytics for layout optimisation.
- Integrate with workforce scheduling from historical peaks.
- Design a proper PCB. Once the breadboard version has run for a month, moving to a two-layer board removes the intermittent-contact failures that dominate prototype faults.
- Add connectivity — an ESP32 and an MQTT publish turn a local gadget into something you can graph, alert on and analyse over months.
- Add persistent local storage (microSD or the on-chip flash) so a network outage does not create a hole in your data.
- Move configuration out of the source: a captive-portal setup page or a JSON config file makes the build reusable without a recompile.
- Add a battery and solar option so the unit survives a power cut and can be sited away from a socket.
- Write a small test harness that feeds synthetic sensor values through the decision logic, so you can validate thresholds without physically triggering the event.
Frequently Asked Questions
References & Learning Resources
These are the primary sources worth reading in full. Manufacturer datasheets always outrank forum posts when the two disagree.
- Object detection (people)Reference
- Crowd countingReference
- Queueing theory / waiting linesReference
- Edge AI / on-device inferenceReference
- Privacy by designReference