tensor·factory

Contributor guide

Dataset & annotation

What makes an ideal helicoil training set, and exactly how to label it so it trains a sharp detector. This is the guide for feeding your own images into the dataset — image requirements, box conventions, the label schema, and the review gate. It deliberately stops at the labeled dataset; running training on it is the pipeline and CLI.

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.

Out of scope

How training works — loss, quantization, the soft-argmax head — lives in Internals; the commands to run it are in the CLI and pipeline. You never touch those to contribute data.

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 statePresenceSeatingDamage classVerdict
flush_passpresentcorrectokpass
slightly_recessedpresentover (too deep)okfail
slightly_proudpresentunder (too high)okfail
cross_threadedpresentunder / off-axiscross_threadedfail
damaged_coilpresentanydeformedfail
missingabsentfail (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.
Seed set you can label today

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.

Tight, not generous

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:

reviewMeaningTrains?
pendingawaiting a human — the default for any auto-label, and for a missing review keyno
approveda human validated it (or it is exact synthetic ground truth)yes
rejectedreviewed and discardedno

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".
  • triage reports 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.
Prev← The pipeline NextLive demo →