Skip to content

The person detector, its int8 engine, and a release that measures its presets - #2

Merged
widgetii merged 2 commits into
masterfrom
person
Oct 11, 2026
Merged

widgetii merged 2 commits into
masterfrom
person

Conversation

@widgetii

Copy link
Copy Markdown
Member

The CPU person detector majestic uses on cameras without an NPU, and the engine that runs it, replacing majestic's TVM build. The model is models/person/model.onnx: QDQ int8, MobileNetV1-0.25 with an FPN-lite head, 66.7k parameters.

Engine (engine/pdnet.c)

C with NEON. It reads the 320×192 NV21 crop directly, keeps int8 activations with per-channel weights, and requantizes with a single rounding. The detection heads go straight from their int32 accumulators to float. The int8 graph's own output range caps the stride-8 confidence at a score of 0.5, in steps of about one logit, and taking the accumulators avoids that.

Hi3516EV300, Cortex-A7, majestic stopped, 200 crops × 3 p50 p90
TVM build (current) 64.7 ms 64.8 ms
this engine 37.2 ms 37.2 ms

Same model and crops, compared on 3000 labelled COCO crops:

  • F1: 0.417 against TVM's 0.418. The paired-bootstrap difference is −0.0017, with a 95% interval of [−0.0074, +0.0043]: a tie.
  • Precision/recall curve: area 0.345 against 0.337.
  • Recall at precision 0.90: 0.052 against 0.025.
  • False alarms per person-free crop: 0.50 against 0.61.

Release (scripts/build.sh, run by release.yml by hand only)

  1. Fetch COCO val2017 and its annotations, hash-checked.
  2. Export person.pdnet (84 KB).
  3. Check the host and ARMv7 NEON builds against the numpy reference (tools/pdq.py) on 40 crops: all six head maps, every value identical. The ARM build, run under qemu-arm for correctness only, must also find exactly the host's boxes on all 3000 crops.
  4. Measure the presets on 3000 labelled crops cut from COCO val2017 in the camera's geometry.

It publishes person.pdnet, person.json (thresholds per preset), points.md and SHA256SUMS. Two local runs produced byte-identical outputs.

Preset rule: balanced is the F1 optimum; fewerFalseAlarms is the F0.5 optimum, per the decision to use F0.5 for all models; catchEverything is recall ≥ 0.90, else the F2 optimum.

preset threshold precision recall false alarms per person-free crop
fewerFalseAlarms 0.45 0.611 0.277 0.242
balanced 0.28 0.473 0.372 0.536
catchEverything 0.10 0.275 0.496 1.563

… presets

models/person/model.onnx: the CPU person detector majestic uses on cameras
without an NPU. QDQ int8, MobileNetV1-0.25 with an FPN-lite head, 66.7k
parameters, 320x192 YUV in.

engine/: majestic's int8 engine for it, in C with NEON. It runs the model
from an NV21 crop, matches the reference arithmetic (tools/pdq.py) bit for
bit, and takes the detection heads to float from their accumulators. On a
Hi3516EV300 it takes 37.2 ms per crop, against 64.7 ms for the TVM build it
replaces.

scripts/build.sh, which the release workflow runs:
- exports the blob;
- checks the host and ARM NEON builds against the reference on 40 crops,
  every value identical, and that the ARM build finds the host's boxes on
  all 3000;
- measures the presets on 3000 labelled COCO val2017 crops: F1-optimal for
  balanced, the F0.5 optimum for fewerFalseAlarms, recall 0.90 or the F2
  optimum for catchEverything.
Two local runs gave byte-identical outputs.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add the person detector, NEON engine, and measured release pipeline

✨ Enhancement 🧪 Tests ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add a quantized person detector and faster CPU engine for cameras without an NPU.
• Gate releases on exact host and ARM results against the reference model.
• Measure three detection presets on reproducible, labelled COCO crops before publication.
Diagram

graph TD
  Model["ONNX model"] --> Export["Blob exporter"] --> Blob["PDNET blob"] --> Engine["NEON engine"] --> Exact{"Exact match?"} --> Scoring["Preset scoring"] --> Release["Manual release"]
  Crops["COCO crops"] --> Engine
  Crops --> Scoring
  Model --> Exact
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Retain and optimize the TVM build
  • ➕ Avoids maintaining a model-specific C inference engine and binary format.
  • ➖ The supplied camera measurements put TVM at 64.7 ms versus 37.2 ms for this engine.
  • ➖ It would not by itself address the stated quantized detection-head score cap.

Recommendation: The dedicated engine is justified by the supplied on-camera latency and exactness checks. Prefer this approach over retaining TVM, while treating synchronization with majestic and the release-time cross-platform checks as ongoing maintenance requirements.

Files changed (16) +1721 / -1

Enhancement (5) +1104 / -0
pdnet.cImplement the quantized CPU detection engine +836/-0

Implement the quantized CPU detection engine

• Loads the exported graph, converts NV21 input to int8, executes convolutions and FPN operations with scalar or ARM NEON kernels, and decodes scored boxes with NMS. Final heads convert int32 accumulators directly to float rather than passing through the graph's clipped int8 outputs.

engine/pdnet.c

pdnet.hExpose the model-loading and detection API +34/-0

Expose the model-loading and detection API

• Defines the opaque engine handle, scored box type, NV21 detection entry point, model dimensions, and raw-head access for exactness checks.

engine/pdnet.h

model.onnxAdd the quantized person-detection model +0/-0

Add the quantized person-detection model

• Adds the QDQ int8 ONNX model exported into the engine's release blob. The binary model's internal changes are not visible in the text diff.

models/person/model.onnx

export_pdnet.pyExport QDQ ONNX graphs to the engine blob +134/-0

Export QDQ ONNX graphs to the engine blob

• Serializes tensor shapes, supported operations, quantization constants, weights, and detection-head metadata into the versioned PDNET format.

tools/export_pdnet.py

points.pyMeasure and select detection preset thresholds +100/-0

Measure and select detection preset thresholds

• Matches detections to labelled people and sweeps score thresholds for precision, recall, F1, and background false alarms. Writes F0.5-, F1-, and recall-target-or-F2-based presets to JSON and a Markdown results table.

tools/points.py

Tests (1) +46 / -0
check_exact.pyRequire exact engine and reference head outputs +46/-0

Require exact engine and reference head outputs

• Reconstructs model inputs from NV21 crops and compares every float32 value in all six dumped head maps with the NumPy reference. Any mismatch fails the build.

tools/check_exact.py

Documentation (1) +63 / -1
README.mdDocument the detector and release contract +63/-1

Document the detector and release contract

• Describes the model, camera input, preset definitions, release contents, build steps, validation gates, and engine ownership.

README.md

Other (9) +508 / -0
release.ymlPublish checked assets through a manual release +63/-0

Publish checked assets through a manual release

• Adds a dispatch-only workflow that installs build dependencies, runs the shared build, and publishes the model, presets, measurements, and hashes under a new tag. Release notes identify the source commit and workflow run.

.github/workflows/release.yml

coco.txtPin COCO evaluation downloads +4/-0

Pin COCO evaluation downloads

• Records URLs and SHA-256 hashes for COCO val2017 images and annotations used to derive preset operating points.

coco.txt

requirements.txtPin model-build Python dependencies +3/-0

Pin model-build Python dependencies

• Pins NumPy, ONNX, and headless OpenCV for export, reference execution, crop generation, and scoring.

requirements.txt

build.shOrchestrate export, exactness checks, and preset measurement +57/-0

Orchestrate export, exactness checks, and preset measurement

• Fetches evaluation inputs, exports the blob, builds host and ARM tools, checks six head maps against the reference, and compares host–ARM detections across the crop set. It then writes measured presets and asset hashes.

scripts/build.sh

fetch.shFetch and verify pinned evaluation inputs +22/-0

Fetch and verify pinned evaluation inputs

• Reuses hash-valid downloads and replaces invalid cached files only after a new download passes SHA-256 verification.

scripts/fetch.sh

crops.pyGenerate deterministic labelled camera-style crops +104/-0

Generate deterministic labelled camera-style crops

• Produces 320×192 NV21 crops around COCO people and from person-free images, with crop-space boxes and ignore flags. A fixed seed makes the evaluation set repeatable.

tools/crops.py

dets.cCollect detections for host and ARM comparisons +66/-0

Collect detections for host and ARM comparisons

• Runs the engine over sorted NV21 crops at a low score floor and prints boxes for cross-platform comparison and threshold sweeps.

tools/dets.c

dump_heads.cDump raw detection heads for exactness checks +28/-0

Dump raw detection heads for exactness checks

• Runs each supplied crop through the engine and writes its six float32 head maps for comparison with the reference.

tools/dump_heads.c

pdq.pyDefine reference integer inference arithmetic +161/-0

Define reference integer inference arithmetic

• Interprets the ONNX QDQ graph with integer accumulators, fixed-point requantization, resize, and add operations. Supports direct-to-float detection heads as the reference for C-engine checks.

tools/pdq.py

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Reused builds can publish unpinned presets ✓ Resolved
Description
build.sh checks the downloaded archive hashes but skips extraction when the COCO image directory
or annotation file already exists. A reused output directory containing changed extracted data feeds
those files into crop generation and preset measurement, even though the pinned archives passed
verification.
Code

scripts/build.sh[R25-27]

+[ -d "$coco/val2017" ] || unzip -qo -d "$coco" "$cache/val2017.zip"
+[ -f "$coco/annotations/instances_val2017.json" ] ||
+	unzip -qo -d "$coco" "$cache/annotations_trainval2017.zip" annotations/instances_val2017.json
Evidence
Archive verification does not cover files already extracted into the output directory; crop
generation reads those extracted files, and the measured thresholds are written to the published
preset file.

scripts/fetch.sh[10-21]
scripts/build.sh[22-27]
tools/crops.py[37-42]
tools/crops.py[85-86]
tools/points.py[76-87]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Existing extracted COCO files are trusted based only on their presence, allowing a reused build directory to measure presets from data other than the hash-pinned archives.
## Fix Focus Areas
- scripts/build.sh[22-27]
## Recommended Fix
Recreate the extracted image and annotation trees from the verified archives on each build, removing old contents first, or verify the extracted contents against a pinned manifest before using them.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Old crops enter later release checks ✓ Resolved
Description
crops.py creates the crop directory without clearing existing files, while build.sh samples and
runs detection over every crop file there. When an output directory is reused, crops left by an
earlier build enter the exactness and host-versus-ARM checks despite not belonging to the current
labels; detections for their names are silently omitted from preset measurement.
Code

tools/crops.py[43]

+    os.makedirs(f"{out}/crops", exist_ok=True)
Evidence
Crop generation leaves old files in place, both release checks enumerate the entire directory, and
scoring iterates current label names rather than rejecting unmatched detection names.

tools/crops.py[43-53]
scripts/build.sh[40-53]
tools/dets.c[34-43]
tools/points.py[37-48]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A reused evaluation directory retains crop files from earlier builds, so release checks run over files outside the current labelled set.
## Fix Focus Areas
- tools/crops.py[34-44]
- scripts/build.sh[40-53]
## Recommended Fix
Remove and recreate the crop directory before generating the current labelled set, then have the build check that the generated crop names match the labels.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Presets cannot select a score of 100 🐞 Bug ≡ Correctness
Description
points.py sweeps np.arange(1, 100), which excludes the valid integer threshold 100. If score-100
boxes provide the best F-score or satisfy the recall target, the chosen preset remains at 99 and
includes score-99 boxes that threshold 100 would exclude.
Code

tools/points.py[68]

+    ts = np.arange(1, 100)
Evidence
The engine produces integer scores in the documented 0–100 range, but the array used to select and
report every preset stops at 99.

engine/pdnet.h[15-18]
engine/pdnet.c[779-783]
tools/points.py[68-82]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Preset optimization omits threshold 100 even though the engine emits scores through 100.
## Fix Focus Areas
- tools/points.py[68-82]
## Recommended Fix
Sweep integer thresholds 1 through 100 inclusive and use the resulting full range for every preset objective and reported operating point.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can tweak Display settings with a live preview to see your comment before it ships

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread scripts/build.sh Outdated
Comment thread tools/crops.py
Comment thread tools/points.py
From review: a reused output directory let previously extracted files, or
crops from another run, into the checks and the measurement, though the
archives had passed their hashes. The archives are now extracted fresh each
build, and crops.py refuses a directory that already holds crops. The
outputs are unchanged.
@widgetii
widgetii merged commit ee157f5 into master Oct 11, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant