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

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

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

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.

Downloads last month
11
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Paper for ftc8565/replitedet-s0