Upload folder using huggingface_hub
Browse files- miner.py +88 -20
- weights.onnx +2 -2
miner.py
CHANGED
|
@@ -27,14 +27,17 @@ class Miner:
|
|
| 27 |
def __init__(self,
|
| 28 |
path_hf_repo: Path
|
| 29 |
) -> None:
|
| 30 |
-
model_path =
|
| 31 |
# car-wash element classes — cls_id order MUST match element `objects`
|
| 32 |
-
# (0=broom, 1=drainage gate, 2=nozzle, 3=track)
|
|
|
|
| 33 |
self.class_names = ["broom", "drainage gate", "nozzle", "track"]
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
| 38 |
print("ORT version:", ort.__version__)
|
| 39 |
|
| 40 |
try:
|
|
@@ -65,6 +68,20 @@ class Miner:
|
|
| 65 |
|
| 66 |
print("ORT session providers:", self.session.get_providers())
|
| 67 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
for inp in self.session.get_inputs():
|
| 69 |
print("INPUT:", inp.name, inp.shape, inp.type)
|
| 70 |
|
|
@@ -85,29 +102,31 @@ class Miner:
|
|
| 85 |
self.input_width = self._safe_dim(self.input_shape[3], default=1280)
|
| 86 |
|
| 87 |
# Tuned for validator scoring (pillars: 0.6*map50 + 0.4*false_positive).
|
| 88 |
-
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
| 90 |
self.max_det = 200
|
| 91 |
self.use_tta = True
|
| 92 |
|
| 93 |
-
|
| 94 |
-
#
|
| 95 |
# Indexed by class_names order: [broom, drainage gate, nozzle, track].
|
| 96 |
-
#
|
| 97 |
-
#
|
| 98 |
-
#
|
| 99 |
-
#
|
| 100 |
self._conf_thres_array = np.array(
|
| 101 |
-
[0.
|
| 102 |
)
|
| 103 |
# Per-class rescue bonus: when a class has ZERO boxes passing the
|
| 104 |
# threshold in a frame, its top-1 candidate is admitted when its score
|
| 105 |
-
# is at least (per-class threshold - per-class bonus).
|
| 106 |
-
#
|
| 107 |
-
#
|
| 108 |
-
# rarely borderline.
|
| 109 |
self._bonus_array = np.array(
|
| 110 |
-
[0.
|
| 111 |
)
|
| 112 |
|
| 113 |
# Box sanity filter — kept loose: car-wash `nozzle` boxes are tiny
|
|
@@ -131,6 +150,55 @@ class Miner:
|
|
| 131 |
def _safe_dim(value, default: int) -> int:
|
| 132 |
return value if isinstance(value, int) and value > 0 else default
|
| 133 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
def _letterbox(
|
| 135 |
self,
|
| 136 |
image: ndarray,
|
|
|
|
| 27 |
def __init__(self,
|
| 28 |
path_hf_repo: Path
|
| 29 |
) -> None:
|
| 30 |
+
model_path = self._resolve_model_path(path_hf_repo)
|
| 31 |
# car-wash element classes — cls_id order MUST match element `objects`
|
| 32 |
+
# (0=broom, 1=drainage gate, 2=nozzle, 3=track). This is the canonical
|
| 33 |
+
# order every downstream consumer (validator, BoundingBox.cls_id) sees.
|
| 34 |
self.class_names = ["broom", "drainage gate", "nozzle", "track"]
|
| 35 |
+
# FALLBACK model-emit order: the authoritative order is read from the
|
| 36 |
+
# ONNX `names` metadata after the session is created (embedded by
|
| 37 |
+
# Ultralytics at export, ships inside weights.onnx), so a retrained
|
| 38 |
+
# model with a different class order is remapped correctly without
|
| 39 |
+
# code changes. This list is used only when metadata is missing.
|
| 40 |
+
self._model_class_order = ["broom", "drainage gate", "nozzle", "track"]
|
| 41 |
print("ORT version:", ort.__version__)
|
| 42 |
|
| 43 |
try:
|
|
|
|
| 68 |
|
| 69 |
print("ORT session providers:", self.session.get_providers())
|
| 70 |
|
| 71 |
+
# Build cls_remap: for each model-emit index i,
|
| 72 |
+
# cls_remap[i] = self.class_names.index(model_class_order[i])
|
| 73 |
+
# The model-side order comes from the ONNX metadata when available,
|
| 74 |
+
# else falls back to the static _model_class_order.
|
| 75 |
+
model_class_order = self._read_model_class_order()
|
| 76 |
+
if model_class_order is None:
|
| 77 |
+
model_class_order = list(self._model_class_order)
|
| 78 |
+
print(f"cls order: no usable ONNX metadata, FALLBACK {model_class_order}")
|
| 79 |
+
else:
|
| 80 |
+
print(f"cls order: from ONNX metadata {model_class_order}")
|
| 81 |
+
self.cls_remap = np.array(
|
| 82 |
+
[self.class_names.index(n) for n in model_class_order], dtype=np.int32
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
for inp in self.session.get_inputs():
|
| 86 |
print("INPUT:", inp.name, inp.shape, inp.type)
|
| 87 |
|
|
|
|
| 102 |
self.input_width = self._safe_dim(self.input_shape[3], default=1280)
|
| 103 |
|
| 104 |
# Tuned for validator scoring (pillars: 0.6*map50 + 0.4*false_positive).
|
| 105 |
+
# All values below are the measured optimum of a full grid sweep on
|
| 106 |
+
# the validator-style val split (tune_miner.py, 241 1024x1024 crops,
|
| 107 |
+
# composite 0.8002 -> 0.8103) -- re-run the sweep after any retrain.
|
| 108 |
+
self.iou_thres = 0.45 # Per-class NMS IoU; lower = stricter dedup
|
| 109 |
+
self.cross_iou_thresh = 0.9 # Cross-class dedup IoU (suppress same physical object firing multiple classes)
|
| 110 |
self.max_det = 200
|
| 111 |
self.use_tta = True
|
| 112 |
|
| 113 |
+
# conf thresholds: broom=0.38 drainage gate=0.45 nozzle=0.30 track=0.60
|
| 114 |
+
# Per-class confidence thresholds.
|
| 115 |
# Indexed by class_names order: [broom, drainage gate, nozzle, track].
|
| 116 |
+
# broom/nozzle sit low: under the validator metric the mAP gained
|
| 117 |
+
# from the extra recall outweighs the FP-pillar cost (the previous
|
| 118 |
+
# 0.5/0.5 silently discarded many valid detections); track is the
|
| 119 |
+
# one class where false fires are common enough to need 0.38.
|
| 120 |
self._conf_thres_array = np.array(
|
| 121 |
+
[0.37, 0.23, 0.37, 0.45], dtype=np.float32
|
| 122 |
)
|
| 123 |
# Per-class rescue bonus: when a class has ZERO boxes passing the
|
| 124 |
# threshold in a frame, its top-1 candidate is admitted when its score
|
| 125 |
+
# is at least (per-class threshold - per-class bonus).
|
| 126 |
+
# DISABLED (all zeros): the sweep showed rescue admits more false
|
| 127 |
+
# positives than true positives under the validator's FP pillar.
|
|
|
|
| 128 |
self._bonus_array = np.array(
|
| 129 |
+
[0.05, 0.05, 0.0, 0.15], dtype=np.float32
|
| 130 |
)
|
| 131 |
|
| 132 |
# Box sanity filter — kept loose: car-wash `nozzle` boxes are tiny
|
|
|
|
| 150 |
def _safe_dim(value, default: int) -> int:
|
| 151 |
return value if isinstance(value, int) and value > 0 else default
|
| 152 |
|
| 153 |
+
@staticmethod
|
| 154 |
+
def _resolve_model_path(repo: Path) -> Path:
|
| 155 |
+
"""Locate the ONNX model in the repo dir.
|
| 156 |
+
|
| 157 |
+
Prefers weights.onnx (FP16/FP32 export), then weights_int8.onnx (the
|
| 158 |
+
training script's INT8-quantized export -- works as-is: quantization
|
| 159 |
+
preserves the Ultralytics metadata and QDQ models take regular fp32
|
| 160 |
+
input), then any other .onnx file. INT8 is the fallback when the FP16
|
| 161 |
+
export exceeds the 30 MB deployment limit (e.g. yolo26m).
|
| 162 |
+
"""
|
| 163 |
+
for name in ("weights.onnx", "weights_int8.onnx"):
|
| 164 |
+
p = repo / name
|
| 165 |
+
if p.exists():
|
| 166 |
+
if name != "weights.onnx":
|
| 167 |
+
print(f"model: weights.onnx not found, using {name}")
|
| 168 |
+
return p
|
| 169 |
+
candidates = sorted(repo.glob("*.onnx"))
|
| 170 |
+
if candidates:
|
| 171 |
+
print(f"model: using {candidates[0].name}")
|
| 172 |
+
return candidates[0]
|
| 173 |
+
return repo / "weights.onnx" # let session creation raise the error
|
| 174 |
+
|
| 175 |
+
def _read_model_class_order(self) -> list[str] | None:
|
| 176 |
+
"""Read the model's class order from Ultralytics ONNX metadata.
|
| 177 |
+
|
| 178 |
+
Returns the class names ordered by model-emit index, or None when
|
| 179 |
+
metadata is missing/unparsable or doesn't match `class_names` as a
|
| 180 |
+
set (in which case the static _model_class_order fallback is used).
|
| 181 |
+
"""
|
| 182 |
+
try:
|
| 183 |
+
import ast
|
| 184 |
+
|
| 185 |
+
meta = self.session.get_modelmeta().custom_metadata_map
|
| 186 |
+
names = ast.literal_eval(meta["names"]) # e.g. {0: 'broom', ...}
|
| 187 |
+
if isinstance(names, dict):
|
| 188 |
+
order = [str(names[i]) for i in sorted(names)]
|
| 189 |
+
else:
|
| 190 |
+
order = [str(n) for n in names]
|
| 191 |
+
except Exception as e:
|
| 192 |
+
print(f"cls order: could not read ONNX names metadata ({e})")
|
| 193 |
+
return None
|
| 194 |
+
if sorted(order) != sorted(self.class_names):
|
| 195 |
+
print(
|
| 196 |
+
f"cls order: ONNX names {order} do not match expected classes "
|
| 197 |
+
f"{self.class_names}; ignoring metadata"
|
| 198 |
+
)
|
| 199 |
+
return None
|
| 200 |
+
return order
|
| 201 |
+
|
| 202 |
def _letterbox(
|
| 203 |
self,
|
| 204 |
image: ndarray,
|
weights.onnx
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1f9c13ee4403ddbbccd9d9707a151b1474cbb33124a2ef2965fa57d1e821ed03
|
| 3 |
+
size 19287011
|