Instructions to use camstack/camstack-trainer with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- ultralytics
How to use camstack/camstack-trainer with ultralytics:
from huggingface_hub import hf_hub_download from ultralytics import YOLO # pick the weights file from this repo's "Files and versions" tab weights = hf_hub_download("camstack/camstack-trainer", "<weights>.pt") model = YOLO(weights) source = 'http://images.cocodataset.org/val2017/000000039769.jpg' model.predict(source=source, save=True) - Notebooks
- Google Colab
- Kaggle
File size: 8,414 Bytes
ebeed67 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 | """The hub's annotated export → an Ultralytics YOLO layout.
Input: the retrain archive (``annotations.json`` first, then ``frames/…`` JPEGs;
schema ``camstack.retrain-export/v2`` or ``/v3``), exactly what
``pipelineAnalytics.writeRetrainArchive`` streams.
What becomes a label (COCO-80 ids, the head the fleet's root keeps):
- ``subject`` of macro ``person`` → ``person`` (0).
- ``subject`` of macro ``vehicle`` / ``animal`` → its COCO class, taken from the
annotation's ``label`` (``car``, ``dog``…) when that label is a COCO class of
the same macro. A vehicle or animal box WITHOUT such a label cannot be given
a COCO class honestly (training "dog" on a cat teaches the wrong species), so
its frame is left out of training and counted as ``unmappable``. Evaluation
is per macro class and still uses it.
- ``model_error`` boxes are never positives: the region is learned as
background, which is what marking a phantom asks for.
- ``package`` / ``face`` / ``plate`` boxes are not root-detector classes and are
ignored (no label line).
Which frames TRAIN:
- every ``complete`` frame ("every subject is boxed"), including one with no
subject at all — a hard negative;
- an INCOMPLETE frame only when it carries at least one annotation (a subject
or a model error): the operator picked it to teach that box. YOLO has no
"ignore region", so an unboxed subject on such a frame IS learned as
background; restricting incomplete frames to annotated ones is the bound on
that cost. Incomplete frames are never evaluated.
Files are renamed to neutral ``000001.jpg`` names: archive paths carry device
and track ids, and nothing the container writes or logs names a frame.
"""
from __future__ import annotations
import hashlib
import json
import shutil
import tarfile
from dataclasses import dataclass, field
from pathlib import Path, PurePosixPath
from typing import Iterable, Optional
from ._vendor import ensure_pipeline_on_path
from ._vendor.camstack_eval.classes import COCO_TO_MACRO
from .errors import dataset_error
MANIFEST = "annotations.json"
ACCEPTED_SCHEMAS = ("camstack.retrain-export/v2", "camstack.retrain-export/v3")
IGNORED_MACROS = ("package", "face", "plate")
def coco_80() -> list[str]:
ensure_pipeline_on_path()
from postprocessors.yolo import COCO_80 # the pool's own label list
return list(COCO_80)
@dataclass(frozen=True)
class YoloBox:
cls_index: int
cx: float
cy: float
w: float
h: float
def line(self) -> str:
return f"{self.cls_index} {self.cx:.6f} {self.cy:.6f} {self.w:.6f} {self.h:.6f}"
@dataclass(frozen=True)
class Frame:
frame_id: str
path: str
device_id: int
captured_at: Optional[int]
complete: bool
labels: tuple[YoloBox, ...]
subjects: int
model_errors: int
unmappable: int
@property
def trainable(self) -> bool:
if self.unmappable > 0:
return False
return self.complete or (self.subjects + self.model_errors) > 0
@dataclass
class LayoutCounts:
frames: dict[str, int] = field(default_factory=dict)
boxes: dict[str, int] = field(default_factory=dict)
def sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1 << 20), b""):
digest.update(chunk)
return digest.hexdigest()
def verify_archive(path: Path, expected_sha256: str) -> None:
if not path.is_file():
raise dataset_error("dataset-missing", "dataset.tar not found")
actual = sha256_file(path)
if actual != expected_sha256:
raise dataset_error("dataset-sha256", "dataset.tar does not match the job's sha256")
def _safe_member(name: str) -> bool:
pure = PurePosixPath(name)
if pure.is_absolute() or ".." in pure.parts:
return False
return name == MANIFEST or (pure.parts[:1] == ("frames",) and pure.suffix.lower() == ".jpg")
def extract_archive(path: Path, dest: Path) -> dict:
"""Extract the manifest and the frame JPEGs only; return the manifest."""
dest.mkdir(parents=True, exist_ok=True)
try:
with tarfile.open(path) as tar:
for member in tar:
if not member.isfile() or not _safe_member(member.name):
continue
target = dest / member.name
target.parent.mkdir(parents=True, exist_ok=True)
source = tar.extractfile(member)
if source is None:
continue
with target.open("wb") as out:
shutil.copyfileobj(source, out)
except tarfile.TarError as exc:
raise dataset_error("dataset-unreadable", "dataset.tar is not a readable tar") from exc
manifest_path = dest / MANIFEST
if not manifest_path.is_file():
raise dataset_error("dataset-no-manifest", "dataset.tar has no annotations.json")
manifest = json.loads(manifest_path.read_text("utf-8"))
if manifest.get("schema") not in ACCEPTED_SCHEMAS:
raise dataset_error("dataset-schema", f"export schema {manifest.get('schema')!r} is not supported")
return manifest
def _clamped(bbox: dict) -> Optional[tuple[float, float, float, float]]:
x1 = max(0.0, float(bbox["x"]))
y1 = max(0.0, float(bbox["y"]))
x2 = min(1.0, float(bbox["x"]) + float(bbox["w"]))
y2 = min(1.0, float(bbox["y"]) + float(bbox["h"]))
if x2 - x1 <= 1e-4 or y2 - y1 <= 1e-4:
return None
return (x1 + x2) / 2, (y1 + y2) / 2, x2 - x1, y2 - y1
def coco_index_for(macro: str, label: Optional[str], names: list[str]) -> Optional[int]:
"""The COCO-80 index a subject trains as, or ``None`` when none is honest."""
if macro == "person":
return names.index("person")
wanted = (label or "").strip().lower()
if wanted in names and COCO_TO_MACRO.get(wanted) == macro:
return names.index(wanted)
return None
def frame_from_entry(entry: dict, names: list[str]) -> Frame:
labels: list[YoloBox] = []
subjects = model_errors = unmappable = 0
for annotation in entry.get("annotations", []):
macro = annotation.get("macroClass")
if annotation.get("kind") == "model_error":
model_errors += 1
continue
if annotation.get("kind") != "subject" or macro in IGNORED_MACROS:
continue
subjects += 1
index = coco_index_for(str(macro), annotation.get("label"), names)
if index is None:
unmappable += 1
continue
box = _clamped(annotation["bbox"])
if box is not None:
labels.append(YoloBox(index, *box))
captured = entry.get("capturedAt")
return Frame(
frame_id=str(entry["frameId"]),
path=str(entry["path"]),
device_id=int(entry["deviceId"]),
captured_at=int(captured) if isinstance(captured, (int, float)) else None,
complete=entry.get("complete") is True,
labels=tuple(labels),
subjects=subjects,
model_errors=model_errors,
unmappable=unmappable,
)
def frames_from_manifest(manifest: dict, names: Optional[list[str]] = None) -> list[Frame]:
names = names if names is not None else coco_80()
return [frame_from_entry(e, names) for e in manifest.get("frames", [])]
def write_split(frames: Iterable[Frame], source_root: Path, out_root: Path, split: str) -> tuple[int, int]:
"""Copy one split's frames + YOLO label files; returns (frames, boxes)."""
images = out_root / "images" / split
labels = out_root / "labels" / split
images.mkdir(parents=True, exist_ok=True)
labels.mkdir(parents=True, exist_ok=True)
count = boxes = 0
for frame in frames:
source = source_root / frame.path
if not source.is_file():
continue
count += 1
stem = f"{count:06d}"
shutil.copyfile(source, images / f"{stem}.jpg")
(labels / f"{stem}.txt").write_text("".join(b.line() + "\n" for b in frame.labels), "utf-8")
boxes += len(frame.labels)
return count, boxes
def write_layout(splits: dict[str, list[Frame]], source_root: Path, out_root: Path) -> LayoutCounts:
counts = LayoutCounts()
for split, frames in splits.items():
n, boxes = write_split(frames, source_root, out_root, split)
counts.frames[split] = n
counts.boxes[split] = boxes
return counts
|