replitedet-s0 / README.md
ftc8565's picture
Card: drop performative-honesty phrasing and the ground-truth-boxes note
6cc880b verified
|
Raw
History Blame Contribute Delete
25.9 kB
---
license: apache-2.0
library_name: tf-lite
pipeline_tag: object-detection
tags:
- object-detection
- tflite
- int8
- quantization
- edge
- robotics
- ftc
- limelight
- mobile
- ssd
---
# RepLiteDet-S0: a compact FTC 2026-2027 BioBuzz Pollen Detector Model for the Limelight 3A
A drop-in 8-bit TFLite model for FTC teams working in season 2026-2027 BioBuzz to detect the game element Pollen. Built by FTC 8565. The complete training scripts follow in the next release.
![architecture](charts/replitedet_dynamic_flow_v2.gif)
*Backbone, then FPN-Lite fusion, then a shared SSD head, then decode and NMS. The diagram
shows the training graph with 12,750 anchors. The exported detector drops the P5 and P6
heads and runs P3 + P4, which is 12,000 anchors.*
## Brief Comparison to the LimeLight3A Default Model
| | RepLiteDet-S0 (ours) | SSD-MNV2, our pipeline | SSD-MNV2, Limelight online trainer |
|---|---|---|---|
| mAP@0.5 | **0.9365** | 0.8516 | 0.4605 |
| mAP@0.75 | **0.7581** | 0.5746 | 0.3044 |
| recall `<10 px` | **0.8213** | 0.6823 | 0.3285 |
| false positives | **410** | 845 | 863 |
| size | **0.64 MB** | 4.96 MB | 4.96 MB |
| latency, **on a Limelight 3A** | **52.8 ms** | 86.5 ms | 86.5 ms* |
**We get +10.0% mAP@0.5 and +20.4% recall on objects under 10 px compared to the same
network trained the normal way, in 0.64 MB instead of 4.96 MB, and 39% faster on a real
Limelight 3A.** Against what the standard cloud training page gives you from the same
dataset and the same budget, it is **+103% mAP@0.5 and 2.5x the recall under 10 px**. Ours
also runs at 320x320 while both baselines run at 300x300, so it is doing more work per
frame, not less.
Our weakest spot is box tightness on large close-up objects. Those numbers are further
down.
## Side-by-side, on real frames
**Read the boxes like this:**
- **Green** is a hit. The box matches a real object at IoU 0.5 or better.
- **Yellow** is a miss. The ball is really there and the model did not find it at its own
threshold.
- **Red** is a false positive. The box matches no real object, either because it is on the
wrong thing entirely, or because it is on the right thing but sits too far off to pass
the IoU test.
![contrast](panels/panel_contrast.png)
**These frames are selected, and the rule is printed on the figure**: every frame where we
find at least 2 more objects than the retrained baseline does. Across the four, **19 of 20**
found by us, **9** by the baseline, **1** by the online trainer. It shows what the difference
looks like when it shows up. The tables above are what it is worth on average. Same three
models, each read at its own threshold.
## Deploying to a Limelight 3A
Upload the 8-bit `.tflite` and the labels file through the Limelight web interface and select the **CPU** runtime, per Limelight's own detector documentation. Both files are required; a model without its labels file will not run.
## What these numbers mean
The camera feeds a 320x320 image. A ball across the field is under 10 px wide; one at intake
range is 50 px or more. Every metric below is measured on 3,546 real objects.
**"Overlap" always means shared area divided by combined area, which is called IoU.** A box
twice the size of the ball only scores 0.5, even though the whole ball is inside it. Too big
costs you as much as too small.
| metric | what it measures | on the robot |
|---|---|---|
| **mAP@0.5** | did it find the ball, with a box overlapping the real one by at least 50% | "there is a ball over there", enough to drive toward it |
| **mAP@0.75** | same thing at a 75% bar, so a loose box now counts as a miss | enough to line up an intake, not just point the robot |
| **mAP@[.5:.95]** | the average across ten strictness levels from 50% to 95% | one number for "finds it AND boxes it well" |
| **mean best IoU** | for each real ball, how well the best box we drew lines up, averaged over **every** ball, so one we never found counts as about 0 | box size is how you estimate distance, so a loose box is a wrong distance |
| **TP / FP** | correct detections / boxes matching no real ball | FP is the robot reacting to something that is not there |
| **best-F1** | the balance of "found most of them" against "was not wrong often" | the single number if you only get one |
| **own threshold** | the confidence cutoff where that model scores best | every model has a different one, so comparing two models at the same cutoff is unfair |
| **NMS** | non-maximum suppression. The model scores thousands of candidate boxes, and one real ball lights up several neighbours at once, so NMS keeps the highest-scoring box and deletes anything overlapping it past a threshold | set too loose it stacks three boxes on one ball, set too tight it merges two balls into one. Ours ships at 0.35 |
| **recall** | of the balls that really are there, the fraction the model finds. It says nothing about box quality, so a box that barely scrapes the 50% bar counts the same as a perfect one | a miss is a ball your robot never knows about |
| **recall by band** | the same number, computed separately for each object-size range | size is distance, so `<10 px` is the far half of the field and `>45 px` is intake range |
### Worked example: the two `<10 px` numbers
The eval pool holds **554 balls under 10 px**, which is the far half of the field. Two numbers describe how we do on them, and they answer different questions.
**recall 0.8213 = how many we found.** Of those 554 balls, the model produced a box that
counted as a hit on **455**, and missed **99**. It says nothing about box quality: a box
that scrapes past the 50% bar counts the same as a perfect one. This is the number that
decides whether the robot knows a far ball exists at all.
**mean best IoU 0.6669 = how well we boxed them, averaged over all 554.** For each ball we
take the best box we drew and measure the overlap, then average. That average *includes*
the 99 we never found, which count as about 0, so this one number blends finding with
boxing and the misses drag it down. Back out the 82% we did find and the boxes on those
sit near **0.81**.
Read together: **we find 4 far balls in 5, and we box the ones we find well.** The same
pair for the retrained baseline is 378 of 554 found, recall 0.6823, at mean IoU 0.5716.
Backing out its recall the same way gives about 0.84 on what it does find. **Almost the
whole gap in that band is finding the ball, not boxing it.**
So `<10 px` recall **0.8213 vs 0.3285** means: of the balls far enough away to be under
10 pixels, we find about **4 in 5** and the cloud-trained model finds about **1 in 3**. In
a match that is the difference between seeing the far side of the field during auto and
driving in blind.
## What you get
| file | what it is | release road map |
|---|---|---|
| `replitedet_s0_int8.tflite` | the model. 0.64 MB, full-INT8, uploads straight to a Limelight 3A | **available today** |
| `pollen_labels.txt` | the labels file the device requires beside it | **available today** |
| `README.md` | every number here, and how it was measured | **available today** |
| `ckpt-*` / `checkpoint` | the float checkpoint, so you can retrain on YOUR data | **available today** |
| `*_vp2.config` | the exact training recipe we used, nothing held back | **available today** |
| training scripts and pipeline | our training pipeline, with the tuned learning-rate schedule | *releasing later* |
## Design
We designed this backwards from one target: an FTC robot looking at a field through a
Limelight 3A.
- **43.9%** of training objects are under 18 px, so the neck fuses high-resolution early
features with deeper ones. That is where the `<10 px` numbers come from.
- The device runs **full-integer INT8 only**. No float, float16 or weight-only fallback.
- The head is a **stock SSD head** with `TFLite_Detection_PostProcess` built into the
graph, so the export is drop-in. Upload the tflite and the labels, pick the CPU runtime,
done. A more modern head would have scored better and would not have been drop-in.
- **0.86 M parameters.** We pre-train on COCO using *our* architecture and
**one class**, relabelling every annotation as a single `object`. That keeps what
pre-training is good for and carries none of the 80-class head width, which is a big part
of **0.64 MB against 5.20 MB**.
## Where this model stands
The baseline is the network the Limelight training route gives you, plain SSD MobileNetV2
300x300 at alpha 1.0, **retrained by us** from its own COCO checkpoint, on our data, for the
same 24,000 steps, with the learning rate scaled correctly for the batch size and its
exported NMS threshold swept the same way ours was. The third comparison is that same
route's own trainer, run on the same data at the same budget.
### The baseline is the same network, and we checked
Same architecture, our rebuild against LimeLight Trainer download:
| | our retrained baseline | the Limelight-trained artifact |
|---|---|---|
| file size | **5,202,224 bytes** | **5,202,224 bytes** |
| CONV_2D / DEPTHWISE_CONV_2D | 55 / 17 | 55 / 17 |
| ADD / RESHAPE / CONCATENATION | 10 / 13 / 2 | 10 / 13 / 2 |
| QUANTIZE / DEQUANTIZE / LOGISTIC | 1 / 2 / 1 | 1 / 2 / 1 |
| `TFLite_Detection_PostProcess` | 1 | 1 |
Same file size to the byte, same operator histogram. It is plain SSD MobileNetV2 300x300 at
alpha 1.0, six levels, 1,917 anchors, with a 1x1 convolutional box predictor. So the
architecture is verified. Their training *recipe* is not visible to us, and that is what the
three models below separate.
### Separating the architecture from the training pipeline
A better network, or the same network trained better? Three models, one variable each.
| model | architecture | trained by | batch | steps |
|---|---|---|---|---|
| **RepLiteDet-S0** (this release) | RepLiteDet-S0 | our pipeline | 8 | 12k / 24k |
| SSD-MobileNetV2 300 | SSD-MobileNetV2 300 | our pipeline | 8 | 12k / 24k |
| SSD-MobileNetV2 300 | SSD-MobileNetV2 300 | **the Limelight online trainer** | 16 | 12k / 24k |
* **RepLiteDet-S0 vs SSD-MobileNetV2 in our pipeline** isolates the architecture.
* **SSD-MobileNetV2 in our pipeline vs the same network from the Limelight online trainer**
isolates the training pipeline. That one is confounded with batch 16 against our 8, which
favours the online trainer, since at equal steps batch 16 sees twice the images.
**SSD-MobileNetV2 300, trained two ways.** Same network, same data, **24,000 steps both
sides** (the online-trainer run is job `eb8961b1`, its log recording `Steps: 24000, Batch
size: 16`), each read at its own best-F1 threshold:
![the Limelight online trainer run](charts/24k-limelight-trained.png)
*The online-trainer run as configured: 24,000 steps, target Coral / CPU, variant **Default**
and we changed nothing except the step count. 141 minutes on their H100. The
trainer's own advice is on the same screen: 4,000 steps for testing, 20,000 for robot use.*
| | mAP@0.5 | mAP@0.75 | mAP@[.5:.95] | best-F1 | mean IoU | TP | FP |
|---|---|---|---|---|---|---|---|
| trained by **our pipeline** | **0.8516** | **0.5746** | **0.5426** | **0.8531** | **0.7849** | **3266** | 845 |
| trained by the **Limelight online trainer** | 0.4605 | 0.3044 | 0.2875 | 0.5092 | 0.5244 | 1506 | 863 |
**+85% mAP@0.5 from the training pipeline alone**, with the confound against us: batch 16
sees twice the images ours does at equal steps. The online-trainer model finds 1,506 of
3,546 objects to our 3,266.
Neither pipeline gains from the second 12k. The online trainer at 12k -> 24k: mAP@0.5 0.4509
-> 0.4605 but best-F1 0.5152 -> 0.5092 and TP 1758 -> 1506; it becomes more conservative, not
better. One axis it wins outright: **12 spurious detections on 118 empty frames, against
RepLiteDet-S0's 17 and 90 for the same network in our pipeline.** It hallucinates least
because it detects least.
### By source: the two that carry the weight
Per source, not averaged: `pollen_clean` is small-object-heavy, `clement_pdp` is the easier
one, and a mean across them would read as uncertainty when it is really the gap between two
distributions. `pollenv1` (153 GT, 4.3%, our own smoke-test set) is reported further down and
included in the pooled numbers, but is too small to head a table.
| source | GT | ours mAP@0.5 | ours mAP95 | baseline mAP@0.5 | baseline mAP95 |
|---|---|---|---|---|---|
| `clement_pdp` | 1131 | 0.9910 | 0.8482 | 0.9578 | 0.7552 |
| `pollen_clean` | 2262 | 0.8985 | 0.5579 | 0.7961 | 0.4408 |
### Full pool (all 3,546 GT), each model at its OWN best-F1 threshold
| model | own thr (n=1) | mAP@0.5 | mAP@[.5:.95] | best-F1 | mean IoU | FP |
|---|---|---|---|---|---|---|
| **RepLiteDet-S0 (ours)** | 0.2 | 0.9365 | 0.6694 | 0.9029 | 0.8314 | 410 |
| SSD-MobileNetV2 300 (baseline) | 0.05 | 0.8516 | 0.5426 | 0.8531 | 0.7849 | 845 |
| SSD-MobileNetV2 300, Limelight online trainer | 0.2 | 0.4605 | 0.2875 | 0.5092 | 0.5244 | 863 |
### By object size: the reason this model exists
Size is distance: `<10 px` is the far half of the field, `>45 px` is intake range.
**Recall: the fraction of objects in that band the model finds.**
| band | GT | ours | SSD-MNV2, our pipeline | SSD-MNV2, LL online trainer |
|---|---|---|---|---|
| `<10` | 554 | **0.8213** | 0.6823 | 0.3285 |
| `10-18` | 1001 | **0.9850** | 0.9351 | 0.3487 |
| `18-28` | 495 | **0.9960** | 0.9758 | 0.8121 |
| `28-45` | 654 | **0.9908** | 0.9786 | 0.7003 |
| `>45` | 842 | **0.9846** | 0.9857 | 0.9276 |
**Mean best IoU: how well the box lines up, averaged over every object in the band. A
missed one counts as about 0, so this blends finding with boxing.**
| band | ours | SSD-MNV2, our pipeline | SSD-MNV2, LL online trainer |
|---|---|---|---|
| `<10` | **0.6669** | 0.5716 | 0.3209 |
| `10-18` | **0.8164** | 0.7536 | 0.2575 |
| `18-28` | **0.9009** | 0.8549 | 0.6858 |
| `28-45` | **0.9076** | 0.8752 | 0.6054 |
| `>45` | **0.8573** | 0.8512 | 0.8176 |
We lead every band on both channels except `>45 px` recall, where the baseline is ahead by
0.0011, which is inside noise, and it is the band we call out as our open problem.
### Per source, including the one we exclude from the headline
| source | GT | RepLiteDet-S0 | SSD-MNV2, our pipeline | SSD-MNV2, LL online trainer |
|---|---|---|---|---|
| `clement_pdp` | 1131 | 0.9910 | 0.9578 | 0.5389 |
| `pollen_clean` | 2262 | 0.8985 | 0.7961 | 0.4289 |
| `pollenv1` *(smoke-test set, 4.3% of GT)* | 153 | 0.9894 | 0.9034 | 0.4823 |
`pollenv1` is an early set we collected ourselves to smoke-test the pipeline. At 153 of 3,546 GT (4.3%) it is too small to support a claim on its own, so we report it and keep it out of the headline, but it stays in the pooled numbers.
## Context: how this compares to YOLO
Context, not a claim. **At float, YOLO is the stronger detector**, 0.85 mAP@[.5:.95]
against our 0.67, and that is the bar we aim at. We list it because it is the obvious thing
a team reaches for.
| model (same data, same 24k budget, same eval harness) | mAP@0.5 | mAP@0.75 | mAP@[.5:.95] |
|---|---|---|---|
| yolov8n, **float** | 0.9937 | 0.9523 | 0.8465 |
| yolo11n, **float** | 0.9926 | 0.9521 | 0.8455 |
| yolov8n, INT8 TFLite | 0.8640 | 0.5422 | 0.5158 |
| yolo11n, INT8 TFLite | 0.8396 | 0.4672 | 0.4699 |
| **this model, INT8 TFLite** | 0.9365 | 0.7581 | 0.6694 |
**A Limelight 3A cannot run it.** The device accepts only full-integer INT8 with a `uint8`
NHWC input. Float32, float16 and weight-only (`w8a32`/`w8a16`) are all rejected at load. A
stock YOLO export also emits one `[1, 4+nc, N]` tensor where the device needs 3-4 separate
float32 outputs; the INT8 rows above required a custom export wrapper we wrote.
Through the one mode the device permits, YOLO loses **0.33-0.38 mAP@[.5:.95]**, and the loss
is box tightness, not detection. It finds the same objects with looser boxes, and the
confidences get squashed into roughly [0, 0.5]. Others report the same thing: YOLOv8
n/s/m/l/x lose **6.1-7.5 mAP points** and
YOLO11 n/s/m/l/x **6.6-8.7** under LiteRT full-integer INT8, against 0.0-0.2 for
dynamic-range:
> Ozturk et al., *Benchmarking Quantized YOLO Models for Real-Time Object Detection on
> Edge Devices*, arXiv:2511.13453. https://arxiv.org/pdf/2511.13453
Same weights, four export widths, identical calibration:
| yolov8n, same weights | mAP@0.5 | mAP@0.75 | box precision @320px |
|---|---|---|---|
| float32 | 0.9937 | 0.9523 | 0.0000018 px |
| int8 weights, float activations (`w8a32`) | 0.9940 | 0.9189 | 0.0000003 px |
| int8 weights, int16 activations (`w8a16`) | 0.9936 | 0.9329 | 0.0114 px |
| **full-integer int8 (the only mode this device runs)** | 0.8640 | **0.5422** | **1.584 px** |
The last column is the mechanism. YOLO packs box coordinates and class scores into **one
output tensor sharing a single 8-bit scale**; at full-integer INT8 that scale is worth
**1.58 px of box resolution** at 320 px input, on a dataset whose median object is under
18 px. It also caps every confidence at 0.4998. Widen the activations and the damage is
gone. Those are the modes the device rejects.
> Two limits on these rows: (1) it is a property of the *TFLite full-integer export path*,
> not of YOLO. The same architectures quantize near-losslessly on Hailo (-1.6%), TensorRT
> (-0.05) and OpenVINO (-2.7%); (2) known mitigations exist (splitting the head's outputs,
> excluding the score sigmoid) and we did not implement them, so a better INT8 YOLO than this
> one certainly exists.
## Latency
**Measured on a Limelight 3A**, as a one-minute running average on the device:
| model | input | on device | size |
|---|---|---|---|
| **RepLiteDet-S0 (ours)** | 320x320 | **52.8 ms** | **0.64 MB** |
| SSD-MobileNetV2 300 | 300x300 | 86.5 ms | 4.96 MB |
**39% faster on the device, while running a larger input**: 320x320 against their 300x300,
so it is doing more work per frame, not less.
*The two SSD-MobileNetV2 rows in the headline table are the **same network**; the device
figure was measured once for that architecture and applies to both.*
## False positives, broken down
**410 false positives** across 3546 objects, at its own best-F1 threshold. The three kinds behave very differently on a robot:
| kind | count | share | what it actually is |
|---|---|---|---|
| **localization** | 319 | 78% | found a REAL object, but the box is too loose to count as a hit (IoU < 0.5) |
| background | 91 | 22% | a detection where there is no object, a genuine hallucination |
| duplicate | 0 | 0% | a second box on an object already detected |
Duplicates are eliminated by the exported NMS threshold, not by training. Hallucinations are
the smallest category:
### Spurious detections on empty frames
Images containing no objects at all, which isolates hallucination:
| model | false positives | per empty image |
|---|---|---|
| **this model** | 17 | **0.1441** |
| SSD-MobileNetV2 300, our pipeline | 90 | **0.7627** |
| SSD-MNV2 300, Limelight online trainer | 12 | **0.1017** |
Fewer than either comparison model. This is the number that predicts how often a robot acts
on nothing.
### Localization FP is the open problem
Most of our false positives are loose boxes on real objects, and that share has **not**
improved across milestones while duplicates went to zero and background FP fell. Same deficit
shows up as mAP@0.75 and as the weakness on very large close-up objects.
Cause is architectural, not a training shortfall: the anchor ladder tops out near **181 px**,
so larger objects have no anchor that fits. **Fixing the ladder is the next release's main
objective.**
## The data this was trained and measured on
Not published. The distribution is, so you can judge transfer to your field and camera.
| split | images | ground-truth boxes | negative images |
|---|---|---|---|
| train | 1,931 | 4,352 | 289 |
| valid | 670 | 1,953 | 70 |
| test | 618 | 1,593 | 59 |
| **evaluation pool (valid+test)** | **1,288** | **3,546** | **129** |
Every number in this card is measured on pooled **valid+test**. Size = shorter box side at a
320x320 reference:
| size band | GT boxes in eval | share |
|---|---|---|
| `<10 px` | 554 | 15.6% |
| `10-18 px` | 1001 | 28.2% |
| `18-28 px` | 495 | 14.0% |
| `28-45 px` | 654 | 18.4% |
| `>45 px` | 842 | 23.7% |
Training set: mean 39.3 px, median 23.4 px, **43.9% under 18 px**. That is why this card
reports recall per band instead of pooled.
| source | eval GT | what it is |
|---|---|---|
| `clement_pdp` | 1,131 | the easier source: larger objects, median ~31 px |
| `pollen_clean` | 2,262 | the hard one: median ~13 px, ~26% under 10 px |
| `pollenv1` | 153 | an early set we collected ourselves to smoke-test the pipeline |
**Leakage check.** Video-derived, so split in chunks with guard frames dropped at
boundaries: **0 of 670** valid and **0 of 618** test images collide with training at a
perceptual-hash distance <= 3.
## Limits
- **Latency is a one-minute running average measured on a Limelight 3A**, taken with the vision pipeline running as it normally would, so it is what a robot actually sees rather than an isolated benchmark figure.
- **Training data not published.** Reproducible in shape, not in absolute value.
- **Single class.** Multi-class retraining needs `num_classes` changed and a re-export.
- **We slightly lose on very large, close-up objects.** `>45 px` recall 0.9846 vs the baseline's
0.9857, that is the only band it leads. Its ladder runs six levels to 1x1; ours tops out near
181 px, so an object filling the view has no anchor that fits. Fixing in next release.
- **Every comparison here is 24,000 training steps on the same images**, each model read at its own best-F1 threshold. The Limelight online trainer runs at batch 16 against our 8, which favours it.
- **Post-processing was tuned, and the tuning costs band recall.** The exported NMS IoU is
**0.35**, swept on the eval pool. Against 0.45 it wins on everything pooled: mAP@0.5
0.9365 vs 0.9311, best-F1 0.9029 vs 0.8837, FP 410 vs 508. It pays for that
in two size bands, `<10px` recall 0.8213 vs 0.8321 and `>45px` 0.9846 vs 0.9964, because
pollen occurs in crowded clusters that tighter suppression merges. If your scene is
cluster-heavy, 0.45 is a one-flag re-export with no retraining. Ask us, or do it yourself once the export path ships.
## Where this goes
For FTC 2026-2027 BioBuzz game element `Pollen` on a Limelight 3A: small enough to leave headroom, faster than the
alternatives measured here, more accurate than the same network trained conventionally, and
drop-in for the detector pipeline you already have. Box tightness on large objects is the
open axis and the next release's target.
Built to be retuned, and the checkpoint plus training pipeline are the next thing we ship.
If you run this one on your own hardware, we would like to hear what you measure.
## Retraining on your own data
The float checkpoint and the exact config are here. The training pipeline we used is not
here yet, and it follows in the next release. What that means in practice:
**This is not a stock TFOD architecture.** The config declares a feature extractor
`ssd_replitedet_s0_fpn_keras`, and the anchor generator is selected by the `_vp2` filename
suffix. Neither exists in a stock Object Detection API install, so `model_builder.build` on
this config fails with an unknown-feature-extractor error until our code is on the path.
If you would rather not wait, everything you need to write your own trainer is published:
the config carries the full recipe, the checkpoint carries the layer names and shapes, and
the anchor geometry, which is the one thing you could NOT work out from either of those,
is written out below.
### The anchor geometry
The config's `multiscale_anchor_generator` block declares 6 anchors per location, but our
generator overrides where they sit. Build stock anchors from that proto and you get a
silently worse model, because the head weights were trained against these:
| level | stride | grid | 6 anchors per cell, as (size px @ offset within the cell) |
|---|---|---|---|
| P3 | 8 | 40x40 | 16 @ (2,2), 16 @ (6,2), 16 @ (2,6), 16 @ (6,6), 32 @ (4,4), 45 @ (4,4) |
| P4 | 16 | 20x20 | 64, 90.5, 90.5, 128, 128, 181, all square and centred at (8,8) |
| P5 | 32 | 10x10 | 128, 181, 181, 256, 256, 362, centred (head dropped at export) |
| P6 | 64 | 5x5 | 256, 362, 362, 512, 512, 724, centred (head dropped at export) |
Sizes are square, in pixels at the 320x320 input. P4-P6 are `base x {1, sqrt2, 2} x
{1, sqrt2}` with bases 64 / 128 / 256, which is why 90.5 and 128 each appear twice. The four
shifted 16 px anchors at P3 are the **virtual P2**: they give sub-cell coverage at stride 8
without paying for a stride-4 level.
The **shipped detector keeps P3 + P4 only**, which is 9,600 + 2,400 = **12,000 anchors**.
The P5
and P6 heads are dropped at export; the training graph has all four levels and 12,750.
Two things that will cost you a day if you miss them:
1. **Keep the config filename ending in `_vp2.config`.** Once our pipeline is out, the
anchor generator is selected by filename. Rename it and you train stock anchors against
a vP2 checkpoint, with no error message and a large accuracy loss.
2. **`fine_tune_checkpoint_type: full`** loads the detection heads too. `detection` loads
only the backbone and leaves the heads random, which is correct if you change the number
of
classes, wrong if you do not.
## License
**Apache License 2.0**, see `LICENSE`. This covers the model weights, the
TFLite export and the training config shipped here. It does not cover the training data,
which is not published, nor the third-party baseline model referenced for comparison, which
is not redistributed here.
Models produced by the Limelight online trainer are **not** redistributed here. Train your own if you want to reproduce that row.