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: 7,183 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 | """RF-DETR postprocessor (DETR-style query decode).
Used by the `rfdetr-package` parcel detector on the `package-detection` step
(per-model `postprocessor: 'rfdetr'` override — the step default is `yolo`).
Model contract, verified on the shipped export (onnx / openvino / coreml all
agree to |d| <= 0.005 in sigmoid space):
input "input" [1, 3, 576, 576] float, square RESIZE (no letterbox),
ImageNet mean/std normalised
(catalog: preprocessMode 'resize' +
inputNormalization 'imagenet')
output "dets" [1, 300, 4] cxcywh, NORMALISED to the input image
output "labels" [1, 300, 2] RAW logits; sigmoid -> per-query prob.
Three things differ from the `yolo` postprocessor and every one of them is a
way to get zero detections while looking correct:
1. **Column 0 is the no-object slot; the real classes start at 1.**
The upstream rfdetr helper slices `logits[..., :-1]` — it drops the LAST
column. On this checkpoint (`num_classes=1` ⇒ head width 2) that would throw
away the `package` column and keep the background one, so every frame
decodes as "nothing, forever". We slice `[..., 1:]`. Measured: the
background column never exceeded 0.003 on any test frame, parcel or not.
2. **No letterbox, so `scale`/`pad` are unused.** Boxes map onto the original
crop by multiplying the normalised cxcywh by orig_w / orig_h directly. The
normalised coords are NOT sigmoid-bounded and DO exceed 1.0 (and go below 0)
on edge queries — they are clamped to the frame, and a box that clamps to
nothing is dropped.
3. **300 independent queries, no objectness.** NMS still runs (near-duplicate
queries do occur) but the model is not anchor-based, so the default IoU is
looser (0.50) than the yolo default on purpose.
DEGENERATE-BOX GUARD (measured, not theoretical): on a parcel-free crop
dominated by one large dark rectangle (the doormat) the top query becomes a box
spanning ~the whole crop at 0.33. Every real parcel detection was <= 25% of the
crop area. Rejecting boxes covering more than `maxAreaFraction` of the crop
removes that failure mode without touching a single true positive.
Output matches the yolo postprocessor:
``{"kind": "detections", "detections": [{"class", "score", "bbox":[x1,y1,x2,y2]}]}``
with pixel bboxes in ORIGINAL frame coordinates.
"""
from __future__ import annotations
import numpy as np
# Fraction of the input crop above which a box is treated as the degenerate
# whole-crop query rather than a parcel. Config key: `maxAreaFraction`.
DEFAULT_MAX_AREA_FRACTION = 0.60
# DETR queries are not anchors, so duplicates overlap less tightly than a yolo
# grid does. Config key: `nmsIouThreshold` (shared with the yolo decoder).
DEFAULT_NMS_IOU = 0.50
def _iou(a: tuple, b: tuple) -> float:
"""IoU between two boxes (x1, y1, x2, y2)."""
ix1 = max(a[0], b[0])
iy1 = max(a[1], b[1])
ix2 = min(a[2], b[2])
iy2 = min(a[3], b[3])
inter = max(0.0, ix2 - ix1) * max(0.0, iy2 - iy1)
if inter == 0:
return 0.0
union = (a[2] - a[0]) * (a[3] - a[1]) + (b[2] - b[0]) * (b[3] - b[1]) - inter
return inter / union if union > 0 else 0.0
def _nms(boxes: list, iou_threshold: float) -> list:
"""Greedy non-maximum suppression over `_xyxy`, highest score first."""
kept: list = []
for box in sorted(boxes, key=lambda b: b["score"], reverse=True):
if all(_iou(box["_xyxy"], k["_xyxy"]) <= iou_threshold for k in kept):
kept.append(box)
return kept
def _resolve_outputs(predictions: dict) -> tuple:
"""(dets, labels) by NAME, falling back to shape when the names are stripped.
The onnx / openvino / coreml exports all keep the `dets` + `labels` names;
the shape fallback covers a re-export that renames them (the boxes tensor is
the one whose last dim is 4).
"""
if "dets" in predictions and "labels" in predictions:
return np.asarray(predictions["dets"]), np.asarray(predictions["labels"])
arrays = [np.asarray(v) for v in predictions.values()]
dets = next((a for a in arrays if a.shape[-1] == 4), None)
labels = next((a for a in arrays if a.shape[-1] != 4), None)
return dets, labels
def postprocess_rfdetr(
predictions: dict,
config: dict,
orig_w: int,
orig_h: int,
scale: float,
pad: tuple,
) -> dict:
"""Decode RF-DETR query output to original-frame detections.
`scale` / `pad` are accepted for signature parity with the other
postprocessors and deliberately UNUSED: this model is a square resize, not a
letterbox, so there is no padding to invert.
"""
conf_threshold = float(config.get("confidence", 0) or 0)
labels_map = config.get("labels") or ["package"]
nms_iou = float(config.get("nmsIouThreshold", DEFAULT_NMS_IOU) or DEFAULT_NMS_IOU)
max_area = float(config.get("maxAreaFraction", DEFAULT_MAX_AREA_FRACTION) or DEFAULT_MAX_AREA_FRACTION)
dets, logits = _resolve_outputs(predictions)
if dets is None or logits is None:
return {"kind": "detections", "detections": []}
dets = dets[0] if dets.ndim == 3 else dets
logits = logits[0] if logits.ndim == 3 else logits
if dets.ndim != 2 or logits.ndim != 2 or logits.shape[1] < 2:
# A single-column `labels` would be the no-object slot alone — nothing
# to decode, and slicing it would raise instead of saying so.
return {"kind": "detections", "detections": []}
probs = 1.0 / (1.0 + np.exp(-logits.astype(np.float32)))
# Column 0 is the DETR background / no-object slot — NOT the last column.
# See the module docstring: dropping the wrong end silently yields zero
# detections on every frame.
class_probs = probs[:, 1:]
best_class = class_probs.argmax(axis=1)
best_score = class_probs.max(axis=1)
frame_area = float(orig_w * orig_h)
if frame_area <= 0:
return {"kind": "detections", "detections": []}
candidates: list = []
for i in np.nonzero(best_score >= conf_threshold)[0]:
cx, cy, w, h = (float(v) for v in dets[i][:4])
# Normalised, NOT sigmoid-bounded: edge queries exceed [0,1] and are
# clamped to the frame rather than dropped.
x1 = max(0.0, (cx - w / 2.0) * orig_w)
y1 = max(0.0, (cy - h / 2.0) * orig_h)
x2 = min(float(orig_w), (cx + w / 2.0) * orig_w)
y2 = min(float(orig_h), (cy + h / 2.0) * orig_h)
if x2 <= x1 or y2 <= y1:
continue
if ((x2 - x1) * (y2 - y1)) / frame_area > max_area:
continue # degenerate whole-crop query (the doormat failure mode)
cls = int(best_class[i])
candidates.append({
"class": labels_map[cls] if cls < len(labels_map) else str(cls),
"score": round(float(best_score[i]), 4),
"bbox": [round(x1, 1), round(y1, 1), round(x2, 1), round(y2, 1)],
"_xyxy": (x1, y1, x2, y2),
})
kept = _nms(candidates, nms_iou)
for box in kept:
box.pop("_xyxy", None)
return {"kind": "detections", "detections": kept}
|