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
camstack-train 0.1.0: YOLOv9 root detector fine-tune (job.json in, result.json + bundle.tar out)
ebeed67 verified Download camstack_train/_vendor/pipeline_python/postprocessors/rfdetr.py from camstack/camstack-trainer: direct link, hf CLI and curl.
- Browser
- Download file 7.18 kB
-
https://huggingface.co/camstack/camstack-trainer/resolve/main/camstack_train/_vendor/pipeline_python/postprocessors/rfdetr.py
- Command line
-
hf download hf://camstack/camstack-trainer/camstack_train/_vendor/pipeline_python/postprocessors/rfdetr.py
-
curl -L -o rfdetr.py https://huggingface.co/camstack/camstack-trainer/resolve/main/camstack_train/_vendor/pipeline_python/postprocessors/rfdetr.py
7.18 kB
| """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} | |