Recipe: retail analytics with occupancy and people counting
Count people across a doorway, measure dwell time in zones, and push it all to an optional GPU edge node that dials home over one outbound connection. The four vision apps, sized honestly.
Why retail analytics
A store owner wants three numbers: how many people came in, how long they lingered where the promotions are, and when it got too crowded. StreamHub answers all three with vision apps that run on your own cameras — no footfall-counting vendor, no per-store subscription. And when one server is not enough cameras, you push the inference to an edge node with a GPU that connects out to your master with zero inbound ports.
Architecture in words
There are four vision Studio Apps, each a Python worker over the app's HLS: occupancy (zone dwell and sessions), counting (line-crossing people counter), crowd (density threshold), and demography (age/gender/emotion). Their per-camera geometry — the zones and lines — is not in the plugin config; it lives per room in a stream_settings table you edit through a polygon/line editor drawn over the live player. Detections emit events (count.in, occupancy.session_started, crowd.threshold, …) and land in each app's own database.
1. Install the apps you need
export BASE="https://your-server.example.com/api/v1"
export TOKEN="sk_...."
curl -X POST $BASE/apps/store/plugins/counting/install -H "Authorization: Bearer $TOKEN"
curl -X POST $BASE/apps/store/plugins/occupancy/install -H "Authorization: Bearer $TOKEN"
curl -X PATCH $BASE/apps/store/plugins/counting -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"enabled": true, "config": {"device": "cpu", "fps": 3.5, "confidence": 0.35}}'
The manifest config for occupancy, counting and crowd is just three fields: device (cpu default, or cuda), fps (3.5), confidence (0.35). Demography adds engine (local InsightFace by default, or openai/deepseek) and an ai_api_key.
2. Draw the line and the zones
Geometry is per room. Set a counting line and an occupancy zone through the settings endpoint (or the visual editor in the app tab):
# a doorway counting line
curl -X PUT $BASE/apps/store/plugins/counting/streams/entrance/settings \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"lines":[{"id":"door","x":0.5,"y":0.5,"length":0.6,"angle":0,"inside":"bottom"}],"capacity":50}'
# a promo-zone dwell polygon
curl -X PUT $BASE/apps/store/plugins/occupancy/streams/promo/settings \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"zones":[{"id":"z1","tag":"promo","detect":"personas","points":[[0.2,0.2],[0.8,0.2],[0.8,0.8],[0.2,0.8]],"alertLimit":15,"alertAfterSec":300}]}'
Points are normalized 0–1; a polygon needs at least three; detect is personas or vehiculos.
3. Read the numbers
Every app exposes an overview, live streams and an event history:
curl -s $BASE/apps/store/plugins/counting/overview -H "Authorization: Bearer $TOKEN"
curl -s "$BASE/apps/store/plugins/counting/events?limit=100" -H "Authorization: Bearer $TOKEN"
Counting emits count.in / count.out / count.occupancy; occupancy emits occupancy.session_started / _ended / alert; crowd emits crowd.count / crowd.threshold.
4. Optional — push inference to a GPU edge node
Install a second box as an edge node (--node-type edge). It holds one outbound WebSocket to the master at wss://your-server.example.com/api/v1/edge/ws, opens no inbound port, pulls the HLS it needs, and POSTs results back. Then assign a stream to it:
curl -s -X POST $BASE/apps/store/edge/assignments -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"pluginId":"occupancy","streamId":"promo","policy":"round_robin"}'
A GPU is optional but transformative: any CUDA card drops inference under ~10 ms/frame.
Sizing, honestly
Cadence dominates. On a measured Tesla T4, occupancy (yolo11m + ByteTrack) runs ~77 FPS per process and the box saturates around ~145 FPS aggregate. That is roughly 36 cameras per T4 at 4 fps sampling (Vision One's cadence), dropping to ~9–10 at 15 fps. VRAM is rarely the limit — the scheduler budgets (vram_free_mb − 500) / 200 streams, about 68 on a 16 GB T4. A CPU-only host handles a handful of cameras at low fps; a single GPU handles a store or two.
What you should see
People crossing the doorway increment count.in; the occupancy overview shows live headcount and dwell sessions per zone; crossing your alertLimit for alertAfterSec raises an occupancy alert. On an edge node, the same events arrive at the master over the outbound connection.
Troubleshooting
- Counts look doubled or jumpy — counting runs at a fixed 3.5 fps on purpose (trackers swap IDs when the frame gap varies); do not raise
fpsexpecting better accuracy. - No detections — confirm the room has a live stream and that you saved geometry for that exact room name.
- Edge node idle — check it registered and heartbeats, and that the plugin is installed on the master with
worker.edgecapability; assignments only target edge-capable apps.
Closing
The vision runtime is open source (AGPL-3.0), so you can run every model on hardware you own. If you need a multi-store rollout, a GPU sizing plan, or dashboards tying footfall to sales, our team does the capacity planning and the edge fleet setup as a consulting engagement.