What this covers
A detector is only as good as the boxes you draw. This page is about producing those boxes well: which images to capture, how to frame and crop them, and how to annotate and validate them so they pass the training gate. If you have a folder of helicoil photos and want them to count, start here.
What you are labeling
There are two detectors, and they want two different label depths:
-
Basic detection (shipping). One box per image
around the insert, class
helicoil— plus negatives (images with no insert) so the model can say absent. This is all the shipping model needs. - Manifestation detection (next, label-blocked). The same box, plus a state: how the insert is seated and whether it is damaged. This is the model that can't train yet — precisely because these state labels don't exist. Producing them is the whole point of contributing.
The manifestation state splits into two independent axes — a seating measurement and a damage class — because "too deep" and "cross-threaded" are orthogonal. Each inspection state maps onto them:
| Inspection state | Presence | Seating | Damage class | Verdict |
|---|---|---|---|---|
flush_pass | present | correct | ok | pass |
slightly_recessed | present | over (too deep) | ok | fail |
slightly_proud | present | under (too high) | ok | fail |
cross_threaded | present | under / off-axis | cross_threaded | fail |
damaged_coil | present | any | deformed | fail |
missing | absent | — | — | fail (no box) |
Two damage classes the model defines — broken_tang and burr
— have no reliable synthetic source (text-to-image renders them as a spring),
so they need real photos. Flagging that gap is itself a useful
contribution.
The ideal training set
Aim for breadth, balance, and box-size spread — in that order. A thousand near-identical centered passes teach the model almost nothing; a few hundred genuinely varied frames teach it a lot.
Composition
- Hundreds per state, not dozens. Each manifestation state needs enough examples to learn from — target ~150–300 each, and keep them roughly balanced. A state with 10 examples will be ignored or memorized, never learned.
- ~1 negative for every 2–3 positives. Empty tapped holes, bare bosses, machined features that are not inserts. Without them the model emits a box on every frame. See Negatives.
- Spread the box sizes. This is the single biggest lever for localization. Shoot the same insert filling the frame and small in a wide field. If every box is ~40% of the frame, the model never learns scale.
- Vary everything that isn't the insert: angle (straight-down, oblique, grazing), material (aluminum, anodized, steel), finish and grime, lighting (even, harsh single-LED, dim microscope), thread size (M3/M6/M10), and defect placement. Variation the model should be invariant to must appear in training.
- Realistic, not pristine. Worn, gritty, uneven light, slightly soft focus. Clean studio renders bias the model toward a look it won't see in the shop.
The repo ships a generator that produces this exact taxonomy. It writes a batch of
photoreal inspection frames plus a manifest.json recording each
intended state — a ready-made pile to practice the annotation loop on:
# 48 photoreal samples across all 6 states -> a gitignored dataset dir
uv run --with google-genai python \
packages/tensor-factory-synth/scripts/gen_samples.py \
--n 48 --out examples/helicoils/images/synth_manifestations
Needs a GEMINI_API_KEY. Filenames are neutral
(sample_NN.png) so you can review the boxes blind; the
intended state for each lives in the manifest, not the pixels. Full prompt catalog:
examples/helicoils/SAMPLES.md.
Image requirements
For each image you feed in:
- Format:
.png,.jpg,.jpeg,.webp, or.bmp. - Resolution: ≥ 480 px on the short side (the model's input). Bigger is fine — it's downscaled. Don't upscale a tiny crop to fake it.
- One insert per frame. The detector is single-object: it labels the first annotation per image. Two inserts in one frame = an ambiguous label. Crop to one.
- The insert is sharp and unambiguous. If you can't tell where the coil ends, neither can the model. Soft background is fine and realistic; the insert itself should be in focus.
- Neutral filenames (
frame_0007.png), so the verdict lives in the label, not the filename — that keeps review honest.
The annotation process
1 — Draw the box on the insert, not the boss
The biggest quality lever, and the most common mistake. The box is the coiled insert in the bore — not the counterbore, chamfer, or the whole machined boss around it. Loose boxes that bound the boss are exactly what caps the current model's localization. Crop tight to the visible coil, consistently, every time. One box per image.
When unsure where the edge is, err tighter. A consistently tight box trains sharper localization than a generous one — and consistency matters more than any single box being perfect.
2 — Add the label
For basic detection, the class is helicoil. For manifestations, label the
state from the schema table —
the seating (under / correct / over) and the damage class. Describe only what you
see in the seating and coil; the pass/fail verdict is derived from the state,
not annotated directly.
3 — Where the labels live
Annotations are COCO
in <dataset>/annotations.coco.json, with the images alongside. Each
box is bbox: [x, y, w, h] in pixels, plus two keys the
training gate reads:
{
"images": [{ "id": 1, "file_name": "frame_0007.png", "width": 480, "height": 480 }],
"annotations": [{
"id": 1, "image_id": 1, "category_id": 1,
"bbox": [54, 85, 147, 116], // x, y, w, h in pixels — tight on the insert
"review": "approved", // pending | approved | rejected
"source": "human" // human | groundingdino | synthetic_gt
}],
"categories": [{ "id": 1, "name": "helicoil" }]
}
4 — Pass the review gate
Only review: "approved" annotations ever enter training. The states:
| review | Meaning | Trains? |
|---|---|---|
pending | awaiting a human — the default for any auto-label, and for a missing review key | no |
approved | a human validated it (or it is exact synthetic ground truth) | yes |
rejected | reviewed and discarded | no |
The rule is untrusted by default: an unmarked or auto-generated box
is pending until a person approves it. Don't approve a box you didn't
actually look at — a wrong "approved" is worse than an honest "pending".
5 — Label in the loop (optional but recommended)
tensor-factory-label drives Label
Studio: push your dataset in (with any auto-label predictions pre-drawn), correct
the boxes, pull the approved result back to COCO.
# point at your Label Studio (or set LABEL_STUDIO_URL / LABEL_STUDIO_API_KEY) uv run tensor-factory-label push --data examples/helicoils/images/synth_manifestations \ --title helicoils --token "$LABEL_STUDIO_API_KEY" # ...correct boxes in the UI, mark them done... uv run tensor-factory-label pull --project 7 \ --out examples/helicoils/images/synth_manifestations/annotations.coco.json # check how many are validated and would train uv run tensor-factory-synth triage --data examples/helicoils/images/synth_manifestations
Prefer hand-editing the JSON for a few boxes? That's fine too — set
review yourself. Label Studio just makes the push → correct → pull loop
faster at volume.
Negatives — teaching absent
A negative is an image with no insert: an empty tapped hole, a bare boss, a machined feature that looks vaguely like one but isn't. They carry no box and no annotation — a negative is trainable by construction (a known absence needs no review).
Drop them in their own directory; the loader scans the folder (or its
images/ subfolder) directly:
examples/helicoils/images/negatives_pool/
images/
empty_hole_001.png
bare_boss_014.png
... # no annotations.coco.json needed
Make negatives hard: the empty hole that most resembles a seated insert is
worth ten blank fields of metal. The synthetic generator's missing state
produces these, or use gen_negatives.py.
Quality bar & common mistakes
- Box the insert, not the boss. The #1 error and the #1 lever.
- Be consistent. Uniform tightness across the set beats any one perfect box.
- One insert per image. Crop multi-insert frames down to one.
- Don't rubber-stamp. Approving an auto-label you didn't check poisons the set silently.
- Balance the states. A rare state with a handful of examples is noise, not signal.
- Spread box sizes and angles. All-centered, all-same-scale data caps localization.
- No near-duplicates. Ten frames of one part from one angle count as roughly one example.
- Keep it real. Grime, wear, uneven light — match the shop, not a render.
Before you submit — checklist
- ☐ Every positive has exactly one box, tight on the insert.
- ☐ Boxes are consistent in how tight they crop.
- ☐ Manifestation labels follow the schema (seating + damage class).
- ☐ Negatives are present, hard, and box-free.
- ☐ States are roughly balanced; box sizes and angles vary.
- ☐ Every annotation you want trained is
review: "approved". - ☐
triagereports the trainable count you expect.
Where the data goes next
- The pipeline — generate, auto-label, review, and train end to end (auto-labeling pre-draws boxes you then approve here).
- CLI reference — the
tensor-factory-synth,-label, and training commands. - Internals — the review gate and why only approved labels train.