| """ |
| TurboVision miner for element `manak0/Detect-car-wash` — ONNX / CPU-safe. |
| |
| Why ONNX-only: the latency-loop compliance checker (branch `latency-loop`) loads THIS |
| miner.py in a sandbox whose image has ONLY onnxruntime + cv2 + numpy + pydantic |
| (NO torch, NO ultralytics), forbids `.pt/.pth/.safetensors` files, forbids importing |
| socket/urllib/http/subprocess and calling open()/eval/exec, blocks the network during |
| inference, caps memory at 8 GiB, and requires the repo to contain a `.onnx` model. |
| It times `predict_batch` on CPU and needs p95 <= element.latency_p95_ms (target 100 ms), |
| and it re-checks that these outputs match your submitted responses at IoU >= 0.85. |
| |
| So: pure onnxruntime, deterministic, small input size. Classes MUST be in manifest |
| order (cls_id == index): 0=broom 1=drainage gate 2=nozzle 3=track. |
| """ |
| from pathlib import Path |
| import os |
|
|
| import numpy as np |
| import cv2 |
| import onnxruntime as ort |
| from pydantic import BaseModel |
|
|
| CLASSES = ["broom", "drainage gate", "nozzle", "track"] |
|
|
| CONF = float(os.environ.get("CARWASH_CONF", "0.15")) |
| |
| |
| _pc = os.environ.get("CARWASH_CONF_PER_CLASS", "") |
| |
| |
| |
| |
| PER_CLASS_CONF = ([float(x) for x in _pc.split(",")] if _pc else [0.32, 0.17, 0.37, 0.41]) |
| |
| |
| |
| |
| _bn = os.environ.get("CARWASH_BONUS_PER_CLASS", "") |
| BONUS_PER_CLASS = ([float(x) for x in _bn.split(",")] if _bn else [0.05, 0.10, 0.10, 0.10]) |
| IOU_NMS = float(os.environ.get("CARWASH_IOU", "0.6")) |
| |
| |
| |
| |
| _pi = os.environ.get("CARWASH_IOU_PER_CLASS", "") |
| IOU_PER_CLASS = ([float(x) for x in _pi.split(",")] if _pi else [0.5, 0.6, 0.30, 0.6]) |
| |
| |
| CROSS_IOU = float(os.environ.get("CARWASH_CROSS_IOU", "0.9")) |
| |
| MIN_SIDE = float(os.environ.get("CARWASH_MIN_SIDE", "3")) |
| MIN_AREA = float(os.environ.get("CARWASH_MIN_AREA", "16")) |
| MAX_AR = float(os.environ.get("CARWASH_MAX_AR", "12")) |
| |
| |
| |
| |
| _ma = os.environ.get("CARWASH_MIN_AREA_FRAC", "") |
| MIN_AREA_FRAC = ([float(x) for x in _ma.split(",")] if _ma else [0.0, 0.0, 0.0, 0.0]) |
| MAX_DET = int(os.environ.get("CARWASH_MAX_DET", "50")) |
| |
| |
| |
| |
| GLOBAL_FALLBACK = os.environ.get("CARWASH_GLOBAL_FALLBACK", "1") != "0" |
| MODEL_FILE = os.environ.get("CARWASH_MODEL", "carwash.onnx") |
|
|
|
|
| class BoundingBox(BaseModel): |
| x1: int |
| y1: int |
| x2: int |
| y2: int |
| cls_id: int |
| conf: float |
|
|
|
|
| class Polygon(BaseModel): |
| cls_id: int |
| conf: float |
| points: list[tuple[int, int]] |
|
|
|
|
| class TVFrameResult(BaseModel): |
| frame_id: int |
| boxes: list[BoundingBox] | None = None |
| polygons: list[Polygon] | None = None |
| keypoints: list[tuple[int, int]] | None = None |
|
|
|
|
| def _letterbox(img: np.ndarray, new_shape: tuple[int, int]) -> tuple[np.ndarray, float, float, float]: |
| """Resize+pad BGR image to new_shape (H,W), keep aspect. Return (img, ratio, pad_w, pad_h).""" |
| h, w = img.shape[:2] |
| nh, nw = new_shape |
| r = min(nh / h, nw / w) |
| uw, uh = int(round(w * r)), int(round(h * r)) |
| resized = cv2.resize(img, (uw, uh), interpolation=cv2.INTER_LINEAR) |
| pad_w, pad_h = (nw - uw) / 2, (nh - uh) / 2 |
| top, bottom = int(round(pad_h - 0.1)), int(round(pad_h + 0.1)) |
| left, right = int(round(pad_w - 0.1)), int(round(pad_w + 0.1)) |
| out = cv2.copyMakeBorder(resized, top, bottom, left, right, cv2.BORDER_CONSTANT, value=(114, 114, 114)) |
| return out, r, left, top |
|
|
|
|
| def _nms(boxes: np.ndarray, scores: np.ndarray, iou_thr: float) -> list[int]: |
| if len(boxes) == 0: |
| return [] |
| x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] |
| areas = np.maximum(0, x2 - x1) * np.maximum(0, y2 - y1) |
| order = scores.argsort()[::-1] |
| keep = [] |
| while order.size > 0: |
| i = order[0] |
| keep.append(int(i)) |
| if order.size == 1: |
| break |
| xx1 = np.maximum(x1[i], x1[order[1:]]) |
| yy1 = np.maximum(y1[i], y1[order[1:]]) |
| xx2 = np.minimum(x2[i], x2[order[1:]]) |
| yy2 = np.minimum(y2[i], y2[order[1:]]) |
| inter = np.maximum(0, xx2 - xx1) * np.maximum(0, yy2 - yy1) |
| iou = inter / (areas[i] + areas[order[1:]] - inter + 1e-9) |
| order = order[1:][iou <= iou_thr] |
| return keep |
|
|
|
|
| def _same_class_nms(boxes: np.ndarray, scores: np.ndarray, iou_thr: float) -> list[int]: |
| """NMS within one class, PLUS containment suppression: drop a box whose center |
| lies inside a kept higher-conf box (or IoU > iou_thr). Kills duplicate detections |
| of the same physical object (e.g. one nozzle boxed twice).""" |
| n = len(boxes) |
| if n == 0: |
| return [] |
| order = scores.argsort()[::-1] |
| cx = (boxes[:, 0] + boxes[:, 2]) / 2.0 |
| cy = (boxes[:, 1] + boxes[:, 3]) / 2.0 |
| areas = np.maximum(0, boxes[:, 2] - boxes[:, 0]) * np.maximum(0, boxes[:, 3] - boxes[:, 1]) |
| keep = [] |
| for i in order: |
| drop = False |
| for j in keep: |
| xx1 = max(boxes[i, 0], boxes[j, 0]); yy1 = max(boxes[i, 1], boxes[j, 1]) |
| xx2 = min(boxes[i, 2], boxes[j, 2]); yy2 = min(boxes[i, 3], boxes[j, 3]) |
| inter = max(0, xx2 - xx1) * max(0, yy2 - yy1) |
| iou = inter / (areas[i] + areas[j] - inter + 1e-9) |
| center_in = (boxes[j, 0] <= cx[i] <= boxes[j, 2]) and (boxes[j, 1] <= cy[i] <= boxes[j, 3]) |
| if iou > iou_thr or center_in: |
| drop = True |
| break |
| if not drop: |
| keep.append(int(i)) |
| return keep |
|
|
|
|
| def _sane_mask(xyxy: np.ndarray, img_area: float) -> np.ndarray: |
| """Keep-mask dropping degenerate/implausible boxes (a common FP source).""" |
| bw = xyxy[:, 2] - xyxy[:, 0] |
| bh = xyxy[:, 3] - xyxy[:, 1] |
| area = bw * bh |
| ar = np.maximum(bw / np.maximum(bh, 1e-6), bh / np.maximum(bw, 1e-6)) |
| return ((bw >= MIN_SIDE) & (bh >= MIN_SIDE) & (area >= MIN_AREA) |
| & (area <= 0.95 * img_area) & (ar <= MAX_AR)) |
|
|
|
|
| def _cross_class_dedup(xyxy: np.ndarray, conf: np.ndarray, cls_id: np.ndarray, |
| margin: np.ndarray, iou_thr: float) -> list[int]: |
| """Suppress near-duplicate boxes ACROSS classes: order by conf-margin then area, |
| keep the best, drop any other-index box with IoU > iou_thr. Kills same-object |
| multi-class fires (nozzle+track on one water patch).""" |
| n = len(xyxy) |
| if n <= 1: |
| return list(range(n)) |
| areas = np.maximum(0, xyxy[:, 2] - xyxy[:, 0]) * np.maximum(0, xyxy[:, 3] - xyxy[:, 1]) |
| order = np.lexsort((-areas, -margin)) |
| suppressed = np.zeros(n, dtype=bool) |
| keep = [] |
| for i in order: |
| if suppressed[i]: |
| continue |
| keep.append(int(i)) |
| xx1 = np.maximum(xyxy[i, 0], xyxy[:, 0]); yy1 = np.maximum(xyxy[i, 1], xyxy[:, 1]) |
| xx2 = np.minimum(xyxy[i, 2], xyxy[:, 2]); yy2 = np.minimum(xyxy[i, 3], xyxy[:, 3]) |
| inter = np.maximum(0, xx2 - xx1) * np.maximum(0, yy2 - yy1) |
| iou = inter / (max(1e-7, areas[i]) + areas - inter + 1e-7) |
| dup = iou > iou_thr |
| dup[i] = False |
| suppressed |= dup |
| return keep |
|
|
|
|
| class Miner: |
| def __init__(self, path_hf_repo: Path) -> None: |
| model_path = str(Path(path_hf_repo) / MODEL_FILE) |
| providers = os.environ.get("CARWASH_PROVIDERS", "CPUExecutionProvider").split(",") |
| avail = ort.get_available_providers() |
| providers = [p for p in providers if p in avail] or ["CPUExecutionProvider"] |
| so = ort.SessionOptions() |
| so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL |
| so.intra_op_num_threads = int(os.environ.get("CARWASH_THREADS", "0")) |
| self.sess = ort.InferenceSession(model_path, sess_options=so, providers=providers) |
| self.inp = self.sess.get_inputs()[0] |
| |
| self.np_dtype = np.float16 if "float16" in (self.inp.type or "") else np.float32 |
| shape = self.inp.shape |
| self.H = int(shape[2]) if isinstance(shape[2], int) else 640 |
| self.W = int(shape[3]) if isinstance(shape[3], int) else 640 |
| self.nc = len(CLASSES) |
| |
| dummy = np.zeros((1, 3, self.H, self.W), dtype=self.np_dtype) |
| self.sess.run(None, {self.inp.name: dummy}) |
| print(f"✅ Car-wash ONNX loaded {MODEL_FILE} input={self.H}x{self.W} providers={providers} conf={CONF}") |
|
|
| def __repr__(self) -> str: |
| return f"CarWash ONNX ({MODEL_FILE}) {self.H}x{self.W} classes={CLASSES} conf={CONF}" |
|
|
| def _preprocess(self, img_bgr: np.ndarray): |
| lb, r, pad_w, pad_h = _letterbox(img_bgr, (self.H, self.W)) |
| rgb = lb[:, :, ::-1].astype(np.float32) / 255.0 |
| chw = np.transpose(rgb, (2, 0, 1)) |
| return chw, r, pad_w, pad_h |
|
|
| def _postprocess(self, out: np.ndarray, r: float, pad_w: float, pad_h: float, |
| orig_w: int, orig_h: int) -> list[BoundingBox]: |
| |
| pred = out[0] |
| |
| |
| |
| if pred.ndim == 2 and pred.shape[1] == 6: |
| pred = pred[pred[:, 4] > 1e-3] |
| boxes_in_all = pred[:, :4].astype(np.float32) |
| conf_all = pred[:, 4].astype(np.float32) |
| cls_id_all = pred[:, 5].astype(np.int32) |
| _is_xywh = False |
| else: |
| if pred.shape[0] == (4 + self.nc): |
| pred = pred.transpose(1, 0) |
| boxes_in_all = pred[:, :4].astype(np.float32) |
| cls_scores = pred[:, 4:4 + self.nc] |
| cls_id_all = cls_scores.argmax(1).astype(np.int32) |
| conf_all = cls_scores.max(1).astype(np.float32) |
| _is_xywh = True |
|
|
| def _to_xyxy(b: np.ndarray) -> np.ndarray: |
| if _is_xywh: |
| cx, cy, w, h = b[:, 0], b[:, 1], b[:, 2], b[:, 3] |
| xy = np.stack([cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2], 1) |
| else: |
| xy = b.astype(np.float32, copy=True) |
| xy[:, [0, 2]] = (xy[:, [0, 2]] - pad_w) / r |
| xy[:, [1, 3]] = (xy[:, [1, 3]] - pad_h) / r |
| xy[:, [0, 2]] = xy[:, [0, 2]].clip(0, orig_w) |
| xy[:, [1, 3]] = xy[:, [1, 3]].clip(0, orig_h) |
| return xy |
|
|
| def _fallback() -> list[BoundingBox]: |
| |
| if not GLOBAL_FALLBACK or len(conf_all) == 0: |
| return [] |
| g = int(conf_all.argmax()) |
| xy = _to_xyxy(boxes_in_all[g:g + 1])[0] |
| if xy[2] - xy[0] < 1 or xy[3] - xy[1] < 1: |
| return [] |
| return [BoundingBox(x1=int(xy[0]), y1=int(xy[1]), x2=int(xy[2]), y2=int(xy[3]), |
| cls_id=int(cls_id_all[g]), conf=float(conf_all[g]))] |
|
|
| |
| floor = np.array(PER_CLASS_CONF, dtype=np.float32) |
| bonus = np.array(BONUS_PER_CLASS, dtype=np.float32) |
| m = conf_all >= floor[cls_id_all] |
| for c in range(self.nc): |
| if bonus[c] <= 0: |
| continue |
| cmask = cls_id_all == c |
| if not cmask.any() or m[cmask].any(): |
| continue |
| idx = np.where(cmask)[0] |
| top = idx[int(conf_all[idx].argmax())] |
| if conf_all[top] >= floor[c] - bonus[c]: |
| m[top] = True |
| if not m.any(): |
| return _fallback() |
| cls_id, conf = cls_id_all[m], conf_all[m] |
| xyxy = _to_xyxy(boxes_in_all[m]) |
| |
| img_area = float(orig_w * orig_h) |
| sm = _sane_mask(xyxy, img_area) |
| if not sm.any(): |
| return _fallback() |
| xyxy, cls_id, conf = xyxy[sm], cls_id[sm], conf[sm] |
| |
| mafrac = np.array(MIN_AREA_FRAC, dtype=np.float32) |
| if mafrac.any(): |
| bw = xyxy[:, 2] - xyxy[:, 0]; bh = xyxy[:, 3] - xyxy[:, 1] |
| am = (bw * bh) >= (mafrac[cls_id] * img_area) |
| if not am.any(): |
| return _fallback() |
| xyxy, cls_id, conf = xyxy[am], cls_id[am], conf[am] |
| |
| if len(conf) > 150: |
| top = np.argsort(-conf)[:150] |
| xyxy, cls_id, conf = xyxy[top], cls_id[top], conf[top] |
| |
| keep_idx = [] |
| for c in np.unique(cls_id): |
| idx = np.where(cls_id == c)[0] |
| iou_c = IOU_PER_CLASS[c] if 0 <= c < len(IOU_PER_CLASS) else IOU_NMS |
| for k in _same_class_nms(xyxy[idx], conf[idx], iou_c): |
| keep_idx.append(int(idx[k])) |
| keep_idx = np.array(keep_idx, dtype=np.intp) |
| xyxy, cls_id, conf = xyxy[keep_idx], cls_id[keep_idx], conf[keep_idx] |
| |
| if CROSS_IOU > 0 and len(xyxy) > 1: |
| margin = conf - floor[cls_id] |
| cd = _cross_class_dedup(xyxy, conf, cls_id, margin, CROSS_IOU) |
| xyxy, cls_id, conf = xyxy[cd], cls_id[cd], conf[cd] |
| out_boxes = [BoundingBox( |
| x1=int(xyxy[j, 0]), y1=int(xyxy[j, 1]), x2=int(xyxy[j, 2]), y2=int(xyxy[j, 3]), |
| cls_id=int(cls_id[j]), conf=float(conf[j]), |
| ) for j in range(len(xyxy))] |
| if not out_boxes: |
| return _fallback() |
| out_boxes.sort(key=lambda b: b.conf, reverse=True) |
| return out_boxes[:MAX_DET] |
|
|
| def predict_batch(self, batch_images, offset: int, n_keypoints: int) -> list[TVFrameResult]: |
| |
| |
| results: list[TVFrameResult] = [] |
| for i, img in enumerate(batch_images): |
| chw, r, pw, ph = self._preprocess(img) |
| inp = np.ascontiguousarray(chw[None], dtype=self.np_dtype) |
| out = self.sess.run(None, {self.inp.name: inp})[0] |
| boxes = self._postprocess(out, r, pw, ph, img.shape[1], img.shape[0]) |
| results.append(TVFrameResult(frame_id=offset + i, boxes=boxes, polygons=[], keypoints=[])) |
| return results |
|
|