tensor_factory (core)
Top-level exports — the dependency-free primitives:
from tensor_factory import BBox, encode_uint8, decode_uint8, max_error_px
BBox
The canonical bounding box: axis-aligned, normalized xyxy in
[0, 1], top-left origin. Immutable (slotted, no
__setattr__), hashable, and resolution-independent — one annotation flows
unchanged from 480 px training to whatever the camera feeds at inference.
Construct directly only with values you know are valid; otherwise use the constructors that repair input:
| Constructor | From |
|---|---|
BBox.clamped(x1, y1, x2, y2) | normalized coords — clamps to [0,1], fixes ordering |
BBox.from_cxcywh(cx, cy, w, h) | normalized center + size |
BBox.from_pixels(x1, y1, x2, y2, *, width, height) | absolute pixels |
| Member | Returns |
|---|---|
.x1 .y1 .x2 .y2 | the four normalized coords (float) |
.width .height .area | derived extents |
.center | (cx, cy) |
.to_cxcywh() | (cx, cy, w, h) |
.to_pixels(*, width, height) | absolute integer (x1, y1, x2, y2) |
.iou(other) | intersection-over-union in [0, 1] |
from tensor_factory import BBox b = BBox(0.1, 0.1, 0.9, 0.9) b.to_pixels(width=480, height=480) # (48, 48, 432, 432) b.iou(BBox(0.0, 0.0, 0.5, 0.5)) # overlap ratio
codec — the 8-bit contract
A detection is four uint8 values. These convert a BBox to and from that form.
from tensor_factory import encode_uint8, decode_uint8, max_error_px encode_uint8(BBox(0.1, 0.1, 0.9, 0.9)) # (26, 26, 230, 230) max_error_px(480) # ~0.94 px (under the 3px budget)
Why four bytes is enough lives in Internals › the contract.
formats — annotation conversion
tensor_factory.formats converts the canonical BBox to/from COCO, YOLO, and Pascal VOC. Every exporter has a matching importer, so a box survives a round-trip (COCO/YOLO are lossless float; VOC rounds to integer pixels).
| To | From | Form |
|---|---|---|
to_coco_bbox(box, *, width, height) | from_coco_bbox(bbox, *, width, height) | COCO [x, y, w, h] abs px |
to_yolo(box) | from_yolo(cx, cy, w, h) | YOLO normalized (cx, cy, w, h) |
to_voc_box(box, *, width, height) | from_voc_box(voc, *, width, height) | VOC xmin/ymin/xmax/ymax int px |
inference — Detector
CPU inference via onnxruntime, behind the infer extra (lazy-imported, so the
pure-Python core stays dependency-free). The fixed contract: input image is
(1, 3, S, S) float32 in [0, 1] (CHW, RGB); output box
is (1, 4) normalized xyxy; a presence-head model adds a
presence output (one objectness logit).
| Method / attr | Returns |
|---|---|
.detect_box(image) | a BBox |
.detect_uint8(image) | the four-uint8 tuple |
.detect_presence(image) | P(target present) — sigmoid(presence_logit); needs a presence head, else raises |
.preprocess(image) | the (1,3,S,S) float32 input array |
.has_presence | True if the model emits a presence output |
.input_name .output_names .session | the underlying ORT session details |
from PIL import Image from tensor_factory.inference import Detector det = Detector("model.onnx", input_size=480) box = det.detect_box(Image.open("frame.png")) det.detect_uint8(Image.open("frame.png")) # (54, 85, 201, 201)
review — the validation gate
tensor_factory.review is the single source of truth for which labels may
train. Each COCO annotation carries review (a triage decision) and
source (provenance). Only approved is trainable;
a missing review key normalizes to pending — untrusted by default.
| States | Sources |
|---|---|
PENDING · APPROVED · REJECTED | GROUNDINGDINO · HUMAN · SYNTHETIC_GT |
| Function | Returns |
|---|---|
normalize(review) | a known state (unknown/None → PENDING) |
is_trainable(review) | True iff approved |
review_summary(coco) | image + annotation counts per state, plus trainable |
See Internals › the review gate for the rationale.
tensor_factory_mcp.core — serving logic
Framework-free detection logic the MCP tools and the HTTP server both wrap, so all surfaces return byte-identical JSON. Detectors are cached by (model, input_size).
| Function | Returns |
|---|---|
detect(image_path, model_path=None, input_size=480) | the detection dict (norm/pixel/uint8 box, image size, model; present/score for a presence head) |
detect_bytes(data, model_path=None, input_size=480) | same, from raw image bytes (HTTP body) |
model_info(model_path=None, input_size=480) | resolved model, input size, input name, providers |
benchmark(model_path=None, input_size=480, n=100) | fps on a synthetic frame |
resolve_model(model_path) | absolute model path (falls back to the bundled default) |
The HTTP server (tensor_factory_mcp.http_server) exposes make_server(host, port, …) and serve(…) for embedding the endpoint in your own process.
tensor_factory_synth — generation + labeling
Top-level exports (generation needs the gemini extra; the labeler needs gpu):
| Symbol | What |
|---|---|
Generator (Protocol) | generate(prompt, seed, *, size=480) -> GeneratedSample |
MockGenerator | deterministic, torch-free; exact ground-truth boxes |
NanoBananaGenerator | Gemini gemini-2.5-flash-image; optional reference image |
AutoLabeler (Protocol) | label(image, features) -> list[Detection] |
GroundingDinoAutoLabeler | open-vocabulary labeler; resolves device cuda → mps → cpu |
GeneratedSample, Detection | dataclasses carrying provenance + review state |
synth_dataset(generator, prompt, features, *, n, out_dir, …) | end-to-end: generate → label → write COCO |
resolve_device(prefer=None), enable_mps_fallback() | device helpers |
tensor_factory_train — model + training
Needs the train extra (torch).
TinyDetector
Four stride-2 conv blocks → a per-edge heatmap → marginal soft-argmax → normalized
xyxy. With presence=True a single global-pooled objectness
logit head rides alongside, so forward returns (box, presence);
otherwise just the box. learn_gain exposes the learnable soft-argmax inverse-temperature. See
Internals › the model.
fit
Trains on <data_dir>/annotations.coco.json + images and exports an int8
ONNX. require_review (default) trains only approved annotations and refuses an
all-pending dataset; negatives turns the objectness head on and trains it
toward absent (no class); with val_frac the best-scoring checkpoint is exported.
export_onnx
Exports on CPU (opset 17), then onnxruntime dynamic-quantizes weights to QUInt8. Output names are fixed — image in, box (and presence for a presence-head model) out — so the runtime reads each by name regardless of graph order.
data loaders
| Function | Returns |
|---|---|
load_coco_boxes(coco, root, *, require_review=True) | list[(path, BBox)] |
load_coco_labeled(coco, root, *, require_review=True) | (list[(path, BBox, label)], names) |
load_negatives(images_dir) | list[Path] (no review gate — a known absence) |
resolve_device(prefer=None) | training device (cuda → mps → cpu) |
tensor_factory_label — Label Studio
| Symbol | What |
|---|---|
LabelStudioClient(url, token, *, timeout=60.0, transport=None) | REST client: create_project, import_tasks, export_json |
bbox_config(labels=("helicoil",)) | RectangleLabels config XML |
coco_to_tasks(coco, image_url) | COCO → Label Studio tasks with predictions |
ls_export_to_coco(export, *, image_field="image") | LS export → COCO (stamps approved/human) |
http_image_url(base_url), local_storage_url(prefix=…) | URL factories for the round-trip |
COCO boxes are normalized [0, 1]; Label Studio boxes are percentages 0–100. The converters handle the scaling, and image file names are preserved through the round-trip via the URL factories.