f7a262ac9a
Build & push images / images (push) Successful in 6m31s
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
52 lines
1.9 KiB
Python
52 lines
1.9 KiB
Python
"""Run an exported classifier (ONNX + sidecar) — torch-free. Used by `evaluate` and by
|
|
the tests; the vision service carries its own, equivalent, reader (vehicle.py)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from .preprocess import Sidecar, load_input, softmax
|
|
|
|
|
|
class OnnxClassifier:
|
|
def __init__(self, model_path: Path, sidecar_path: Path | None = None) -> None:
|
|
import onnxruntime as ort
|
|
|
|
self.model_path = Path(model_path)
|
|
self.sidecar = Sidecar.read(sidecar_path or self.model_path.with_suffix(".json"))
|
|
opts = ort.SessionOptions()
|
|
opts.intra_op_num_threads = 2
|
|
self._session = ort.InferenceSession(
|
|
str(self.model_path), sess_options=opts, providers=["CPUExecutionProvider"]
|
|
)
|
|
self._input = self._session.get_inputs()[0].name
|
|
|
|
@property
|
|
def classes(self) -> list[str]:
|
|
return list(self.sidecar.classes)
|
|
|
|
def predict_inputs(self, x: Any, batch: int = 64) -> Any:
|
|
"""[N,3,S,S] float32 → probabilities [N,K]."""
|
|
import numpy as np
|
|
|
|
outs = []
|
|
for i in range(0, len(x), batch):
|
|
logits = self._session.run(None, {self._input: x[i : i + batch]})[0]
|
|
outs.append(softmax(logits))
|
|
return np.concatenate(outs, axis=0) if outs else np.zeros((0, len(self.classes)), np.float32)
|
|
|
|
def predict_files(self, paths: list[Path], batch: int = 64) -> tuple[Any, list[int]]:
|
|
"""Decode + classify crop files. Returns (probs, indices of paths that decoded)."""
|
|
import numpy as np
|
|
|
|
xs, kept = [], []
|
|
for i, p in enumerate(paths):
|
|
x = load_input(p, self.sidecar.input_size)
|
|
if x is not None:
|
|
xs.append(x)
|
|
kept.append(i)
|
|
if not xs:
|
|
return np.zeros((0, len(self.classes)), np.float32), []
|
|
return self.predict_inputs(np.stack(xs), batch), kept
|