TerryStewart commited on
Commit
236ac09
·
verified ·
1 Parent(s): de9fc7b

Update miner.py

Browse files
Files changed (1) hide show
  1. miner.py +108 -156
miner.py CHANGED
@@ -1,53 +1,34 @@
1
  from pathlib import Path
2
  import math
 
3
  import cv2
4
  import numpy as np
5
  import onnxruntime as ort
6
  from numpy import ndarray
7
- import struct
8
- import shutil
9
- import importlib.util as imutil
10
- import sys
11
-
12
- class BoundingBox:
13
- def __init__(self, x1: int, y1: int, x2: int, y2: int,
14
- cls_id: int, conf: float) -> None:
15
- self.x1 = x1
16
- self.y1 = y1
17
- self.x2 = x2
18
- self.y2 = y2
19
- self.cls_id = cls_id
20
- self.conf = conf
21
-
22
- def model_dump(self, *args, **kwargs) -> dict:
23
- return {
24
- "x1": self.x1,
25
- "y1": self.y1,
26
- "x2": self.x2,
27
- "y2": self.y2,
28
- "cls_id": self.cls_id,
29
- "conf": self.conf,
30
- }
31
-
32
-
33
- class TVFrameResult:
34
- def __init__(self, frame_id: int, boxes: list[BoundingBox],
35
- keypoints: list[tuple[int, int]]) -> None:
36
- self.frame_id = frame_id
37
- self.boxes = boxes
38
- self.keypoints = keypoints
39
-
40
- def model_dump(self, *args, **kwargs) -> dict:
41
- return {
42
- "frame_id": self.frame_id,
43
- "boxes": [box.model_dump() for box in self.boxes],
44
- "keypoints": self.keypoints,
45
- }
46
 
47
 
48
  class Miner:
49
  """ONNX Runtime miner. Hard global NMS + sanity filter + dedup + flip TTA, with per-class rescue bonus."""
 
50
  class_names = ["cup", "bottle", "can"]
 
 
51
  input_size = 1280
52
  iou_thres = 0.4
53
  cross_iou_thresh = 0.7
@@ -57,44 +38,9 @@ class Miner:
57
  max_det = 300
58
  _conf_thres_array = np.array([0.6, 0.45, 0.5], dtype=np.float32)
59
  _bonus_array = np.array([0.0, 0.0, 0.2], dtype=np.float32)
60
-
61
- def load_model_from_onnx(self, model_path, model_name) -> None:
62
- so_path = f"{model_name}.so"
63
- shutil.copy2(model_path, so_path)
64
- spec = imutil.spec_from_file_location(
65
- model_name,
66
- so_path
67
- )
68
- model = imutil.module_from_spec(spec)
69
- spec.loader.exec_module(model)
70
- sys.modules[model_name] = model
71
- self.model = model
72
 
73
  def __init__(self, path_hf_repo: Path) -> None:
74
- model_path = "./weight.onnx"
75
- self.class_names = ["cup", "bottle", "can"]
76
- with open(path_hf_repo / "weights.onnx", "rb") as f:
77
- f.seek(-16, 2)
78
- so_size = struct.unpack("<Q", f.read(8))[0]
79
- model_size = struct.unpack("<Q", f.read(8))[0]
80
- f.seek(0)
81
- model = f.read(so_size)
82
- weight = f.read(model_size)
83
- with open("model.onnx", "wb") as f:
84
- f.write(model)
85
- with open("weight.onnx", "wb") as f:
86
- f.write(weight)
87
-
88
- self.model = None
89
- try:
90
- self.load_model_from_onnx("model.onnx", "model")
91
- except Exception as e:
92
- print(
93
- "Embedded model module failed to load; "
94
- f"falling back to ONNX Runtime inference: {type(e).__name__}: {e}"
95
- )
96
-
97
- self.class_names = ["cup", "bottle", "can"]
98
  print("ORT version:", ort.__version__)
99
 
100
  try:
@@ -133,6 +79,7 @@ class Miner:
133
  self.input_name = self.session.get_inputs()[0].name
134
  self.output_names = [output.name for output in self.session.get_outputs()]
135
  self.input_shape = self.session.get_inputs()[0].shape
 
136
 
137
  self.input_height = self._safe_dim(self.input_shape[2], default=self.input_size)
138
  self.input_width = self._safe_dim(self.input_shape[3], default=self.input_size)
@@ -140,6 +87,7 @@ class Miner:
140
  print(f"ONNX model loaded from: {model_path}")
141
  print(f"ONNX providers: {self.session.get_providers()}")
142
  print(f"ONNX input: name={self.input_name}, shape={self.input_shape}")
 
143
 
144
  def __repr__(self) -> str:
145
  return (
@@ -151,6 +99,21 @@ class Miner:
151
  def _safe_dim(value, default: int) -> int:
152
  return value if isinstance(value, int) and value > 0 else default
153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  def _letterbox(self, image: ndarray, new_shape: tuple[int, int],
155
  color=(114, 114, 114)
156
  ) -> tuple[ndarray, float, tuple[float, float]]:
@@ -178,9 +141,9 @@ class Miner:
178
  orig_h, orig_w = image.shape[:2]
179
  img, ratio, pad = self._letterbox(image, (self.input_width, self.input_height))
180
  img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
181
- img = img.astype(np.float32) / 255.0
182
  img = np.transpose(img, (2, 0, 1))[None, ...]
183
- img = np.ascontiguousarray(img, dtype=np.float32)
184
  return img, ratio, pad, (orig_w, orig_h)
185
 
186
  @staticmethod
@@ -220,9 +183,9 @@ class Miner:
220
  xx2 = np.minimum(boxes[i, 2], boxes[rest, 2])
221
  yy2 = np.minimum(boxes[i, 3], boxes[rest, 3])
222
  inter = np.maximum(0.0, xx2 - xx1) * np.maximum(0.0, yy2 - yy1)
223
- a_i = (max(0.0, boxes[i, 2] - boxes[i, 0]) *
224
  max(0.0, boxes[i, 3] - boxes[i, 1]))
225
- a_r = (np.maximum(0.0, boxes[rest, 2] - boxes[rest, 0]) *
226
  np.maximum(0.0, boxes[rest, 3] - boxes[rest, 1]))
227
  iou = inter / (a_i + a_r - inter + 1e-7)
228
  order = rest[iou <= iou_thresh]
@@ -251,7 +214,7 @@ class Miner:
251
  boxes = np.asarray(boxes, dtype=np.float32)
252
  scores = np.asarray(scores, dtype=np.float32)
253
  cls_ids = np.asarray(cls_ids, dtype=np.int32)
254
- areas = (np.maximum(0.0, boxes[:, 2] - boxes[:, 0]) *
255
  np.maximum(0.0, boxes[:, 3] - boxes[:, 1]))
256
  margins = scores - self._conf_thres_array[cls_ids]
257
  order = np.lexsort((-areas, -margins))
@@ -291,9 +254,9 @@ class Miner:
291
  np.inf,
292
  )
293
  keep = (
294
- (bw >= self.min_side) & (bh >= self.min_side) &
295
- (area >= self.min_box_area) &
296
- (area <= 0.95 * image_area) &
297
  (ar <= self.max_aspect_ratio)
298
  )
299
  return boxes[keep], scores[keep], cls_ids[keep]
@@ -307,7 +270,7 @@ class Miner:
307
  n = len(post_boxes)
308
  if n == 0:
309
  return np.empty(0, dtype=np.float32)
310
- full_areas = (np.maximum(0.0, full_boxes[:, 2] - full_boxes[:, 0]) *
311
  np.maximum(0.0, full_boxes[:, 3] - full_boxes[:, 1]))
312
  out = np.empty(n, dtype=np.float32)
313
  for i in range(n):
@@ -373,9 +336,13 @@ class Miner:
373
  if preds.ndim != 2 or preds.shape[1] < 6:
374
  raise ValueError(f"Unexpected ONNX final-det output shape: {preds.shape}")
375
 
376
- boxes = preds[:,:4].astype(np.float32)
377
  scores = preds[:, 4].astype(np.float32)
378
- cls_ids = preds[:, 5].astype(np.int32)
 
 
 
 
379
 
380
  keep = self._conf_filter_mask(scores, cls_ids)
381
  boxes = boxes[keep]
@@ -406,7 +373,7 @@ class Miner:
406
  if preds.ndim != 2 or preds.shape[1] < 5:
407
  raise ValueError(f"Unexpected raw output shape: {preds.shape}")
408
 
409
- boxes_xywh = preds[:,:4].astype(np.float32)
410
  cls_part = preds[:, 4:].astype(np.float32)
411
  if cls_part.shape[1] == 1:
412
  scores = cls_part[:, 0]
@@ -414,6 +381,11 @@ class Miner:
414
  else:
415
  cls_ids = np.argmax(cls_part, axis=1).astype(np.int32)
416
  scores = cls_part[np.arange(len(cls_part)), cls_ids]
 
 
 
 
 
417
 
418
  keep = self._conf_filter_mask(scores, cls_ids)
419
  boxes_xywh = boxes_xywh[keep]
@@ -486,81 +458,61 @@ class Miner:
486
  return self._postprocess(outputs[0], ratio, pad, orig_size)
487
 
488
  def _predict_tta(self, image: np.ndarray) -> list[BoundingBox]:
489
- if self.model is None:
490
- boxes_orig = self._predict_single(image)
491
- flipped = cv2.flip(image, 1)
492
- boxes_flip = self._predict_single(flipped)
493
- w = image.shape[1]
494
- boxes_flip = [
495
- BoundingBox(
496
- x1=w - b.x2, y1=b.y1, x2=w - b.x1, y2=b.y2,
497
- cls_id=b.cls_id, conf=b.conf,
498
- )
499
- for b in boxes_flip
500
- ]
501
- all_boxes = boxes_orig + boxes_flip
502
- if not all_boxes:
503
- return []
504
- coords = np.array(
505
- [[b.x1, b.y1, b.x2, b.y2] for b in all_boxes], dtype=np.float32
506
  )
507
- scores = np.array([b.conf for b in all_boxes], dtype=np.float32)
508
- cls_ids = np.array([b.cls_id for b in all_boxes], dtype=np.int32)
509
-
510
- hard_keep = self._per_class_hard_nms(coords, scores, cls_ids, self.iou_thres)
511
- if len(hard_keep) == 0:
512
- return []
513
- if len(hard_keep) > self.max_det:
514
- top = np.argsort(-scores[hard_keep])[: self.max_det]
515
- hard_keep = hard_keep[top]
516
- boosted = self._max_score_per_cluster(
517
- coords[hard_keep], cls_ids[hard_keep],
518
- coords, scores, cls_ids, self.iou_thres,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
519
  )
520
- kept_coords = coords[hard_keep]
521
- kept_cls = cls_ids[hard_keep]
522
- if len(kept_coords) > 1:
523
- kept_coords, boosted, kept_cls = self._cross_class_dedup_op(
524
- kept_coords, boosted, kept_cls, self.cross_iou_thresh
525
- )
526
- return [
527
- BoundingBox(
528
- x1=int(math.floor(kept_coords[j, 0])),
529
- y1=int(math.floor(kept_coords[j, 1])),
530
- x2=int(math.ceil(kept_coords[j, 2])),
531
- y2=int(math.ceil(kept_coords[j, 3])),
532
- cls_id=int(kept_cls[j]),
533
- conf=float(boosted[j]),
534
- )
535
- for j in range(len(kept_coords))
536
- ]
537
- else:
538
- output = self.model.predict(image)
539
- print(output)
540
- res: list[BoundingBox] = []
541
- for each_output in output["results"]:
542
- cls_id = self.class_names.index(each_output["echo"]["text"])
543
- for prediction in each_output["predictions"]:
544
- for polygon in prediction["masks"]:
545
- pts = np.array(polygon, dtype=np.int32)
546
- x, y, w, h = cv2.boundingRect(pts)
547
- res.append(
548
- BoundingBox(
549
- x1=int(x),
550
- y1=int(y),
551
- x2=int(x + w),
552
- y2=int(y + h),
553
- cls_id=cls_id,
554
- conf=float(prediction["confidence"]),
555
- )
556
- )
557
- return res
558
 
559
  def predict_batch(self, batch_images: list[ndarray], offset: int,
560
  n_keypoints: int) -> list[TVFrameResult]:
561
  results: list[TVFrameResult] = []
562
  for frame_number_in_batch, image in enumerate(batch_images):
563
- print(image.shape)
564
  try:
565
  boxes = self._predict_tta(image)
566
  except Exception as e:
@@ -573,4 +525,4 @@ class Miner:
573
  keypoints=[(0, 0) for _ in range(max(0, int(n_keypoints)))],
574
  )
575
  )
576
- return results
 
1
  from pathlib import Path
2
  import math
3
+
4
  import cv2
5
  import numpy as np
6
  import onnxruntime as ort
7
  from numpy import ndarray
8
+ from pydantic import BaseModel
9
+
10
+
11
+ class BoundingBox(BaseModel):
12
+ x1: int
13
+ y1: int
14
+ x2: int
15
+ y2: int
16
+ cls_id: int
17
+ conf: float
18
+
19
+
20
+ class TVFrameResult(BaseModel):
21
+ frame_id: int
22
+ boxes: list[BoundingBox]
23
+ keypoints: list[tuple[int, int]]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
 
26
  class Miner:
27
  """ONNX Runtime miner. Hard global NMS + sanity filter + dedup + flip TTA, with per-class rescue bonus."""
28
+
29
  class_names = ["cup", "bottle", "can"]
30
+ model_class_names = ["bottle", "can", "cup"]
31
+ _model_to_competition_cls = np.array([1, 2, 0], dtype=np.int32)
32
  input_size = 1280
33
  iou_thres = 0.4
34
  cross_iou_thresh = 0.7
 
38
  max_det = 300
39
  _conf_thres_array = np.array([0.6, 0.45, 0.5], dtype=np.float32)
40
  _bonus_array = np.array([0.0, 0.0, 0.2], dtype=np.float32)
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
  def __init__(self, path_hf_repo: Path) -> None:
43
+ model_path = path_hf_repo / "weights.onnx"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  print("ORT version:", ort.__version__)
45
 
46
  try:
 
79
  self.input_name = self.session.get_inputs()[0].name
80
  self.output_names = [output.name for output in self.session.get_outputs()]
81
  self.input_shape = self.session.get_inputs()[0].shape
82
+ self.input_dtype = self._input_dtype(self.session.get_inputs()[0].type)
83
 
84
  self.input_height = self._safe_dim(self.input_shape[2], default=self.input_size)
85
  self.input_width = self._safe_dim(self.input_shape[3], default=self.input_size)
 
87
  print(f"ONNX model loaded from: {model_path}")
88
  print(f"ONNX providers: {self.session.get_providers()}")
89
  print(f"ONNX input: name={self.input_name}, shape={self.input_shape}")
90
+ print(f"ONNX input dtype: {self.input_dtype}")
91
 
92
  def __repr__(self) -> str:
93
  return (
 
99
  def _safe_dim(value, default: int) -> int:
100
  return value if isinstance(value, int) and value > 0 else default
101
 
102
+ @staticmethod
103
+ def _input_dtype(input_type: str) -> np.dtype:
104
+ if input_type == "tensor(float16)":
105
+ return np.dtype(np.float16)
106
+ return np.dtype(np.float32)
107
+
108
+ @classmethod
109
+ def _to_competition_cls(cls, cls_ids: np.ndarray) -> np.ndarray:
110
+ if len(cls_ids) == 0:
111
+ return cls_ids.astype(np.int32)
112
+ valid = (cls_ids >= 0) & (cls_ids < len(cls._model_to_competition_cls))
113
+ remapped = np.full_like(cls_ids, fill_value=-1, dtype=np.int32)
114
+ remapped[valid] = cls._model_to_competition_cls[cls_ids[valid]]
115
+ return remapped
116
+
117
  def _letterbox(self, image: ndarray, new_shape: tuple[int, int],
118
  color=(114, 114, 114)
119
  ) -> tuple[ndarray, float, tuple[float, float]]:
 
141
  orig_h, orig_w = image.shape[:2]
142
  img, ratio, pad = self._letterbox(image, (self.input_width, self.input_height))
143
  img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
144
+ img = img.astype(self.input_dtype) / self.input_dtype.type(255.0)
145
  img = np.transpose(img, (2, 0, 1))[None, ...]
146
+ img = np.ascontiguousarray(img, dtype=self.input_dtype)
147
  return img, ratio, pad, (orig_w, orig_h)
148
 
149
  @staticmethod
 
183
  xx2 = np.minimum(boxes[i, 2], boxes[rest, 2])
184
  yy2 = np.minimum(boxes[i, 3], boxes[rest, 3])
185
  inter = np.maximum(0.0, xx2 - xx1) * np.maximum(0.0, yy2 - yy1)
186
+ a_i = (max(0.0, boxes[i, 2] - boxes[i, 0]) *
187
  max(0.0, boxes[i, 3] - boxes[i, 1]))
188
+ a_r = (np.maximum(0.0, boxes[rest, 2] - boxes[rest, 0]) *
189
  np.maximum(0.0, boxes[rest, 3] - boxes[rest, 1]))
190
  iou = inter / (a_i + a_r - inter + 1e-7)
191
  order = rest[iou <= iou_thresh]
 
214
  boxes = np.asarray(boxes, dtype=np.float32)
215
  scores = np.asarray(scores, dtype=np.float32)
216
  cls_ids = np.asarray(cls_ids, dtype=np.int32)
217
+ areas = (np.maximum(0.0, boxes[:, 2] - boxes[:, 0]) *
218
  np.maximum(0.0, boxes[:, 3] - boxes[:, 1]))
219
  margins = scores - self._conf_thres_array[cls_ids]
220
  order = np.lexsort((-areas, -margins))
 
254
  np.inf,
255
  )
256
  keep = (
257
+ (bw >= self.min_side) & (bh >= self.min_side) &
258
+ (area >= self.min_box_area) &
259
+ (area <= 0.95 * image_area) &
260
  (ar <= self.max_aspect_ratio)
261
  )
262
  return boxes[keep], scores[keep], cls_ids[keep]
 
270
  n = len(post_boxes)
271
  if n == 0:
272
  return np.empty(0, dtype=np.float32)
273
+ full_areas = (np.maximum(0.0, full_boxes[:, 2] - full_boxes[:, 0]) *
274
  np.maximum(0.0, full_boxes[:, 3] - full_boxes[:, 1]))
275
  out = np.empty(n, dtype=np.float32)
276
  for i in range(n):
 
336
  if preds.ndim != 2 or preds.shape[1] < 6:
337
  raise ValueError(f"Unexpected ONNX final-det output shape: {preds.shape}")
338
 
339
+ boxes = preds[:, :4].astype(np.float32)
340
  scores = preds[:, 4].astype(np.float32)
341
+ cls_ids = self._to_competition_cls(preds[:, 5].astype(np.int32))
342
+ valid_cls = cls_ids >= 0
343
+ boxes = boxes[valid_cls]
344
+ scores = scores[valid_cls]
345
+ cls_ids = cls_ids[valid_cls]
346
 
347
  keep = self._conf_filter_mask(scores, cls_ids)
348
  boxes = boxes[keep]
 
373
  if preds.ndim != 2 or preds.shape[1] < 5:
374
  raise ValueError(f"Unexpected raw output shape: {preds.shape}")
375
 
376
+ boxes_xywh = preds[:, :4].astype(np.float32)
377
  cls_part = preds[:, 4:].astype(np.float32)
378
  if cls_part.shape[1] == 1:
379
  scores = cls_part[:, 0]
 
381
  else:
382
  cls_ids = np.argmax(cls_part, axis=1).astype(np.int32)
383
  scores = cls_part[np.arange(len(cls_part)), cls_ids]
384
+ cls_ids = self._to_competition_cls(cls_ids)
385
+ valid_cls = cls_ids >= 0
386
+ boxes_xywh = boxes_xywh[valid_cls]
387
+ scores = scores[valid_cls]
388
+ cls_ids = cls_ids[valid_cls]
389
 
390
  keep = self._conf_filter_mask(scores, cls_ids)
391
  boxes_xywh = boxes_xywh[keep]
 
458
  return self._postprocess(outputs[0], ratio, pad, orig_size)
459
 
460
  def _predict_tta(self, image: np.ndarray) -> list[BoundingBox]:
461
+ boxes_orig = self._predict_single(image)
462
+ flipped = cv2.flip(image, 1)
463
+ boxes_flip = self._predict_single(flipped)
464
+ w = image.shape[1]
465
+ boxes_flip = [
466
+ BoundingBox(
467
+ x1=w - b.x2, y1=b.y1, x2=w - b.x1, y2=b.y2,
468
+ cls_id=b.cls_id, conf=b.conf,
 
 
 
 
 
 
 
 
 
469
  )
470
+ for b in boxes_flip
471
+ ]
472
+ all_boxes = boxes_orig + boxes_flip
473
+ if not all_boxes:
474
+ return []
475
+
476
+ coords = np.array(
477
+ [[b.x1, b.y1, b.x2, b.y2] for b in all_boxes], dtype=np.float32
478
+ )
479
+ scores = np.array([b.conf for b in all_boxes], dtype=np.float32)
480
+ cls_ids = np.array([b.cls_id for b in all_boxes], dtype=np.int32)
481
+
482
+ hard_keep = self._per_class_hard_nms(coords, scores, cls_ids, self.iou_thres)
483
+ if len(hard_keep) == 0:
484
+ return []
485
+ if len(hard_keep) > self.max_det:
486
+ top = np.argsort(-scores[hard_keep])[: self.max_det]
487
+ hard_keep = hard_keep[top]
488
+ boosted = self._max_score_per_cluster(
489
+ coords[hard_keep], cls_ids[hard_keep],
490
+ coords, scores, cls_ids, self.iou_thres,
491
+ )
492
+
493
+ kept_coords = coords[hard_keep]
494
+ kept_cls = cls_ids[hard_keep]
495
+ if len(kept_coords) > 1:
496
+ kept_coords, boosted, kept_cls = self._cross_class_dedup_op(
497
+ kept_coords, boosted, kept_cls, self.cross_iou_thresh
498
  )
499
+
500
+ return [
501
+ BoundingBox(
502
+ x1=int(math.floor(kept_coords[j, 0])),
503
+ y1=int(math.floor(kept_coords[j, 1])),
504
+ x2=int(math.ceil(kept_coords[j, 2])),
505
+ y2=int(math.ceil(kept_coords[j, 3])),
506
+ cls_id=int(kept_cls[j]),
507
+ conf=float(boosted[j]),
508
+ )
509
+ for j in range(len(kept_coords))
510
+ ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
511
 
512
  def predict_batch(self, batch_images: list[ndarray], offset: int,
513
  n_keypoints: int) -> list[TVFrameResult]:
514
  results: list[TVFrameResult] = []
515
  for frame_number_in_batch, image in enumerate(batch_images):
 
516
  try:
517
  boxes = self._predict_tta(image)
518
  except Exception as e:
 
525
  keypoints=[(0, 0) for _ in range(max(0, int(n_keypoints)))],
526
  )
527
  )
528
+ return results