Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Builds the person model and publishes it as a release. Run by hand
# (Actions -> release -> Run workflow) and only that way: release assets are
# never uploaded from a workstation or by any check that runs on a pull
# request or a push, so each one traces to the commit and run that built it.
name: release

on:
workflow_dispatch:
inputs:
tag:
description: Release tag to create (e.g. v1). Must not exist yet.
required: true

permissions:
contents: write

concurrency:
group: release
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-22.04
timeout-minutes: 120
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Dependencies
run: |
pip install -q -r requirements.txt
sudo apt-get update -qq
sudo apt-get install -y -qq gcc-arm-linux-gnueabihf qemu-user-static >/dev/null

- uses: actions/cache@v4
with:
path: .cache
key: coco-${{ hashFiles('coco.txt') }}

- name: Build, check and measure
run: bash scripts/build.sh out

- name: Publish
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ inputs.tag }}
run: |
{
echo "Built by run ${{ github.run_id }} from ${{ github.sha }}."
echo
echo "\`person.pdnet\` is the int8 person detector majestic's CPU engine loads;"
echo "\`person.json\`, installed beside it, is what its three object-detection"
echo "presets mean for this model. Both the host engine and its ARMv7 NEON"
echo "build were checked bit for bit against the reference arithmetic before"
echo "anything was measured. Measured on labelled crops of COCO val2017:"
echo
cat out/points.md
} > notes.md
gh release create "$TAG" --target "${{ github.sha }}" --title "$TAG" \
--notes-file notes.md out/person.pdnet out/person.json out/SHA256SUMS out/points.md
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.cache/
out/
__pycache__/
64 changes: 63 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,64 @@
# cpu-models
CPU-run detection models for OpenIPC cameras, built and published reproducibly

Detection models that OpenIPC cameras run on their CPU, with majestic's own
int8 engine, and the release that publishes them. The first is the person
detector majestic's object detection uses on cameras without an NPU.

## The person detector

`models/person/model.onnx` is a statically quantized (QDQ) int8 network:
MobileNetV1-0.25 with an FPN-lite head, 66.7k parameters, a 320x192 YUV
input, and boxes at strides 8, 16 and 32. It was trained for majestic on COCO
2017 person boxes and fine-tuned on hard negatives mined from a lab camera's
own crops; while those mined crops were sorted, an Ultralytics YOLO model was
used to find which ones contained a person.

The camera hands it a crop of the motion region at the scale it was trained
for, in NV21; it reports people with a score from 0 to 100.

## Releases

Published only by the `release` workflow (Actions -> release -> Run workflow,
with a new tag), never by hand. A release carries:

- `person.pdnet`: the model as majestic's engine loads it, exported from the
ONNX by `tools/export_pdnet.py`;
- `person.json`: the score thresholds behind majestic's three presets, for this
model, installed beside it:
- `balanced`: the F1-optimal threshold;
- `fewerFalseAlarms`: the F0.5 optimum;
- `catchEverything`: the highest threshold keeping recall at 0.90, else the
F2 optimum;
- `points.md`: what each preset achieves; and `SHA256SUMS`.

## Building

```sh
pip install -r requirements.txt # Python 3.11
sudo apt-get install gcc-arm-linux-gnueabihf qemu-user-static
bash scripts/build.sh [out]
```

`scripts/build.sh` is the whole build, the same here and in the workflow. It:

1. fetches COCO val2017 and its annotations, hash-checked (`coco.txt`);
2. exports the blob;
3. builds the engine (`engine/pdnet.c`) for the host and for ARMv7 with NEON,
the camera's path;
4. checks both against `tools/pdq.py`, the model's arithmetic written out in
numpy. On 40 crops, every value of all six head maps must be identical,
and the ARM build, run under qemu-arm, must find exactly the host's boxes
on all 3000 crops;
5. measures the presets: `tools/crops.py` cuts 3000 labelled 320x192 crops
from COCO val2017 in the camera's geometry, 2000 around a person and 1000
with nobody in them, and `tools/points.py` scores the engine's detections
on them.

A build of one commit is byte-identical anywhere.

`engine/` is the engine majestic carries; a change to it lands here and there
together.

## License

MIT.
4 changes: 4 additions & 0 deletions coco.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# COCO val2017 and its annotations, for the presets' operating points, pinned
# by hash. file sha256 url
val2017.zip 4f7e2ccb2866ec5041993c9cf2a952bbed69647b115d0f74da7ce8f4bef82f05 http://images.cocodataset.org/zips/val2017.zip
annotations_trainval2017.zip 113a836d90195ee1f884e704da6304dfaaecff1f023f49b6ca93c4aaae470268 http://images.cocodataset.org/annotations/annotations_trainval2017.zip
Loading
Loading