SuperBitDev commited on
Commit
20a4942
·
verified ·
1 Parent(s): 3b2eeb0

Upload folder using huggingface_hub

Browse files
Files changed (2) hide show
  1. miner.py +88 -20
  2. 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 = path_hf_repo / "weights.onnx"
31
  # car-wash element classes — cls_id order MUST match element `objects`
32
- # (0=broom, 1=drainage gate, 2=nozzle, 3=track) and the YOLO training order.
 
33
  self.class_names = ["broom", "drainage gate", "nozzle", "track"]
34
- model_class_order = ["broom", "drainage gate", "nozzle", "track"]
35
- self.cls_remap = np.array(
36
- [self.class_names.index(n) for n in model_class_order], dtype=np.int32
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
- self.iou_thres = 0.5 # Per-class NMS IoU; lower = stricter dedup
89
- self.cross_iou_thresh = 0.8 # Cross-class dedup IoU (suppress same physical object firing multiple classes)
 
 
 
90
  self.max_det = 200
91
  self.use_tta = True
92
 
93
- # Per-class confidence thresholds (ported pattern from fire001 miner).
94
- # A single global conf cannot serve both tiny nozzles and large tracks.
95
  # Indexed by class_names order: [broom, drainage gate, nozzle, track].
96
- # broom (0.35) -- distinctive long handle, moderate
97
- # drainage gate (0.30) -- floor element often water-obscured
98
- # nozzle (0.25) -- TINY GT objects (median ~290 px²), permissive
99
- # track (0.35) -- large clear object when present, moderate
100
  self._conf_thres_array = np.array(
101
- [0.5, 0.4, 0.5, 0.23], dtype=np.float32
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). Nozzles get the
106
- # biggest rescue because spray + motion blur often shaves a few points
107
- # off otherwise valid detections; track gets the smallest because it's
108
- # rarely borderline.
109
  self._bonus_array = np.array(
110
- [0.15, 0.1, 0, 0.05], dtype=np.float32
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:9d5cc4c83c9b5a943d33a678a44839854645b639c8c4d1c68acf04906f8a0058
3
- size 19408004
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1f9c13ee4403ddbbccd9d9707a151b1474cbb33124a2ef2965fa57d1e821ed03
3
+ size 19287011