| --- |
| 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. |
|
|
|  |
|
|
| *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. |
|
|
|  |
|
|
| **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 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. |
|
|
|
|