Files
julian f7a262ac9a
Build & push images / images (push) Successful in 6m31s
feat(trainer): phase-B body-type classifier — trainer job on the collector host + the classifier stage on the booth
apps/trainer (parking-trainer): inspect / train / evaluate / publish. Reads the wash
collector's SQLite + crops read-only off its volume; time split (validation = newest
slice); thin classes dropped; damped class weights; `features` mode (frozen ImageNet
backbone, on-disk feature cache, seconds to retrain) and `finetune` mode (light
augmentation). CPU-only torch from PyTorch's wheel index. ONNX export checked against
the torch model; NO model file below the validation floor (exit 3, report still written);
exit 2 = not enough labels. `evaluate` scores a shipped model on labels reviewed after
training + the unlabelled pile; `publish` PUTs a version folder to a Gitea generic package.
Light core deps; the `train` extra is heavy — CI syncs without it, torch tests skip.

apps/vision: BodyTypeClassifier (bodytype.onnx + sidecar = the preprocessing contract:
crop margin, input size, RGB 0-255, normalisation inside the graph) and
RefinedVehicleDetector over YOLOX — refines only `car` or a class the classifier trained
on, min-confidence, `detector_class` on the result; path set but no file = phase B off
without an error; a broken file is a health detail. models/bodytype.version (tracked,
empty) pins the published version the Dockerfile fetches at build (BuildKit secret;
a pin that cannot be fetched fails the build). Verified: a trainer model gives identical
probabilities inside the vision service; both images built and smoke-tested.

Delivery: parking-trainer image in build-images.yml, the `trainer` compose profile on the
collector stack (CPU, read-only data, TRAINER_OUT), commented TRAINER_OUT/PUBLISH_TOKEN in
the wash-collector stack, .dockerignore for both Python contexts, trainer deps synced in CI.

Wiki: bodytype-classifier-training rewritten as built (+ one fleet model not per site,
secrets/access, where the crops live), opencv-anpr-service §Phase B, vision-review-outbox,
vision-service-packaging, fleet-deployment-komodo, index, log.

Claude-Session: https://claude.ai/code/session_01FWncR69HgGPuei1dLrW3cU
2026-09-07 11:14:50 +02:00

74 lines
2.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""The /analyze response contract — the shape the Node VisionClient adapter consumes.
Mirrors the first-cut API in wiki/entities/opencv-anpr-service.md:
{ plate: {text, confidence, bbox}|null, vehicle: {...}|null, modelVersion, tookMs }
Job 2 (vehicle attributes) is scaffolded as an optional field, not yet populated —
fast-alpr is plate-only; the vehicle stage is built later on the same ONNX runtime.
"""
from __future__ import annotations
from pydantic import BaseModel, Field
class BBox(BaseModel):
"""Plate bounding box in pixels (top-left origin)."""
x1: int
y1: int
x2: int
y2: int
class PlateResult(BaseModel):
text: str
# The plate's confidence = the MIN of fast-alpr's per-character confidences (a plate
# is only as trustworthy as its weakest character). See recognizer.py.
confidence: float = Field(ge=0.0, le=1.0)
bbox: BBox | None = None
# Predicted issuing region/country (advisory; fast-alpr's global model emits this).
region: str | None = None
class VehicleResult(BaseModel):
"""Job 2 — vehicle attributes. `body_type` is ADVISORY: the Node server records it
beside the plate and the Car Wash desk pre-selects the site category it maps to; the
operator decides, a disagreement is flagged, nothing is ever gated on it. Values come
from the shared vocabulary (car, sedan, hatchback, suv, minivan, pickup, van, truck,
bus, motorcycle) — anything else is ignored by Node. Phase A (a COCO detector) emits
car/truck/bus/motorcycle; the finer classes need the body-type classifier. Not yet
produced by any bundled recognizer."""
colour: str | None = None
body_type: str | None = None
# Confidence of `body_type` (0–1). Node compares it to the site's threshold.
confidence: float | None = Field(default=None, ge=0.0, le=1.0)
# The vehicle's box in frame pixels — the crop a reviewer sees / a classifier eats.
bbox: BBox | None = None
# Phase B: the detector's coarse class when the body-type classifier ran on this crop
# (body_type is then the classifier's answer if confident, else the detector's).
detector_class: str | None = None
make: str | None = None
model: str | None = None
class AnalyzeResponse(BaseModel):
# The single best plate, or null when none was found.
plate: PlateResult | None = None
# All plates found (a frame may contain several vehicles).
plates: list[PlateResult] = Field(default_factory=list)
vehicle: VehicleResult | None = None
# True when the best plate is below the confidence floor — Node should treat the
# read as advisory only and prefer the ticket path. See fail-state-safety.
low_confidence: bool = False
model_version: str
took_ms: float
class HealthResponse(BaseModel):
status: str
recognizer: str
ready: bool
model_version: str
detail: str | None = None