From 6d1869adef630ca66f23018da9e11212be937310 Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin <6576495+widgetii@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:52:44 +0300 Subject: [PATCH 1/2] The person detector, its int8 engine, and a release that measures its 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. --- .github/workflows/release.yml | 63 +++ .gitignore | 3 + README.md | 64 ++- coco.txt | 4 + engine/pdnet.c | 836 ++++++++++++++++++++++++++++++++++ engine/pdnet.h | 34 ++ models/person/model.onnx | Bin 0 -> 141882 bytes requirements.txt | 3 + scripts/build.sh | 57 +++ scripts/fetch.sh | 22 + tools/check_exact.py | 46 ++ tools/crops.py | 104 +++++ tools/dets.c | 66 +++ tools/dump_heads.c | 28 ++ tools/export_pdnet.py | 134 ++++++ tools/pdq.py | 161 +++++++ tools/points.py | 100 ++++ 17 files changed, 1724 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 coco.txt create mode 100644 engine/pdnet.c create mode 100644 engine/pdnet.h create mode 100644 models/person/model.onnx create mode 100644 requirements.txt create mode 100755 scripts/build.sh create mode 100755 scripts/fetch.sh create mode 100644 tools/check_exact.py create mode 100644 tools/crops.py create mode 100644 tools/dets.c create mode 100644 tools/dump_heads.c create mode 100644 tools/export_pdnet.py create mode 100644 tools/pdq.py create mode 100644 tools/points.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..3dd6928 --- /dev/null +++ b/.github/workflows/release.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..83a4942 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.cache/ +out/ +__pycache__/ diff --git a/README.md b/README.md index 25fabb5..7eb5658 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/coco.txt b/coco.txt new file mode 100644 index 0000000..a4f7ccc --- /dev/null +++ b/coco.txt @@ -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 diff --git a/engine/pdnet.c b/engine/pdnet.c new file mode 100644 index 0000000..038b3a9 --- /dev/null +++ b/engine/pdnet.c @@ -0,0 +1,836 @@ +#define _POSIX_C_SOURCE 200809L +#include "pdnet.h" + +#include +#include +#include +#include +#include + +#if defined(__ARM_NEON) || defined(__ARM_NEON__) +#include +#define PDNET_NEON 1 +#endif + +#define PDNET_MAGIC "MAJPDNT1" +#define MAX_TENSORS 64 +#define MAX_OPS 64 +#define MAX_LEVELS 3 +#define NMS_IOU 0.55f + +#ifdef PDNET_PROFILE +#include +double pdnet_prof_us[MAX_OPS]; +void pdnet_profile_dump(const PdNet *n, int runs); +#endif + +enum { OP_CONV = 1, OP_RESIZE2X = 2, OP_ADD = 3 }; + +typedef struct { + int h, w, c; + int8_t *q; /* int8 activation, HWC */ + float *f; /* float head output, HWC */ +} Tensor; + +typedef struct { + int type; + int in, in2, out; + /* conv */ + int k, stride, pad, dw, cin, cout, float_out; + int32_t in_zp, out_zp; + const int32_t *ms; /* (mult, shift) pairs */ + const float *scale; /* float_out */ + const int32_t *bias; + const int8_t *w; + int32_t *bias_folded; /* 1x1: bias - in_zp * sum(w) */ + /* For the NEON kernels, prepared at load: weights widened to int16 with + * the output channel innermost (1x1: [cin][cout]; kxk: [k][k][cin][cout]; + * depthwise: [k][k][c]), and each channel's requantization as a + * multiplier and the negated right shift vrshlq_s64 wants. */ + int16_t *w16; + int32_t *mult; + int64_t *nshift; + int kpad; /* kxk: k*k*cin rounded up to 8, the im2col row */ + /* add */ + int32_t zp_a, zp_b, zp_o, m_a, s_a, m_b, s_b, m_o, s_o; +} Op; + +struct PdNet { + uint8_t *blob; + int in_w, in_h; + int8_t lut_y[256], lut_c[256]; + int n_t, n_ops, n_lv; + Tensor t[MAX_TENSORS]; + Op op[MAX_OPS]; + struct { int stride, cls, reg; } lv[MAX_LEVELS]; + int32_t *acc; + int16_t *col; /* im2col rows for the NEON kxk */ +}; + +/* ---- arithmetic ---------------------------------------------------------- */ + +/* acc * mult * 2^(shift - 31), rounded once, half up. */ +static inline int32_t requant(int32_t acc, int32_t mult, int32_t shift) { + const int n = 31 - shift; + const int64_t p = (int64_t)acc * mult; + return (int32_t)((p + ((int64_t)1 << (n - 1))) >> n); +} + +static inline int8_t clamp8(int32_t v) { + return (int8_t)(v < -128 ? -128 : v > 127 ? 127 : v); +} + +/* ---- loading --------------------------------------------------------------- */ + +typedef struct { + const uint8_t *p, *end; + bool bad; +} Rd; + +static const void *take(Rd *r, size_t n) { + if (r->bad || (size_t)(r->end - r->p) < n) { + r->bad = true; + return NULL; + } + const void *v = r->p; + r->p += n; + return v; +} + +static uint32_t u32(Rd *r) { + const uint8_t *b = take(r, 4); + return b ? (uint32_t)b[0] | (uint32_t)b[1] << 8 | (uint32_t)b[2] << 16 | + (uint32_t)b[3] << 24 + : 0; +} + +static int32_t i32(Rd *r) { return (int32_t)u32(r); } + +static float f32(Rd *r) { + uint32_t v = u32(r); + float f; + memcpy(&f, &v, 4); + return f; +} + +static bool tensor_ok(const PdNet *n, uint32_t i) { return i < (uint32_t)n->n_t; } + +static bool parse(PdNet *n, Rd *r) { + const void *m = take(r, 8); + if (!m || memcmp(m, PDNET_MAGIC, 8) || u32(r) != 1) + return false; + n->in_w = (int)u32(r); + n->in_h = (int)u32(r); + const float in_scale = f32(r); + const int32_t in_zp = i32(r); + if (r->bad || n->in_w <= 0 || n->in_w > 4096 || n->in_h <= 0 || n->in_h > 4096 || + !(in_scale > 0)) + return false; + /* The frame's bytes to the network's int8 input, as the float model's + * preprocessing defines it: Y from BT.709 limited range to -1..1, U and + * V from their offset to about -1..1. Two tables of 256. */ + const float ky = (float)(255.0 / 219.0 / 128.0), kc = (float)(255.0 / 224.0 / 128.0); + for (int v = 0; v < 256; v++) { + const float y = ((float)v - 16.0f) * ky - 1.0f; + const float c = ((float)v - 128.0f) * kc; + n->lut_y[v] = clamp8((int32_t)rintf(y / in_scale) + in_zp); + n->lut_c[v] = clamp8((int32_t)rintf(c / in_scale) + in_zp); + } + n->n_t = (int)u32(r); + if (r->bad || n->n_t <= 0 || n->n_t > MAX_TENSORS) + return false; + for (int i = 0; i < n->n_t; i++) { + n->t[i].h = (int)u32(r); + n->t[i].w = (int)u32(r); + n->t[i].c = (int)u32(r); + if (r->bad || n->t[i].h <= 0 || n->t[i].h > 4096 || n->t[i].w <= 0 || + n->t[i].w > 4096 || n->t[i].c <= 0 || n->t[i].c > 4096) + return false; + } + n->n_ops = (int)u32(r); + if (r->bad || n->n_ops <= 0 || n->n_ops > MAX_OPS) + return false; + for (int i = 0; i < n->n_ops; i++) { + Op *o = &n->op[i]; + o->type = (int)u32(r); + if (o->type == OP_CONV) { + o->in = (int)u32(r); + o->out = (int)u32(r); + o->k = (int)u32(r); + o->stride = (int)u32(r); + o->pad = (int)u32(r); + o->dw = (int)u32(r); + o->cin = (int)u32(r); + o->cout = (int)u32(r); + o->float_out = (int)u32(r); + o->in_zp = i32(r); + o->out_zp = i32(r); + if (r->bad || !tensor_ok(n, o->in) || !tensor_ok(n, o->out) || o->k < 1 || + o->k > 7 || o->stride < 1 || o->stride > 4 || o->pad > o->k) + return false; + const Tensor *ti = &n->t[o->in], *to = &n->t[o->out]; + if (ti->c != o->cin || to->c != o->cout || (o->dw && o->cin != o->cout) || + to->h != (ti->h + 2 * o->pad - o->k) / o->stride + 1 || + to->w != (ti->w + 2 * o->pad - o->k) / o->stride + 1) + return false; + if (o->float_out) + o->scale = take(r, (size_t)o->cout * 4); + else + o->ms = take(r, (size_t)o->cout * 8); + o->bias = take(r, (size_t)o->cout * 4); + const size_t wn = o->dw ? (size_t)o->k * o->k * o->cout + : (size_t)o->cout * o->k * o->k * o->cin; + o->w = take(r, wn); + take(r, (4 - wn % 4) % 4); + if (r->bad) + return false; + if (!o->float_out) + for (int c = 0; c < o->cout; c++) { + const int32_t sh = o->ms[2 * c + 1]; + if (sh > 30 || sh < -31) + return false; + } + } else if (o->type == OP_RESIZE2X) { + o->in = (int)u32(r); + o->out = (int)u32(r); + if (r->bad || !tensor_ok(n, o->in) || !tensor_ok(n, o->out)) + return false; + const Tensor *ti = &n->t[o->in], *to = &n->t[o->out]; + if (to->h != 2 * ti->h || to->w != 2 * ti->w || to->c != ti->c) + return false; + } else if (o->type == OP_ADD) { + o->in = (int)u32(r); + o->in2 = (int)u32(r); + o->out = (int)u32(r); + o->zp_a = i32(r); o->zp_b = i32(r); o->zp_o = i32(r); + o->m_a = i32(r); o->s_a = i32(r); + o->m_b = i32(r); o->s_b = i32(r); + o->m_o = i32(r); o->s_o = i32(r); + if (r->bad || !tensor_ok(n, o->in) || !tensor_ok(n, o->in2) || + !tensor_ok(n, o->out)) + return false; + const Tensor *a = &n->t[o->in], *b = &n->t[o->in2], *to = &n->t[o->out]; + if (a->h != b->h || a->w != b->w || a->c != b->c || to->h != a->h || + to->w != a->w || to->c != a->c) + return false; + } else { + return false; + } + } + n->n_lv = (int)u32(r); + if (r->bad || n->n_lv <= 0 || n->n_lv > MAX_LEVELS) + return false; + for (int i = 0; i < n->n_lv; i++) { + n->lv[i].stride = (int)u32(r); + n->lv[i].cls = (int)u32(r); + n->lv[i].reg = (int)u32(r); + if (r->bad || !tensor_ok(n, n->lv[i].cls) || !tensor_ok(n, n->lv[i].reg) || + n->t[n->lv[i].cls].c != 1 || n->t[n->lv[i].reg].c != 4) + return false; + } + return !r->bad && n->t[0].h == n->in_h && n->t[0].w == n->in_w && n->t[0].c == 3; +} + +/* Widened, channel-innermost weights and vector requantization constants + * for a convolution's NEON kernel. Harmless without NEON. */ +static bool prepare(Op *o) { + if (o->type != OP_CONV) + return true; + const int k = o->k, ci = o->cin, co = o->cout; + const size_t wn = o->dw ? (size_t)k * k * co : (size_t)co * k * k * ci; + if (!(o->w16 = malloc(wn * sizeof(int16_t)))) + return false; + if (o->dw) { + for (size_t i = 0; i < wn; i++) + o->w16[i] = o->w[i]; + } else { + /* [k][k][cin] rows, cout innermost; a kxk's rows padded with zeros to + * a multiple of 8, the im2col row its kernel reads 8 at a time. */ + o->kpad = (k * k * ci + 7) & ~7; + free(o->w16); + if (!(o->w16 = calloc((size_t)o->kpad * co, sizeof(int16_t)))) + return false; + for (int c = 0; c < co; c++) + for (int t = 0; t < k * k; t++) + for (int i = 0; i < ci; i++) + o->w16[((size_t)t * ci + i) * co + c] = + o->w[((size_t)c * k * k + t) * ci + i]; + } + if (o->float_out) + return true; + if (!(o->mult = malloc((size_t)co * sizeof(int32_t))) || + !(o->nshift = malloc((size_t)co * sizeof(int64_t)))) + return false; + for (int c = 0; c < co; c++) { + o->mult[c] = o->ms[2 * c]; + o->nshift[c] = -(int64_t)(31 - o->ms[2 * c + 1]); + } + return true; +} + +PdNet *pdnet_open(const char *path) { + FILE *f = fopen(path, "rb"); + if (!f) + return NULL; + PdNet *n = calloc(1, sizeof(*n)); + long len = -1; + if (n && !fseek(f, 0, SEEK_END) && (len = ftell(f)) > 0 && len < (16 << 20) && + !fseek(f, 0, SEEK_SET) && (n->blob = malloc((size_t)len)) && + fread(n->blob, 1, (size_t)len, f) == (size_t)len) { + Rd r = {n->blob, n->blob + len, false}; + if (parse(n, &r)) { + fclose(f); + f = NULL; + /* Buffers: every tensor its own, which for this network is a few + * hundred KB and spares a liveness analysis. */ + bool ok = true; + for (int i = 0; ok && i < n->n_t; i++) { + const size_t e = (size_t)n->t[i].h * n->t[i].w * n->t[i].c; + bool is_float = false; + for (int j = 0; j < n->n_ops; j++) + if (n->op[j].type == OP_CONV && n->op[j].out == i && n->op[j].float_out) + is_float = true; + if (is_float) + ok = (n->t[i].f = malloc(e * sizeof(float))) != NULL; + else + ok = (n->t[i].q = malloc(e)) != NULL; + } + int max_c = 0; + for (int j = 0; j < n->n_ops; j++) + if (n->op[j].type == OP_CONV && n->op[j].cout > max_c) + max_c = n->op[j].cout; + ok = ok && (n->acc = malloc((size_t)max_c * sizeof(int32_t))); + for (int j = 0; ok && j < n->n_ops; j++) { + Op *o = &n->op[j]; + if (o->type != OP_CONV || o->k != 1 || o->dw) + continue; + if (!(o->bias_folded = malloc((size_t)o->cout * sizeof(int32_t)))) { + ok = false; + break; + } + for (int c = 0; c < o->cout; c++) { + int32_t s = 0; + for (int i = 0; i < o->cin; i++) + s += o->w[(size_t)c * o->cin + i]; + o->bias_folded[c] = o->bias[c] - o->in_zp * s; + } + } + size_t col = 0; + for (int j = 0; ok && j < n->n_ops; j++) { + ok = prepare(&n->op[j]); + const Op *o = &n->op[j]; + if (ok && o->type == OP_CONV && !o->dw && o->k > 1) { + const Tensor *to = &n->t[o->out]; + const size_t e = (size_t)to->w * o->kpad; + if (e > col) + col = e; + } + } + ok = ok && (!col || (n->col = malloc(col * sizeof(int16_t)))); + if (ok) + return n; + } + } + if (f) + fclose(f); + pdnet_close(n); + return NULL; +} + +void pdnet_close(PdNet *n) { + if (!n) + return; + for (int i = 0; i < n->n_t; i++) { + free(n->t[i].q); + free(n->t[i].f); + } + for (int j = 0; j < n->n_ops; j++) { + free(n->op[j].bias_folded); + free(n->op[j].w16); + free(n->op[j].mult); + free(n->op[j].nshift); + } + free(n->acc); + free(n->col); + free(n->blob); + free(n); +} + +int pdnet_width(const PdNet *n) { return n->in_w; } +int pdnet_height(const PdNet *n) { return n->in_h; } + +/* ---- the layers -------------------------------------------------------------- */ + +static inline void emit(const Op *o, int32_t acc, int c, int8_t *q, float *f) { + if (o->float_out) + *f = (float)((double)acc * (double)o->scale[c]); + else + *q = clamp8(requant(acc, o->ms[2 * c], o->ms[2 * c + 1]) + o->out_zp); +} + +/* Sum of a[i] * w[i] over n int8 pairs. */ +static inline int32_t dot_i8(const int8_t *a, const int8_t *w, int n) { + int i = 0; + int32_t s = 0; +#ifdef PDNET_NEON + int32x4_t c = vdupq_n_s32(0); + for (; i + 16 <= n; i += 16) { + const int8x16_t av = vld1q_s8(a + i), wv = vld1q_s8(w + i); + c = vpadalq_s16(c, vmull_s8(vget_low_s8(av), vget_low_s8(wv))); + c = vpadalq_s16(c, vmull_s8(vget_high_s8(av), vget_high_s8(wv))); + } + for (; i + 8 <= n; i += 8) + c = vpadalq_s16(c, vmull_s8(vld1_s8(a + i), vld1_s8(w + i))); + s = vgetq_lane_s32(c, 0) + vgetq_lane_s32(c, 1) + vgetq_lane_s32(c, 2) + + vgetq_lane_s32(c, 3); +#endif + for (; i < n; i++) + s += a[i] * w[i]; + return s; +} + +#ifdef PDNET_NEON +/* Eight channels' accumulators to int8: acc * mult rounded once by the + * channel's shift (vrshlq_s64 adds the half before shifting, as requant() + * does), plus the zero point, saturated. Bit for bit requant() + clamp8(). */ +static inline int8x8_t requant8(const Op *o, int c, int32x4_t lo, int32x4_t hi) { + const int32x4_t m0 = vld1q_s32(o->mult + c), m1 = vld1q_s32(o->mult + c + 4); + const int64_t *ns = o->nshift + c; + int64x2_t p0 = vrshlq_s64(vmull_s32(vget_low_s32(lo), vget_low_s32(m0)), vld1q_s64(ns)); + int64x2_t p1 = vrshlq_s64(vmull_s32(vget_high_s32(lo), vget_high_s32(m0)), vld1q_s64(ns + 2)); + int64x2_t p2 = vrshlq_s64(vmull_s32(vget_low_s32(hi), vget_low_s32(m1)), vld1q_s64(ns + 4)); + int64x2_t p3 = vrshlq_s64(vmull_s32(vget_high_s32(hi), vget_high_s32(m1)), vld1q_s64(ns + 6)); + const int32x4_t zp = vdupq_n_s32(o->out_zp); + const int32x4_t r0 = vaddq_s32(vcombine_s32(vqmovn_s64(p0), vqmovn_s64(p1)), zp); + const int32x4_t r1 = vaddq_s32(vcombine_s32(vqmovn_s64(p2), vqmovn_s64(p3)), zp); + return vqmovn_s16(vcombine_s16(vqmovn_s32(r0), vqmovn_s32(r1))); +} + +/* The multiply both 1x1 and kxk come down to: npix rows of K inputs (K a + * multiple of 8) against a [K][cout] int16 matrix, cout a multiple of 8. + * Four pixels share every weight load; inputs come in 8 at a time and are + * used lane by lane (vmlal_lane_s16), so nothing crosses from the NEON + * registers to the core ones in the inner loop. IN is int8_t (a 1x1's + * activations, zero point folded into its bias) or int16_t (an im2col row, + * zero point already taken out). */ +#define GEMM_LOAD8_int8_t(p) vmovl_s8(vld1_s8(p)) +#define GEMM_LOAD8_int16_t(p) vld1q_s16(p) +#define DEFINE_GEMM(IN) \ + static void gemm_##IN(const Op *o, const IN *a, int K, const int32_t *bias, \ + int8_t *out, int npix) { \ + const int cout = o->cout; \ + int m = 0; \ + for (; m + 4 <= npix; m += 4) { \ + const IN *a0 = a + (size_t)m * K, *a1 = a0 + K, *a2 = a1 + K, *a3 = a2 + K; \ + for (int c = 0; c < cout; c += 8) { \ + const int32x4_t b0 = vld1q_s32(bias + c), b1 = vld1q_s32(bias + c + 4); \ + int32x4_t l0 = b0, h0 = b1, l1 = b0, h1 = b1; \ + int32x4_t l2 = b0, h2 = b1, l3 = b0, h3 = b1; \ + const int16_t *w = o->w16 + c; \ + for (int i = 0; i < K; i += 8) { \ + const int16x8_t x0 = GEMM_LOAD8_##IN(a0 + i); \ + const int16x8_t x1 = GEMM_LOAD8_##IN(a1 + i); \ + const int16x8_t x2 = GEMM_LOAD8_##IN(a2 + i); \ + const int16x8_t x3 = GEMM_LOAD8_##IN(a3 + i); \ + GEMM_STEP(0, vget_low_s16) GEMM_STEP(1, vget_low_s16) \ + GEMM_STEP(2, vget_low_s16) GEMM_STEP(3, vget_low_s16) \ + GEMM_STEP(0, vget_high_s16) GEMM_STEP(1, vget_high_s16) \ + GEMM_STEP(2, vget_high_s16) GEMM_STEP(3, vget_high_s16) \ + } \ + vst1_s8(out + (size_t)(m + 0) * cout + c, requant8(o, c, l0, h0)); \ + vst1_s8(out + (size_t)(m + 1) * cout + c, requant8(o, c, l1, h1)); \ + vst1_s8(out + (size_t)(m + 2) * cout + c, requant8(o, c, l2, h2)); \ + vst1_s8(out + (size_t)(m + 3) * cout + c, requant8(o, c, l3, h3)); \ + } \ + } \ + for (; m < npix; m++) { \ + const IN *a0 = a + (size_t)m * K; \ + for (int c = 0; c < cout; c += 8) { \ + int32x4_t lo = vld1q_s32(bias + c), hi = vld1q_s32(bias + c + 4); \ + const int16_t *w = o->w16 + c; \ + for (int i = 0; i < K; i++, w += cout) { \ + const int16x8_t wv = vld1q_s16(w); \ + lo = vmlal_n_s16(lo, vget_low_s16(wv), a0[i]); \ + hi = vmlal_n_s16(hi, vget_high_s16(wv), a0[i]); \ + } \ + vst1_s8(out + (size_t)m * cout + c, requant8(o, c, lo, hi)); \ + } \ + } \ + } +/* One half (4 inputs) of an 8-input step: four weight rows, each against + * its input lane, for all four pixels. */ +#define GEMM_LANE(P, HALF, L) \ + { const int16x8_t wv = vld1q_s16(w); w += cout; \ + l0 = vmlal_lane_s16(l0, vget_low_s16(wv), HALF(x0), L); \ + h0 = vmlal_lane_s16(h0, vget_high_s16(wv), HALF(x0), L); \ + l1 = vmlal_lane_s16(l1, vget_low_s16(wv), HALF(x1), L); \ + h1 = vmlal_lane_s16(h1, vget_high_s16(wv), HALF(x1), L); \ + l2 = vmlal_lane_s16(l2, vget_low_s16(wv), HALF(x2), L); \ + h2 = vmlal_lane_s16(h2, vget_high_s16(wv), HALF(x2), L); \ + l3 = vmlal_lane_s16(l3, vget_low_s16(wv), HALF(x3), L); \ + h3 = vmlal_lane_s16(h3, vget_high_s16(wv), HALF(x3), L); } +/* GEMM_STEP(lane, half): the step order above walks lanes 0..3 of the low + * half, then 0..3 of the high half, matching the weight rows i..i+7. */ +#define GEMM_STEP(L, HALF) GEMM_LANE(0, HALF, L) +DEFINE_GEMM(int8_t) +DEFINE_GEMM(int16_t) + +static void conv1x1_neon(const Op *o, const Tensor *ti, Tensor *to) { + gemm_int8_t(o, ti->q, o->cin, o->bias_folded, to->q, to->h * to->w); +} + +/* kxk (the stem) as im2col and the same multiply: each output pixel's k*k*cin + * inputs, zero point taken out, padding as 0, rows padded to a multiple of 8. */ +static void conv_kxk_neon(const Op *o, const Tensor *ti, Tensor *to, int16_t *col) { + const int k = o->k, st = o->stride, pad = o->pad, cin = o->cin, K = o->kpad; + /* One output row at a time, so the rows stay in L1 between being written + * and being read: the first layer's whole im2col is a megabyte. */ + for (int oy = 0; oy < to->h; oy++) { + int16_t *r = col; + for (int ox = 0; ox < to->w; ox++, r += K) { + int16_t *q = r; + for (int ky = 0; ky < k; ky++) { + const int iy = oy * st + ky - pad; + for (int kx = 0; kx < k; kx++) { + const int ix = ox * st + kx - pad; + if (iy < 0 || iy >= ti->h || ix < 0 || ix >= ti->w) { + for (int i = 0; i < cin; i++) + *q++ = 0; + continue; + } + const int8_t *ip = ti->q + ((size_t)iy * ti->w + ix) * cin; + for (int i = 0; i < cin; i++) + *q++ = (int16_t)(ip[i] - o->in_zp); + } + } + for (; q < r + K; q++) + *q = 0; + } + gemm_int16_t(o, col, K, o->bias, to->q + (size_t)oy * to->w * o->cout, to->w); + } +} + +/* Depthwise: eight channels at a time, the chunk's nine weight vectors held + * in registers across every pixel, and the accumulators across the taps. + * Interior pixels skip the bounds tests; the border takes them. */ +static void depthwise_neon(const Op *o, const Tensor *ti, Tensor *to) { + const int k = o->k, st = o->stride, pad = o->pad, c = o->cout; + const int16x8_t zp = vdupq_n_s16((int16_t)o->in_zp); + const size_t rs = (size_t)ti->w * c; /* input row stride */ + for (int ch = 0; ch < c; ch += 8) { + int16x8_t w[9]; + if (k == 3) + for (int t = 0; t < 9; t++) + w[t] = vld1q_s16(o->w16 + (size_t)t * c + ch); + const int32x4_t b0 = vld1q_s32(o->bias + ch), b1 = vld1q_s32(o->bias + ch + 4); + for (int oy = 0; oy < to->h; oy++) { + const int y0 = oy * st - pad; + for (int ox = 0; ox < to->w; ox++) { + const int x0 = ox * st - pad; + int32x4_t lo = b0, hi = b1; + if (k == 3 && y0 >= 0 && y0 + 2 < ti->h && x0 >= 0 && x0 + 2 < ti->w) { + const int8_t *p = ti->q + (size_t)y0 * rs + (size_t)x0 * c + ch; +#define DW_TAP(T, OFF) \ + { const int16x8_t a = vsubq_s16(vmovl_s8(vld1_s8(p + (OFF))), zp); \ + lo = vmlal_s16(lo, vget_low_s16(a), vget_low_s16(w[T])); \ + hi = vmlal_s16(hi, vget_high_s16(a), vget_high_s16(w[T])); } + DW_TAP(0, 0) DW_TAP(1, c) DW_TAP(2, 2 * c) + DW_TAP(3, rs) DW_TAP(4, rs + c) DW_TAP(5, rs + 2 * c) + DW_TAP(6, 2 * rs) DW_TAP(7, 2 * rs + c) DW_TAP(8, 2 * rs + 2 * c) +#undef DW_TAP + } else { + for (int ky = 0; ky < k; ky++) { + const int iy = y0 + ky; + if (iy < 0 || iy >= ti->h) + continue; + for (int kx = 0; kx < k; kx++) { + const int ix = x0 + kx; + if (ix < 0 || ix >= ti->w) + continue; + const int16x8_t a = vsubq_s16( + vmovl_s8(vld1_s8(ti->q + (size_t)iy * rs + (size_t)ix * c + ch)), zp); + const int16x8_t wv = vld1q_s16(o->w16 + (size_t)(ky * k + kx) * c + ch); + lo = vmlal_s16(lo, vget_low_s16(a), vget_low_s16(wv)); + hi = vmlal_s16(hi, vget_high_s16(a), vget_high_s16(wv)); + } + } + } + vst1_s8(to->q + ((size_t)oy * to->w + ox) * c + ch, requant8(o, ch, lo, hi)); + } + } + } +} +#endif + +/* 1x1: every output pixel is a row of cin bytes against cout weight rows. + * The zero point is folded into the bias at load. */ +static void conv1x1(const Op *o, const Tensor *ti, Tensor *to) { + const int hw = to->h * to->w, cin = o->cin, cout = o->cout; + for (int m = 0; m < hw; m++) { + const int8_t *a = ti->q + (size_t)m * cin; + for (int c = 0; c < cout; c++) { + const int32_t acc = o->bias_folded[c] + dot_i8(a, o->w + (size_t)c * cin, cin); + emit(o, acc, c, to->q ? to->q + (size_t)m * cout + c : NULL, + to->f ? to->f + (size_t)m * cout + c : NULL); + } + } +} + +/* kxk, all channels mixed: the stem. Taps outside the picture are padding, + * which contributes (zp - zp) * w = 0 and is skipped. */ +static void conv_kxk(const Op *o, const Tensor *ti, Tensor *to, int32_t *acc) { + const int k = o->k, st = o->stride, pad = o->pad, cin = o->cin, cout = o->cout; + for (int oy = 0; oy < to->h; oy++) + for (int ox = 0; ox < to->w; ox++) { + for (int c = 0; c < cout; c++) + acc[c] = o->bias[c]; + for (int ky = 0; ky < k; ky++) { + const int iy = oy * st + ky - pad; + if (iy < 0 || iy >= ti->h) + continue; + for (int kx = 0; kx < k; kx++) { + const int ix = ox * st + kx - pad; + if (ix < 0 || ix >= ti->w) + continue; + const int8_t *ip = ti->q + ((size_t)iy * ti->w + ix) * cin; + for (int c = 0; c < cout; c++) { + const int8_t *wp = o->w + (((size_t)c * k + ky) * k + kx) * cin; + int32_t s = 0; + for (int i = 0; i < cin; i++) + s += ((int32_t)ip[i] - o->in_zp) * wp[i]; + acc[c] += s; + } + } + } + const size_t base = ((size_t)oy * to->w + ox) * cout; + for (int c = 0; c < cout; c++) + emit(o, acc[c], c, to->q ? to->q + base + c : NULL, + to->f ? to->f + base + c : NULL); + } +} + +/* Depthwise kxk: channels innermost, so each tap is one contiguous run. */ +static void depthwise(const Op *o, const Tensor *ti, Tensor *to, int32_t *acc) { + const int k = o->k, st = o->stride, pad = o->pad, c = o->cout; + for (int oy = 0; oy < to->h; oy++) + for (int ox = 0; ox < to->w; ox++) { + for (int ch = 0; ch < c; ch++) + acc[ch] = o->bias[ch]; + for (int ky = 0; ky < k; ky++) { + const int iy = oy * st + ky - pad; + if (iy < 0 || iy >= ti->h) + continue; + for (int kx = 0; kx < k; kx++) { + const int ix = ox * st + kx - pad; + if (ix < 0 || ix >= ti->w) + continue; + const int8_t *ip = ti->q + ((size_t)iy * ti->w + ix) * c; + const int8_t *wp = o->w + ((size_t)ky * k + kx) * c; + int ch = 0; +#ifdef PDNET_NEON + const int16x8_t zp = vdupq_n_s16((int16_t)o->in_zp); + for (; ch + 8 <= c; ch += 8) { + const int16x8_t a = vsubq_s16(vmovl_s8(vld1_s8(ip + ch)), zp); + const int16x8_t w = vmovl_s8(vld1_s8(wp + ch)); + int32x4_t lo = vld1q_s32(acc + ch), hi = vld1q_s32(acc + ch + 4); + lo = vmlal_s16(lo, vget_low_s16(a), vget_low_s16(w)); + hi = vmlal_s16(hi, vget_high_s16(a), vget_high_s16(w)); + vst1q_s32(acc + ch, lo); + vst1q_s32(acc + ch + 4, hi); + } +#endif + for (; ch < c; ch++) + acc[ch] += ((int32_t)ip[ch] - o->in_zp) * wp[ch]; + } + } + const size_t base = ((size_t)oy * to->w + ox) * c; + for (int ch = 0; ch < c; ch++) + emit(o, acc[ch], ch, to->q ? to->q + base + ch : NULL, + to->f ? to->f + base + ch : NULL); + } +} + +static void resize2x(const Tensor *ti, Tensor *to) { + const int c = ti->c; + for (int y = 0; y < to->h; y++) + for (int x = 0; x < to->w; x++) + memcpy(to->q + ((size_t)y * to->w + x) * c, + ti->q + ((size_t)(y / 2) * ti->w + x / 2) * c, (size_t)c); +} + +static void add(const Op *o, const Tensor *a, const Tensor *b, Tensor *to) { + const size_t e = (size_t)to->h * to->w * to->c; + size_t i = 0; +#ifdef PDNET_NEON + /* The same three roundings, two lanes of 64 bits at a time. */ + const int64x2_t na = vdupq_n_s64(-(int64_t)(31 - o->s_a)); + const int64x2_t nb = vdupq_n_s64(-(int64_t)(31 - o->s_b)); + const int64x2_t no = vdupq_n_s64(-(int64_t)(31 - o->s_o)); + const int32x2_t ma = vdup_n_s32(o->m_a), mb = vdup_n_s32(o->m_b), mo = vdup_n_s32(o->m_o); + const int32x4_t za = vdupq_n_s32(o->zp_a), zb = vdupq_n_s32(o->zp_b), zo = vdupq_n_s32(o->zp_o); +#define ADD_RQ(x, m, nsh) \ + vcombine_s32(vqmovn_s64(vrshlq_s64(vmull_s32(vget_low_s32(x), m), nsh)), \ + vqmovn_s64(vrshlq_s64(vmull_s32(vget_high_s32(x), m), nsh))) + for (; i + 8 <= e; i += 8) { + const int16x8_t va = vmovl_s8(vld1_s8(a->q + i)), vb = vmovl_s8(vld1_s8(b->q + i)); + int32x4_t out[2]; + for (int h = 0; h < 2; h++) { + const int16x4_t ha = h ? vget_high_s16(va) : vget_low_s16(va); + const int16x4_t hb = h ? vget_high_s16(vb) : vget_low_s16(vb); + const int32x4_t xa = vshlq_n_s32(vsubq_s32(vmovl_s16(ha), za), 20); + const int32x4_t xb = vshlq_n_s32(vsubq_s32(vmovl_s16(hb), zb), 20); + const int32x4_t sum = vaddq_s32(ADD_RQ(xa, ma, na), ADD_RQ(xb, mb, nb)); + out[h] = vaddq_s32(ADD_RQ(sum, mo, no), zo); + } + vst1_s8(to->q + i, vqmovn_s16(vcombine_s16(vqmovn_s32(out[0]), vqmovn_s32(out[1])))); + } +#undef ADD_RQ +#endif + for (; i < e; i++) { + const int32_t ra = requant(((int32_t)a->q[i] - o->zp_a) * (1 << 20), o->m_a, o->s_a); + const int32_t rb = requant(((int32_t)b->q[i] - o->zp_b) * (1 << 20), o->m_b, o->s_b); + to->q[i] = clamp8(requant(ra + rb, o->m_o, o->s_o) + o->zp_o); + } +} + +/* ---- the frame in, the people out ------------------------------------------- */ + +static float sigmoidf(float x) { return 1.0f / (1.0f + expf(-x)); } + +static float iou(const PdBox *a, const PdBox *b) { + const int x0 = a->x > b->x ? a->x : b->x, y0 = a->y > b->y ? a->y : b->y; + const int x1 = a->x + a->w < b->x + b->w ? a->x + a->w : b->x + b->w; + const int y1 = a->y + a->h < b->y + b->h ? a->y + a->h : b->y + b->h; + if (x1 <= x0 || y1 <= y0) + return 0; + const float i = (float)(x1 - x0) * (y1 - y0); + return i / ((float)a->w * a->h + (float)b->w * b->h - i); +} + +static int by_score(const void *a, const void *b) { + return ((const PdBox *)b)->score - ((const PdBox *)a)->score; +} + +int pdnet_detect( + PdNet *n, const uint8_t *y, int ystride, const uint8_t *vu, int vustride, + PdBox *out, int max, int score_min) { + if (!n || !y || !vu || !out || max <= 0) + return -1; + int8_t *in = n->t[0].q; + for (int j = 0; j < n->in_h; j++) { + const uint8_t *yr = y + (size_t)j * ystride, *cr = vu + (size_t)(j / 2) * vustride; + int8_t *p = in + (size_t)j * n->in_w * 3; + for (int i = 0; i < n->in_w; i++) { + const int ci = i & ~1; + p[3 * i + 0] = n->lut_y[yr[i]]; + p[3 * i + 1] = n->lut_c[cr[ci + 1]]; /* U; NV21 puts V first */ + p[3 * i + 2] = n->lut_c[cr[ci]]; /* V */ + } + } +#ifdef PDNET_PROFILE + struct timespec t0, t1; +#endif + for (int j = 0; j < n->n_ops; j++) { + const Op *o = &n->op[j]; + Tensor *to = &n->t[o->out]; +#ifdef PDNET_PROFILE + clock_gettime(CLOCK_MONOTONIC, &t0); +#endif + if (o->type == OP_CONV) { + const Tensor *ti = &n->t[o->in]; +#ifdef PDNET_NEON + /* Eight output channels a time; the heads (1 and 4 channels, and + * float) take the plain path, which costs them nothing. */ + if (!o->float_out && o->cout % 8 == 0 && + (o->dw || o->k > 1 || o->cin % 8 == 0)) { + if (o->dw) + depthwise_neon(o, ti, to); + else if (o->k == 1) + conv1x1_neon(o, ti, to); + else + conv_kxk_neon(o, ti, to, n->col); + } else +#endif + if (o->dw) + depthwise(o, ti, to, n->acc); + else if (o->k == 1) + conv1x1(o, ti, to); + else + conv_kxk(o, ti, to, n->acc); + } else if (o->type == OP_RESIZE2X) { + resize2x(&n->t[o->in], to); + } else { + add(o, &n->t[o->in], &n->t[o->in2], to); + } +#ifdef PDNET_PROFILE + clock_gettime(CLOCK_MONOTONIC, &t1); + pdnet_prof_us[j] += (t1.tv_sec - t0.tv_sec) * 1e6 + (t1.tv_nsec - t0.tv_nsec) / 1e3; +#endif + } + + /* Decode as the TVM shim did: an anchor-free head, a box per cell whose + * score clears the floor, distances in stride units, then NMS. */ + static PdBox cand[4096]; + int nc = 0; + for (int l = 0; l < n->n_lv; l++) { + const Tensor *tc = &n->t[n->lv[l].cls], *tr = &n->t[n->lv[l].reg]; + const int st = n->lv[l].stride; + for (int cy = 0; cy < tc->h; cy++) + for (int cx = 0; cx < tc->w; cx++) { + const int i = cy * tc->w + cx; + const int s = (int)(sigmoidf(tc->f[i]) * 100.0f + 0.5f); + if (s < score_min || nc == (int)(sizeof(cand) / sizeof(cand[0]))) + continue; + const float *r = tr->f + (size_t)i * 4; + const float ccx = (cx + 0.5f) * st, ccy = (cy + 0.5f) * st; + const int x0 = (int)(ccx - fmaxf(0.0f, r[0]) * st); + const int y0 = (int)(ccy - fmaxf(0.0f, r[1]) * st); + const int x1 = (int)(ccx + fmaxf(0.0f, r[2]) * st); + const int y1 = (int)(ccy + fmaxf(0.0f, r[3]) * st); + if (x1 <= x0 || y1 <= y0) + continue; + cand[nc++] = (PdBox){x0, y0, x1 - x0, y1 - y0, s}; + } + } + qsort(cand, (size_t)nc, sizeof(cand[0]), by_score); + int kept = 0; + for (int i = 0; i < nc && kept < max; i++) { + bool dup = false; + for (int j = 0; j < kept && !dup; j++) + dup = iou(&cand[i], &out[j]) > NMS_IOU; + if (!dup) + out[kept++] = cand[i]; + } + return kept; +} + +const float *pdnet_head(const PdNet *n, int level, int reg, int *h, int *w) { + if (!n || level < 0 || level >= n->n_lv) + return NULL; + const Tensor *t = &n->t[reg ? n->lv[level].reg : n->lv[level].cls]; + *h = t->h; + *w = t->w; + return t->f; +} + +#ifdef PDNET_PROFILE +void pdnet_profile_dump(const PdNet *n, int runs) { + double total = 0; + for (int j = 0; j < n->n_ops; j++) + total += pdnet_prof_us[j]; + for (int j = 0; j < n->n_ops; j++) { + const Op *o = &n->op[j]; + const Tensor *to = &n->t[o->out]; + const char *kind = o->type == OP_RESIZE2X ? "resize" : o->type == OP_ADD ? "add" + : o->dw ? "dw" : o->k == 1 ? "1x1" : "kxk"; + long macs = 0; + if (o->type == OP_CONV) + macs = (long)to->h * to->w * o->cout * o->k * o->k * (o->dw ? 1 : o->cin); + fprintf(stderr, "op %2d %-6s k%d s%d %3d->%3d out %3dx%3d %7.2f ms %5.1f%% %6.1f MMAC/s\n", + j, kind, o->k, o->stride, o->cin, o->cout, to->h, to->w, + pdnet_prof_us[j] / runs / 1e3, 100 * pdnet_prof_us[j] / total, + macs ? macs / (pdnet_prof_us[j] / runs) : 0.0); + } + fprintf(stderr, "total %.2f ms per run\n", total / runs / 1e3); +} +#endif diff --git a/engine/pdnet.h b/engine/pdnet.h new file mode 100644 index 0000000..7840fb2 --- /dev/null +++ b/engine/pdnet.h @@ -0,0 +1,34 @@ +#pragma once + +/* pdnet: the CPU person detector's int8 network, run directly. + * + * Loads the blob export_pdnet.py writes from the quantized model and runs it + * on an NV21 frame of the network's input size: int8 activations, per-channel + * int8 weights, int32 accumulators, one rounding per requantization, and the + * detection heads taken to float straight from their accumulators. */ + +#include +#include + +typedef struct PdNet PdNet; + +typedef struct { + int x, y, w, h; + int score; /* 0..100 */ +} PdBox; + +PdNet *pdnet_open(const char *path); +void pdnet_close(PdNet *n); +int pdnet_width(const PdNet *n); +int pdnet_height(const PdNet *n); + +/* Detect people on one NV21 frame of exactly pdnet_width() x pdnet_height(): + * boxes in input pixels, scores 0..100, at most `max`, best first, after + * suppression. Returns the count, or -1. */ +int pdnet_detect( + PdNet *n, const uint8_t *y, int ystride, const uint8_t *vu, int vustride, + PdBox *out, int max, int score_min); + +/* The raw head outputs of the last pdnet_detect(), for checking against the + * reference: level 0..2, cls (1 channel) or reg (4), HWC float. */ +const float *pdnet_head(const PdNet *n, int level, int reg, int *h, int *w); diff --git a/models/person/model.onnx b/models/person/model.onnx new file mode 100644 index 0000000000000000000000000000000000000000..320dcc4db2e5f4f8d736bc59c0faf44c4eb62b47 GIT binary patch literal 141882 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 "$out/dets.txt" +# The camera's path must find the same boxes, not just the same heads. +qemu-arm-static "$out/dets.arm" "$out/person.pdnet" "$out/eval/crops" > "$out/dets.arm.txt" +cmp "$out/dets.txt" "$out/dets.arm.txt" || { echo "ARM and host detections differ"; exit 1; } + +python3 "$src/tools/points.py" "$out/eval/labels.json" "$out/dets.txt" \ + "$out/person.json" "$out/points.md" +cd "$out" && sha256sum person.pdnet person.json > SHA256SUMS && cat SHA256SUMS diff --git a/scripts/fetch.sh b/scripts/fetch.sh new file mode 100755 index 0000000..8a4413b --- /dev/null +++ b/scripts/fetch.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# fetch.sh : every "file sha256 url" line of , into +# , hash-checked. A cached file whose hash does not match is +# fetched again, and a download lands in the cache only once its hash +# matches, so an interrupted one can never be mistaken for the file. +set -euo pipefail +mkdir -p "$2" +grep -v '^#' "$1" | while read -r file sha url; do + [ -n "$file" ] || continue + dst="$2/$file" + if [ -f "$dst" ] && echo "$sha $dst" | sha256sum -c --status 2>/dev/null; then + continue + fi + tmp="$dst.part" + if ! curl -sfL --retry 3 -o "$tmp" "$url" || + ! echo "$sha $tmp" | sha256sum -c --status; then + rm -f "$tmp" + echo "fetch.sh: $file from $url: download failed or sha256 mismatch" >&2 + exit 1 + fi + mv "$tmp" "$dst" +done diff --git a/tools/check_exact.py b/tools/check_exact.py new file mode 100644 index 0000000..d6d637d --- /dev/null +++ b/tools/check_exact.py @@ -0,0 +1,46 @@ +# check_exact.py ... +# +# The engine must compute what the quantized model defines. For each crop, +# .heads (written by dump_heads, from pdnet.c) must equal pdq.py's +# reference arithmetic exactly, every value of all six head maps. Exits +# non-zero on any difference, so a release stops before it measures or +# publishes anything. +import sys + +import numpy as np + +sys.path.insert(0, __file__.rsplit("/", 1)[0]) +import pdq # noqa: E402 + +W, H = 320, 192 + + +def chw_from_nv21(y, vu): + """The float model's input, as majestic's preprocessing defines it.""" + ky = np.float32(255 / 219 / 128) + kc = np.float32(255 / 224 / 128) + Y = (y.astype(np.float32) - np.float32(16)) * ky - np.float32(1) + V = (vu[:, 0::2].astype(np.float32) - np.float32(128)) * kc + U = (vu[:, 1::2].astype(np.float32) - np.float32(128)) * kc + up = lambda p: np.repeat(np.repeat(p, 2, 0), 2, 1) + return np.stack([Y, up(U), up(V)]).astype(np.float32) + + +g = pdq.Graph(sys.argv[1]) +crops = sys.argv[2:] +if not crops: + sys.exit("check_exact: no crops") +bad = 0 +for f in crops: + raw = np.fromfile(f, np.uint8) + ref = pdq.run(g, chw_from_nv21(raw[:W * H].reshape(H, W), raw[W * H:].reshape(H // 2, W)), + head="float") + got = np.fromfile(f + ".heads", np.float32) + want = np.concatenate([r.transpose(1, 2, 0).reshape(-1) for r in ref]).astype(np.float32) + if got.shape != want.shape or not np.array_equal(got, want): + n = int(np.sum(got != want)) if got.shape == want.shape else -1 + print(f"check_exact: {f}: {n} values differ") + bad += 1 +if bad: + sys.exit(f"check_exact: {bad} of {len(crops)} crops differ from the reference") +print(f"check_exact: all {len(crops)} crops identical to the reference") diff --git a/tools/crops.py b/tools/crops.py new file mode 100644 index 0000000..9847b0a --- /dev/null +++ b/tools/crops.py @@ -0,0 +1,104 @@ +# crops.py [n_person] [n_background] +# +# The labelled set the person model's presets are measured on, in the +# geometry the camera hands the model: 320x192 crops around a person at the +# scale the detector was trained for (the person ~47% of the crop's height, as +# majestic places a crop), off-centre the way a motion region is, and crops of +# person-free COCO val2017 images for the false alarms. Each crop is NV21, +# BT.709 limited range, as the VPSS delivers it; labels.json lists the people +# inside each in crop pixels, marking crowds and people less than half inside +# the crop to be ignored. Seeded: the same inputs give the same crops. +import json +import os +import random +import sys + +import cv2 +import numpy as np + +W, H = 320, 192 +FILL = 0.47 + + +def nv21(rgb): + r, g, b = rgb[..., 0], rgb[..., 1], rgb[..., 2] + y = 16 + (0.2126 * r + 0.7152 * g + 0.0722 * b) * 219 / 255 + cb = 128 + (-0.1146 * r - 0.3854 * g + 0.5 * b) * 224 / 255 + cr = 128 + (0.5 * r - 0.4542 * g - 0.0458 * b) * 224 / 255 + sub = lambda c: c.reshape(H // 2, 2, W // 2, 2).mean((1, 3)) + vu = np.stack([sub(cr), sub(cb)], -1).reshape(H // 2, W) + return (np.clip(y, 0, 255).round().astype(np.uint8), + np.clip(vu, 0, 255).round().astype(np.uint8)) + + +def main(coco, out, n_person=2000, n_bg=1000): + n_person, n_bg = int(n_person), int(n_bg) + rnd = random.Random(20261011) + ann = json.load(open(f"{coco}/annotations/instances_val2017.json")) + imgs = {i["id"]: i for i in ann["images"]} + people = {} + for a in ann["annotations"]: + if a["category_id"] == 1: + people.setdefault(a["image_id"], []).append(a) + os.makedirs(f"{out}/crops", exist_ok=True) + labels = {} + + def emit(name, img, x0, y0, cw, ch, anns): + x0, y0, cw, ch = (int(round(v)) for v in (x0, y0, cw, ch)) + crop = img[y0:y0 + ch, x0:x0 + cw] + rgb = cv2.resize(cv2.cvtColor(crop, cv2.COLOR_BGR2RGB), (W, H), + interpolation=cv2.INTER_AREA).astype(np.float32) + y, vu = nv21(rgb) + with open(f"{out}/crops/{name}.nv21", "wb") as f: + f.write(y.tobytes() + vu.tobytes()) + sx, sy = W / cw, H / ch + boxes = [] + for a in anns: + bx, by, bw, bh = a["bbox"] + ix0, iy0 = max(bx, x0), max(by, y0) + ix1, iy1 = min(bx + bw, x0 + cw), min(by + bh, y0 + ch) + if ix1 <= ix0 or iy1 <= iy0: + continue + visible = (ix1 - ix0) * (iy1 - iy0) / max(bw * bh, 1e-9) + boxes.append({"box": [(ix0 - x0) * sx, (iy0 - y0) * sy, + (ix1 - ix0) * sx, (iy1 - iy0) * sy], + "ignore": bool(a["iscrowd"]) or visible < 0.5}) + labels[name] = boxes + + cands = [(iid, a) for iid, anns in sorted(people.items()) for a in anns + if not a["iscrowd"] and a["bbox"][3] >= 24] + rnd.shuffle(cands) + made = 0 + for iid, a in cands: + if made >= n_person: + break + im = imgs[iid] + bx, by, bw, bh = a["bbox"] + ch = bh / FILL + cw = ch * W / H + if cw > im["width"] or ch > im["height"]: + continue + cx = bx + bw / 2 + rnd.uniform(-cw / 3, cw / 3) + cy = by + bh / 2 + rnd.uniform(-ch / 6, ch / 6) + x0 = min(max(cx - cw / 2, 0), im["width"] - cw) + y0 = min(max(cy - ch / 2, 0), im["height"] - ch) + img = cv2.imread(f"{coco}/val2017/{im['file_name']}") + emit(f"p{made:05d}_{iid}", img, x0, y0, cw, ch, people[iid]) + made += 1 + empty = sorted(i for i in imgs if i not in people) + rnd.shuffle(empty) + for k, iid in enumerate(empty[:n_bg]): + im = imgs[iid] + ch = rnd.uniform(0.35, 1.0) * min(im["height"], im["width"] * H / W) + cw = ch * W / H + x0 = rnd.uniform(0, im["width"] - cw) + y0 = rnd.uniform(0, im["height"] - ch) + img = cv2.imread(f"{coco}/val2017/{im['file_name']}") + emit(f"b{k:05d}_{iid}", img, x0, y0, cw, ch, []) + with open(f"{out}/labels.json", "w") as f: + json.dump(labels, f, sort_keys=True) + print(f"crops.py: {made} person crops, {min(n_bg, len(empty))} person-free") + + +if __name__ == "__main__": + main(*sys.argv[1:]) diff --git a/tools/dets.c b/tools/dets.c new file mode 100644 index 0000000..f53601a --- /dev/null +++ b/tools/dets.c @@ -0,0 +1,66 @@ +#define _POSIX_C_SOURCE 200809L +/* dets : every box the engine finds on every crop, as + * " x y w h score" (input pixels, score 0..100), at a score floor of + * 1, so points.py can sweep every threshold. The crops in name order. */ +#include +#include +#include +#include + +#include "pdnet.h" + +static int cmp(const void *a, const void *b) { + return strcmp(*(char *const *)a, *(char *const *)b); +} + +int main(int argc, char **argv) { + if (argc != 3) { + fprintf(stderr, "usage: %s \n", argv[0]); + return 2; + } + PdNet *n = pdnet_open(argv[1]); + if (!n) { + fprintf(stderr, "dets: cannot open %s\n", argv[1]); + return 1; + } + const int w = pdnet_width(n), h = pdnet_height(n); + const size_t frame = (size_t)w * h * 3 / 2; + DIR *d = opendir(argv[2]); + if (!d) + return 1; + char **names = NULL; + size_t nn = 0, cap = 0; + struct dirent *e; + while ((e = readdir(d))) { + const size_t l = strlen(e->d_name); + if (l <= 5 || strcmp(e->d_name + l - 5, ".nv21")) + continue; + if (nn == cap && !(names = realloc(names, (cap = cap ? 2 * cap : 1024) * sizeof(*names)))) + return 1; + names[nn++] = strdup(e->d_name); + } + closedir(d); + qsort(names, nn, sizeof(*names), cmp); + unsigned char *buf = malloc(frame); + PdBox box[256]; + for (size_t i = 0; i < nn; i++) { + char path[4096]; + snprintf(path, sizeof path, "%s/%s", argv[2], names[i]); + FILE *f = fopen(path, "rb"); + if (!f || fread(buf, 1, frame, f) != frame) { + fprintf(stderr, "dets: cannot read %s\n", path); + return 1; + } + fclose(f); + const int k = pdnet_detect(n, buf, w, buf + (size_t)w * h, w, box, 256, 1); + if (k < 0) + return 1; + names[i][strlen(names[i]) - 5] = 0; + for (int j = 0; j < k; j++) + printf("%s %d %d %d %d %d\n", names[i], box[j].x, box[j].y, box[j].w, + box[j].h, box[j].score); + } + fprintf(stderr, "dets: %zu crops\n", nn); + pdnet_close(n); + return 0; +} diff --git a/tools/dump_heads.c b/tools/dump_heads.c new file mode 100644 index 0000000..43250b8 --- /dev/null +++ b/tools/dump_heads.c @@ -0,0 +1,28 @@ +/* dump_heads ...: each crop's six head maps, raw float32, + * to .heads, for comparison with pdq.py. */ +#include +#include +#include "pdnet.h" +int main(int argc, char **argv) { + PdNet *n = pdnet_open(argv[1]); + if (!n) { fprintf(stderr, "cannot open %s\n", argv[1]); return 1; } + const int w = pdnet_width(n), h = pdnet_height(n); + unsigned char *buf = malloc((size_t)w * h * 3 / 2); + PdBox box[64]; + for (int a = 2; a < argc; a++) { + FILE *f = fopen(argv[a], "rb"); + if (!f || fread(buf, 1, (size_t)w * h * 3 / 2, f) != (size_t)w * h * 3 / 2) return 1; + fclose(f); + pdnet_detect(n, buf, w, buf + (size_t)w * h, w, box, 64, 101); + char out[1024]; snprintf(out, sizeof out, "%s.heads", argv[a]); + FILE *o = fopen(out, "wb"); + for (int l = 0; l < 3; l++) + for (int r = 0; r < 2; r++) { + int hh, ww; const float *p = pdnet_head(n, l, r, &hh, &ww); + fwrite(p, sizeof(float), (size_t)hh * ww * (r ? 4 : 1), o); + } + fclose(o); + } + pdnet_close(n); + return 0; +} diff --git a/tools/export_pdnet.py b/tools/export_pdnet.py new file mode 100644 index 0000000..638491e --- /dev/null +++ b/tools/export_pdnet.py @@ -0,0 +1,134 @@ +# export_pdnet.py +# +# The person detector's QDQ graph as the blob pdnet.c runs: the same walk, +# constants and arithmetic as pdq.run(head="float"), which is the reference +# the C engine is checked against bit for bit. +# +# Layout, little-endian, every section 4-byte aligned: +# "MAJPDNT1" u32 version=1 +# u32 in_w in_h f32 in_scale i32 in_zp +# u32 n_tensors, then per tensor u32 h w c +# u32 n_ops, then per op u32 type, and: +# CONV(1): u32 in out k stride pad depthwise cin cout float_out +# i32 in_zp out_zp +# float_out ? f32 scale[cout] : i32 (mult, shift)[cout] +# i32 bias[cout] +# i8 w[...] then pad to 4 -- [cout][cin] for 1x1, +# [cout][k][k][cin] for kxk, [k][k][c] for depthwise +# RESIZE2X(2): u32 in out (nearest, same quantization) +# ADD(3): u32 a b out i32 zp_a zp_b zp_out +# i32 mult_a shift_a mult_b shift_b mult_out shift_out +# u32 n_levels, then per level u32 stride cls_tensor reg_tensor +import struct +import sys + +import numpy as np +import onnx + +sys.path.insert(0, __file__.rsplit("/", 1)[0]) +import pdq # noqa: E402 + +CONV, RESIZE2X, ADD = 1, 2, 3 +ADD_SHIFT = 20 + + +def main(src, dst): + g = pdq.Graph(src) + shapes = {} # tensor name -> (h, w, c) + ids = {} + out = bytearray() + + def tid(name, shape): + if name not in ids: + ids[name] = len(ids) + shapes[name] = shape + return ids[name] + + in_w, in_h = 320, 192 + qi = next(n for n in g.nodes if n.op_type == "QuantizeLinear" and n.input[0] == g.input) + in_scale, in_zp = g.qparams[qi.output[0]] + tid(qi.output[0], (in_h, in_w, 3)) + ops = [] + final = {} + for n in g.nodes: + if n.op_type == "Conv": + src_t, s_in, zp_in = g.dq_source(n.input[0]) + wq, s_w, _ = g.dq_source(n.input[1]) + w = g.init[wq].astype(np.int8) + bias = np.zeros(w.shape[0], np.int32) + if len(n.input) > 2: + bq, _, _ = g.dq_source(n.input[2]) + bias = g.init[bq].astype(np.int32) + a = {x.name: onnx.helper.get_attribute_value(x) for x in n.attribute} + k, st, grp = a["kernel_shape"][0], a["strides"][0], a.get("group", 1) + pad = a.get("pads", [0, 0, 0, 0])[0] + ih, iw, ic = shapes[src_t] + oh, ow = (ih + 2 * pad - k) // st + 1, (iw + 2 * pad - k) // st + 1 + co = w.shape[0] + dw = grp > 1 + real = float(s_in) * s_w.astype(np.float64) + q = next(c for c in g.nodes if n.output[0] in c.input) + dq = next(c for c in g.nodes if q.output[0] in c.input) + is_final = any(o in g.outputs for o in dq.output) + o = tid(q.output[0], (oh, ow, co)) + if is_final: + final[dq.output[0]] = o + zp_out = 0 + else: + s_out, zp_out = g.qparams[q.output[0]] + rec = struct.pack("<10I2i", CONV, ids[src_t], o, k, st, pad, int(dw), ic, co, + int(is_final), int(zp_in), int(zp_out)) + if is_final: + rec += real.astype(np.float32).tobytes() + else: + ms = [pdq.quantize_multiplier(r / s_out) for r in real] + rec += np.array(ms, np.int32).reshape(-1).tobytes() + rec += bias.tobytes() + if dw: + wb = w[:, 0].transpose(1, 2, 0) # k, k, c + elif k == 1: + wb = w[:, :, 0, 0] # co, ci + else: + wb = w.transpose(0, 2, 3, 1) # co, k, k, ci + wb = np.ascontiguousarray(wb).tobytes() + rec += wb + bytes(-len(wb) % 4) + ops.append(rec) + elif n.op_type == "Resize": + src_t, _, _ = g.dq_source(n.input[0]) + q = next(c for c in g.nodes if n.output[0] in c.input) + h, w_, c = shapes[src_t] + ops.append(struct.pack("<3I", RESIZE2X, ids[src_t], tid(q.output[0], (2 * h, 2 * w_, c)))) + elif n.op_type == "Add": + (ta, s_a, zp_a), (tb, s_b, zp_b) = (g.dq_source(i) for i in n.input) + q = next(c for c in g.nodes if n.output[0] in c.input) + s_out, zp_out = g.qparams[q.output[0]] + twice = 2 * max(float(s_a), float(s_b)) + ma = pdq.quantize_multiplier(float(s_a) / twice) + mb = pdq.quantize_multiplier(float(s_b) / twice) + mo = pdq.quantize_multiplier(twice / ((1 << ADD_SHIFT) * s_out)) + assert shapes[ta] == shapes[tb] + o = tid(q.output[0], shapes[ta]) + ops.append(struct.pack("<4I9i", ADD, ids[ta], ids[tb], o, int(zp_a), int(zp_b), + int(zp_out), *ma, *mb, *mo)) + out += b"MAJPDNT1" + struct.pack(" 0 as (int32 mult, shift): m == mult * 2^(shift - 31).""" + if m == 0: + return 0, 0 + mant, exp = np.frexp(m) + q = int(round(mant * (1 << 31))) + if q == 1 << 31: + q //= 2 + exp += 1 + return q, int(exp) + + +def requant(acc, mult, shift): + """acc * mult * 2^(shift - 31), rounded once (half up). + + Not TFLite's MultiplyByQuantizedMultiplier, which rounds twice -- once in + the doubling high multiply, again in the power-of-two divide -- and so + lands one off the exact result for a quarter of negative values at some + shifts. One 64-bit product and one rounding shift agree with exact + rounding except at exact halves; ARMv7 NEON has the 32x32->64 multiply + this needs (vmull_s32).""" + n = (31 - np.asarray(shift, np.int64)) + prod = acc.astype(np.int64) * np.asarray(mult, np.int64) + return (prod + (np.int64(1) << (n - 1))) >> n + + +class Graph: + def __init__(self, path): + m = onnx.load(path) + self.init = {t.name: nh.to_array(t) for t in m.graph.initializer} + for n in m.graph.node: + if n.op_type == "Constant": + self.init[n.output[0]] = nh.to_array(n.attribute[0].t) + self.nodes = [n for n in m.graph.node if n.op_type != "Constant"] + self.prod = {o: n for n in self.nodes for o in n.output} + self.outputs = [o.name for o in m.graph.output] + self.input = m.graph.input[0].name + self.qparams = {} # quantized tensor name -> (scale, zp) + for n in self.nodes: + if n.op_type == "QuantizeLinear": + self.qparams[n.output[0]] = (float(self.init[n.input[1]]), + int(self.init[n.input[2]])) + + def dq_source(self, name): + """A DequantizeLinear's quantized input and its (scale, zp).""" + n = self.prod[name] + assert n.op_type == "DequantizeLinear", (name, n.op_type) + src = n.input[0] + s = self.init[n.input[1]] + zp = self.init[n.input[2]] if len(n.input) > 2 else np.zeros_like(s, np.int32) + return src, s, zp + + +def run(g, x_chw, head="float", keep=None, rq="fixed"): + """x_chw: float32 [3, H, W] as the float model takes it.""" + t = {} # tensor name -> int8 NHWC (H, W, C) for quantized activations + qi = next(n for n in g.nodes if n.op_type == "QuantizeLinear" and n.input[0] == g.input) + s, zp = g.qparams[qi.output[0]] + t[qi.output[0]] = np.clip(np.round(x_chw.transpose(1, 2, 0) / s) + zp, -128, 127).astype(np.int32) + outs = {} + for n in g.nodes: + if n.op_type == "Conv": + src, s_in, zp_in = g.dq_source(n.input[0]) + wq, s_w, zp_w = g.dq_source(n.input[1]) + w = g.init[wq].astype(np.int32) # [Co, Ci/g, k, k] + assert not np.any(zp_w) + bias = np.zeros(w.shape[0], np.int64) + if len(n.input) > 2: + bq, s_b, _ = g.dq_source(n.input[2]) + bias = g.init[bq].astype(np.int64) + a = {x.name: onnx.helper.get_attribute_value(x) for x in n.attribute} + k, st, grp = a["kernel_shape"][0], a["strides"][0], a.get("group", 1) + pads = a.get("pads", [0, 0, 0, 0]) + x = t[src] - int(zp_in) + H, W, C = x.shape + xp = np.zeros((H + pads[0] + pads[2], W + pads[1] + pads[3], C), np.int32) + xp[pads[0]:pads[0] + H, pads[1]:pads[1] + W] = x + Ho = (xp.shape[0] - k) // st + 1 + Wo = (xp.shape[1] - k) // st + 1 + Co = w.shape[0] + acc = np.zeros((Ho, Wo, Co), np.int64) + cpg_in, cpg_out = C // grp, Co // grp + for ky in range(k): + for kx in range(k): + patch = xp[ky:ky + st * Ho:st, kx:kx + st * Wo:st] # Ho, Wo, C + if grp == 1: + acc += patch.astype(np.int64) @ w[:, :, ky, kx].T.astype(np.int64) + elif cpg_in == 1 and cpg_out == 1: + acc += patch.astype(np.int64) * w[:, 0, ky, kx].astype(np.int64) + else: + raise NotImplementedError("grouped conv") + acc += bias + real_scale = float(s_in) * s_w.astype(np.float64) # per output channel + q = next(c for c in g.nodes if n.output[0] in c.input) + final = any(o in g.outputs for o in + next(c for c in g.nodes if q.output[0] in c.input).output) + if final and head == "float": + # the scale as the blob stores it, float32, so the C engine + # can match this exactly + outs[q.output[0]] = acc * real_scale.astype(np.float32).astype(np.float64) + continue + s_out, zp_out = g.qparams[q.output[0]] + if rq == "float": # onnxruntime's way: float, round half to even + y = np.round((acc * real_scale).astype(np.float32) / np.float32(s_out)).astype(np.int64) + zp_out + else: + mults = [quantize_multiplier(r / s_out) for r in real_scale] + mult = np.array([m for m, _ in mults], np.int64) + shift = np.array([e for _, e in mults], np.int64) + y = requant(acc, mult, shift) + zp_out + t[q.output[0]] = np.clip(y, -128, 127).astype(np.int32) + elif n.op_type == "Resize": + src, s_in, zp_in = g.dq_source(n.input[0]) + q = next(c for c in g.nodes if n.output[0] in c.input) + s_out, zp_out = g.qparams[q.output[0]] + assert abs(float(s_in) - s_out) < 1e-9 and int(zp_in) == zp_out + t[q.output[0]] = t[src].repeat(2, 0).repeat(2, 1) # nearest, x2 + elif n.op_type == "Add": + (sa, s_a, zp_a), (sb, s_b, zp_b) = (g.dq_source(i) for i in n.input) + q = next(c for c in g.nodes if n.output[0] in c.input) + s_out, zp_out = g.qparams[q.output[0]] + # TFLite's quantized add: both sides to a shared 2^20 grid + ls = 20 + twice = 2 * max(float(s_a), float(s_b)) + ma, ea = quantize_multiplier(float(s_a) / twice) + mb, eb = quantize_multiplier(float(s_b) / twice) + mo, eo = quantize_multiplier(twice / ((1 << ls) * s_out)) + A = (t[sa] - int(zp_a)).astype(np.int64) << ls + B = (t[sb] - int(zp_b)).astype(np.int64) << ls + y = requant(requant(A, ma, ea) + requant(B, mb, eb), mo, eo) + zp_out + t[q.output[0]] = np.clip(y, -128, 127).astype(np.int32) + if keep is not None: + keep.update(t) + res = [] + for o in g.outputs: + dq = g.prod[o] + if dq.input[0] in outs: + v = outs[dq.input[0]] + else: + s, zp = g.qparams[dq.input[0]] + v = (t[dq.input[0]] - zp) * s + res.append(v.transpose(2, 0, 1).astype(np.float32)) # C, H, W + return res diff --git a/tools/points.py b/tools/points.py new file mode 100644 index 0000000..cd836fc --- /dev/null +++ b/tools/points.py @@ -0,0 +1,100 @@ +# points.py +# +# What majestic's three object-detection presets mean for the person model: +# the score threshold of each, measured on the labelled crops (crops.py) from +# the engine's own detections (dets). A box is right when it overlaps a person +# by IoU 0.5; people marked ignore are neither a hit nor a miss. Matching is +# greedy by score, so one pass gives exact precision and recall at every +# threshold. +# +# balanced the F1-optimal threshold +# fewerFalseAlarms the F0.5 optimum (precision weighted) +# catchEverything the highest threshold with recall >= 0.90, else the +# F2 optimum +# +# Writes {"presets": {preset: {"human": threshold}}} for majestic to read +# beside the model, and a table of what each preset achieves, including the +# false alarms per person-free crop. +import json +import sys +from collections import defaultdict + +import numpy as np + +PRESETS = ["fewerFalseAlarms", "balanced", "catchEverything"] + + +def iou(a, b): + w = min(a[0] + a[2], b[0] + b[2]) - max(a[0], b[0]) + h = min(a[1] + a[3], b[1] + b[3]) - max(a[1], b[1]) + if w <= 0 or h <= 0: + return 0.0 + i = w * h + return i / (a[2] * a[3] + b[2] * b[3] - i) + + +def main(labels_path, dets_path, out_json, out_md): + labels = json.load(open(labels_path)) + dets = defaultdict(list) + for line in open(dets_path): + p = line.split() + if len(p) == 6: + dets[p[0]].append((int(p[5]), [float(v) for v in p[1:5]])) + npos = sum(1 for g in labels.values() for b in g if not b["ignore"]) + empty = [c for c, g in labels.items() if not g] + rec, bg = [], [] + for c, g in labels.items(): + used = [False] * len(g) + for s, box in sorted(dets.get(c, []), key=lambda d: -d[0]): + best, bi = 0.0, -1 + for i, b in enumerate(g): + if used[i] and not b["ignore"]: + continue + o = iou(box, b["box"]) + if o > best: + best, bi = o, i + if best >= 0.5: + if g[bi]["ignore"]: + continue + used[bi] = True + rec.append((s, True)) + else: + rec.append((s, False)) + if not g: + bg.append(s) + s = np.array([r[0] for r in rec]) + hit = np.array([r[1] for r in rec], bool) + bg = np.array(bg) + ts = np.arange(1, 100) + tp = np.array([(hit & (s >= t)).sum() for t in ts], float) + fp = np.array([(~hit & (s >= t)).sum() for t in ts], float) + some = tp + fp > 0 + p = np.divide(tp, tp + fp, out=np.zeros_like(tp), where=some) + r = tp / max(npos, 1) + f = lambda b: (1 + b * b) * p * r / np.maximum(b * b * p + r, 1e-9) + ok = np.nonzero(r >= 0.90)[0] + idx = {"fewerFalseAlarms": int(f(0.5).argmax()), "balanced": int(f(1).argmax()), + "catchEverything": int(ok[-1]) if len(ok) else int(f(2).argmax())} + presets, rows = {}, [] + for name in PRESETS: + i = idx[name] + t = ts[i] / 100 + presets[name] = {"human": round(float(t), 2)} + rows.append(f"| {name} | {t:.2f} | {p[i]:.3f} | {r[i]:.3f} | " + f"{2 * p[i] * r[i] / max(p[i] + r[i], 1e-9):.3f} | " + f"{(bg >= ts[i]).sum() / max(len(empty), 1):.3f} |") + with open(out_json, "w") as o: + json.dump({"presets": presets}, o, indent=1, sort_keys=True) + table = "\n".join([ + f"{len(labels)} crops of COCO val2017 ({len(empty)} with nobody in them), " + f"{npos} people counted.", + "", + "| preset | threshold | precision | recall | F1 | false alarms per person-free crop |", + "|---|---|---|---|---|---|"] + rows) + "\n" + with open(out_md, "w") as o: + o.write(table) + print(table) + + +if __name__ == "__main__": + main(*sys.argv[1:]) From a297ec8cde9f4ad5395eebcc3bf67efe80ee0335 Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin <6576495+widgetii@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:29:07 +0300 Subject: [PATCH 2/2] build: extract COCO fresh and refuse a non-empty crops directory 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. --- scripts/build.sh | 8 +++++--- tools/crops.py | 3 +++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/scripts/build.sh b/scripts/build.sh index 0d36e18..7b777ad 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -20,11 +20,13 @@ cache=$src/.cache mkdir -p "$out" "$cache" bash "$src/scripts/fetch.sh" "$src/coco.txt" "$cache" +# Extracted fresh every build, from the archives just hash-checked: files left +# in a reused output directory are not what was pinned. coco=$out/coco +rm -rf "$coco" "$out/eval" mkdir -p "$coco" -[ -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 +unzip -qo -d "$coco" "$cache/val2017.zip" +unzip -qo -d "$coco" "$cache/annotations_trainval2017.zip" annotations/instances_val2017.json python3 "$src/tools/export_pdnet.py" "$src/models/person/model.onnx" "$out/person.pdnet" diff --git a/tools/crops.py b/tools/crops.py index 9847b0a..e5fbaef 100644 --- a/tools/crops.py +++ b/tools/crops.py @@ -40,6 +40,9 @@ def main(coco, out, n_person=2000, n_bg=1000): for a in ann["annotations"]: if a["category_id"] == 1: people.setdefault(a["image_id"], []).append(a) + # A directory holding crops from another run would mix them in. + if os.path.isdir(f"{out}/crops") and os.listdir(f"{out}/crops"): + sys.exit(f"crops.py: {out}/crops is not empty") os.makedirs(f"{out}/crops", exist_ok=True) labels = {}