ArthurSword commited on
Commit
2693ca2
·
verified ·
1 Parent(s): 9d725a4

scorevision: push artifact

Browse files
Files changed (1) hide show
  1. miner.py +119 -275
miner.py CHANGED
@@ -1,59 +1,11 @@
1
- """Strict consumer for an Ultralytics end-to-end-NMS ONNX (person element).
2
-
3
- Contract: static batch-1 NCHW input, one ``[1, 300, 6]`` output whose rows are
4
- ``x1, y1, x2, y2, confidence, model_class_id``. NMS runs inside the graph, so
5
- this module only validates the tensors, applies the confidence gate, undoes the
6
- letterbox, drops geometrically impossible boxes and caps the result count.
7
-
8
- No ``dataclass`` declarations: ScoreVision's ``spec_from_file_location`` loader
9
- does not register the module in ``sys.modules`` before executing it, which makes
10
- dataclass decoration fail at import.
11
-
12
- ELEMENT_CONFIG below is written by ``scripts/build_submission.py`` from the
13
- policy chosen on the validation split -- do not hand-edit it without re-running
14
- ``scripts/optimize_policy.py``, since ``nms_iou`` in particular must match the
15
- value frozen into the ONNX graph at export time.
16
- """
17
-
18
- import ast
19
  import json
20
- import math
21
- import os
22
  from pathlib import Path
23
- from typing import Any
24
-
25
  import cv2
26
  import numpy as np
27
  import onnxruntime as ort
28
  from numpy import ndarray
29
  from pydantic import BaseModel
30
 
31
- API_VERSION = "c2-strict-v1"
32
- STRICT_INFERENCE_ENV = "SV_STRICT_INFERENCE"
33
- OUTPUT_ROWS = 300
34
- OUTPUT_COLUMNS = 6
35
-
36
- ELEMENT_CONFIG: dict[str, Any] = {
37
- "element_id": "person",
38
- "classes": ["person"],
39
- "coco_ids": [0],
40
- "imgsz": 704,
41
- "conf": 0.5,
42
- "nms_iou": 0.45, # frozen into the ONNX graph at export
43
- "max_det": 300,
44
- "min_side": 3,
45
- "max_aspect": 10,
46
- "max_area_frac": 0.95,
47
- "scale_w": 1, # box geometry calibration to the validator standard
48
- "scale_h": 1,
49
- "conf_steps": ((30, 0.5), (60, 0.55), (None, 0.6)),
50
- }
51
-
52
-
53
- class InferenceInvariantError(RuntimeError):
54
- """A model tensor or decoded detection violated the serving contract."""
55
-
56
-
57
  class BoundingBox(BaseModel):
58
  x1: int
59
  y1: int
@@ -62,240 +14,132 @@ class BoundingBox(BaseModel):
62
  cls_id: int
63
  conf: float
64
 
65
-
66
  class TVFrameResult(BaseModel):
67
  frame_id: int
68
  boxes: list[BoundingBox]
69
  keypoints: list[tuple[int, int]]
70
 
71
-
72
- def _conf_for(n_raw: int) -> float:
73
- """Confidence gate, optionally raised on crowded frames.
74
-
75
- The wrong-box penalty is a per-image absolute count that saturates at ten,
76
- so a frame holding ninety people can lose the whole term while a frame with
77
- six cannot. When CONF_STEPS is non-empty the gate is spent where that
78
- exposure actually is. Empty steps means a flat gate.
79
- """
80
- for limit, conf in ELEMENT_CONFIG.get("conf_steps", ()):
81
- if limit is None or n_raw < limit:
82
- return float(conf)
83
- return float(ELEMENT_CONFIG["conf"])
84
-
85
-
86
  class Miner:
87
- api_version = API_VERSION
88
- strict_inference = False
89
 
90
  def __init__(self, path_hf_repo: Path) -> None:
91
- cfg = ELEMENT_CONFIG
92
- self.config = cfg
93
- self.class_names = list(cfg["classes"])
94
- self.conf_threshold = float(cfg["conf"])
95
- self.max_det = int(cfg["max_det"])
96
- self.min_side = float(cfg.get("min_side", 3.0))
97
- self.max_aspect_ratio = float(cfg.get("max_aspect", 10.0))
98
- self.max_area_frac = float(cfg.get("max_area_frac", 0.95))
99
- self.conf_steps = tuple(tuple(s) for s in cfg.get("conf_steps", ()))
100
- # box geometry calibration: the validator draws wider/taller boxes than
101
- # the training sources; scale about the centre before the IoU match.
102
- self.scale_w = float(cfg.get("scale_w", 1.0))
103
- self.scale_h = float(cfg.get("scale_h", 1.0))
104
- raw = os.environ.get(STRICT_INFERENCE_ENV, "")
105
- if raw not in ("", "0", "1"):
106
- raise ValueError(f"{STRICT_INFERENCE_ENV} must be '1' or unset/'0', got {raw!r}")
107
- self.strict_inference = raw == "1"
108
- self.inference_failures = 0
109
-
110
- # The referee grants one physical core (two SMT threads). Let ORT own
111
- # both for the convolution graph and keep OpenCV single-threaded: a
112
- # second OpenCV worker is slower and adds tail jitter on two siblings.
113
- cv2.setNumThreads(1)
114
- options = ort.SessionOptions()
115
- options.intra_op_num_threads = 2
116
- options.inter_op_num_threads = 1
117
- options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL
118
- options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
119
- model_path = Path(path_hf_repo) / "weights.onnx"
120
- try:
121
- self.session = ort.InferenceSession(
122
- str(model_path), sess_options=options,
123
- providers=["CUDAExecutionProvider", "CPUExecutionProvider"],
124
- )
125
- except Exception as exc:
126
- print(f"CUDA session failed, falling back to CPU: {exc}")
127
- self.session = ort.InferenceSession(
128
- str(model_path), sess_options=options, providers=["CPUExecutionProvider"]
129
- )
130
-
131
- self.input_name = self.session.get_inputs()[0].name
132
- self.output_names = [o.name for o in self.session.get_outputs()]
133
- shape = self.session.get_inputs()[0].shape
134
- if len(shape) != 4 or shape[1] != 3:
135
- raise ValueError(f"model input must be rank-4 NCHW with 3 channels, got {shape!r}")
136
- if not all(isinstance(v, int) and v > 0 for v in shape[2:]):
137
- raise ValueError(f"e2e export must have static spatial dims, got {shape[2:]!r}")
138
- if (shape[2], shape[3]) != (cfg["imgsz"], cfg["imgsz"]):
139
- raise ValueError(f"model input {shape[2]}x{shape[3]} != config imgsz {cfg['imgsz']}")
140
- self.input_height = self.input_width = int(cfg["imgsz"])
141
- out_shape = self.session.get_outputs()[0].shape
142
- if list(out_shape) != [1, OUTPUT_ROWS, OUTPUT_COLUMNS]:
143
- raise ValueError(f"e2e output must be [1, {OUTPUT_ROWS}, {OUTPUT_COLUMNS}], got {out_shape!r}")
144
- self.model_names = self._read_model_names()
145
- self._warmup(3)
146
- print(
147
- f"api_version={self.api_version} element={cfg['element_id']} imgsz={cfg['imgsz']} "
148
- f"conf={self.conf_threshold} steps={self.conf_steps} "
149
- f"providers={self.session.get_providers()}"
150
- )
151
-
152
- def _read_model_names(self) -> tuple[str, ...]:
153
- try:
154
- raw = self.session.get_modelmeta().custom_metadata_map["names"]
155
- parsed = ast.literal_eval(raw)
156
- if not isinstance(parsed, dict):
157
- raise TypeError("names metadata is not a dict")
158
- keys = sorted(parsed)
159
- if keys != list(range(len(keys))):
160
- raise ValueError(f"names metadata keys not consecutive from zero: {keys[:8]}")
161
- return tuple(str(parsed[i]) for i in keys)
162
- except Exception as exc:
163
- raise ValueError(f"e2e ONNX requires valid names metadata: {exc}") from exc
164
-
165
- @staticmethod
166
- def _letterbox(image: ndarray, new_shape: tuple[int, int], color=(114, 114, 114)):
167
- height, width = image.shape[:2]
168
- new_width, new_height = new_shape
169
- ratio = min(new_width / width, new_height / height)
170
- rw, rh = int(round(width * ratio)), int(round(height * ratio))
171
- if (rw, rh) != (width, height):
172
- image = cv2.resize(
173
- image, (rw, rh),
174
- interpolation=cv2.INTER_CUBIC if ratio > 1.0 else cv2.INTER_LINEAR,
175
- )
176
- dw, dh = (new_width - rw) / 2.0, (new_height - rh) / 2.0
177
- padded = cv2.copyMakeBorder(
178
- image,
179
- int(round(dh - 0.1)), int(round(dh + 0.1)),
180
- int(round(dw - 0.1)), int(round(dw + 0.1)),
181
- cv2.BORDER_CONSTANT, value=color,
182
- )
183
- return padded, ratio, (dw, dh)
184
-
185
- def _decode(self, output, ratio, pad, original_size):
186
- rows = output[0]
187
- boxes = rows[:, :4].astype(np.float32, copy=True)
188
- scores = rows[:, 4].astype(np.float32, copy=False)
189
- raw_classes = rows[:, 5]
190
- if (scores < 0).any() or (scores > 1).any():
191
- raise InferenceInvariantError("e2e confidence outside [0, 1]")
192
- rounded = np.rint(raw_classes)
193
- if not np.array_equal(raw_classes, rounded):
194
- raise InferenceInvariantError("e2e model class ids must be exact integers")
195
- model_classes = rounded.astype(np.int64)
196
- if (model_classes < 0).any() or (model_classes >= len(self.model_names)).any():
197
- raise InferenceInvariantError("e2e model class id outside range")
198
-
199
- active = scores > 0.0
200
- reversed_rows = active & ((boxes[:, 2] < boxes[:, 0]) | (boxes[:, 3] < boxes[:, 1]))
201
- if reversed_rows.any():
202
- raise InferenceInvariantError("e2e output contains reversed boxes")
203
-
204
- # A floor below every reachable gate; nothing under it can be emitted.
205
- floor = min([self.conf_threshold] + [float(c) for _, c in self.conf_steps])
206
- keep = active & (model_classes == 0) & (scores >= np.float32(floor))
207
- if not keep.any():
208
- return np.empty((0, 4), np.float32), np.empty((0,), np.float32)
209
- boxes, scores = boxes[keep], scores[keep]
210
-
211
- boxes[:, [0, 2]] = (boxes[:, [0, 2]] - pad[0]) / ratio
212
- boxes[:, [1, 3]] = (boxes[:, [1, 3]] - pad[1]) / ratio
213
- width, height = original_size
214
- if self.scale_w != 1.0 or self.scale_h != 1.0:
215
- cx = (boxes[:, 0] + boxes[:, 2]) * 0.5
216
- cy = (boxes[:, 1] + boxes[:, 3]) * 0.5
217
- hw = (boxes[:, 2] - boxes[:, 0]) * 0.5 * np.float32(self.scale_w)
218
- hh = (boxes[:, 3] - boxes[:, 1]) * 0.5 * np.float32(self.scale_h)
219
- boxes = np.stack([cx - hw, cy - hh, cx + hw, cy + hh], 1).astype(np.float32)
220
- boxes[:, [0, 2]] = np.clip(boxes[:, [0, 2]], 0, width - 1)
221
- boxes[:, [1, 3]] = np.clip(boxes[:, [1, 3]], 0, height - 1)
222
-
223
- bw = boxes[:, 2] - boxes[:, 0]
224
- bh = boxes[:, 3] - boxes[:, 1]
225
- aspect = np.maximum(bw / np.maximum(bh, 1e-6), bh / np.maximum(bw, 1e-6))
226
- sane = (
227
- (bw >= self.min_side)
228
- & (bh >= self.min_side)
229
- & (bw * bh <= self.max_area_frac * float(width * height))
230
- & (aspect <= self.max_aspect_ratio)
231
- )
232
- boxes, scores = boxes[sane], scores[sane]
233
- if not len(scores):
234
- return boxes, scores
235
- order = np.argsort(-scores, kind="stable")[: self.max_det]
236
- boxes, scores = boxes[order], scores[order]
237
- gate = _conf_for(len(scores)) if self.conf_steps else self.conf_threshold
238
- emit = scores >= np.float32(gate)
239
- return boxes[emit], scores[emit]
240
-
241
- def _predict_single(self, image: np.ndarray) -> list[BoundingBox]:
242
- if not isinstance(image, np.ndarray) or image.ndim != 3 or image.shape[2] != 3:
243
- raise ValueError(f"expected HWC 3-channel image, got {getattr(image, 'shape', type(image))}")
244
- if image.dtype != np.uint8:
245
- image = image.astype(np.uint8)
246
- original_height, original_width = image.shape[:2]
247
- padded, ratio, pad = self._letterbox(image, (self.input_width, self.input_height))
248
- blob = cv2.dnn.blobFromImage(padded, scalefactor=1 / 255.0, swapRB=True)
249
- outputs = self.session.run(self.output_names, {self.input_name: blob})
250
- output = outputs[0]
251
- if not np.isfinite(output).all():
252
- raise InferenceInvariantError("model output must be finite everywhere")
253
- if list(output.shape) != [1, OUTPUT_ROWS, OUTPUT_COLUMNS]:
254
- raise ValueError(f"unexpected output shape {list(output.shape)}")
255
- boxes, scores = self._decode(output, ratio, pad, (original_width, original_height))
256
- return [
257
- BoundingBox(
258
- x1=int(math.floor(b[0])), y1=int(math.floor(b[1])),
259
- x2=int(math.ceil(b[2])), y2=int(math.ceil(b[3])),
260
- cls_id=0, conf=float(s),
261
- )
262
- for b, s in zip(boxes, scores)
263
- if b[2] > b[0] and b[3] > b[1]
264
- ]
265
-
266
- def _warmup(self, iterations: int) -> None:
267
- dummy = np.zeros((720, 1280, 3), dtype=np.uint8)
268
- original = self.strict_inference
269
- self.strict_inference = True
270
- try:
271
- for _ in range(iterations):
272
- self.predict_batch(batch_images=[dummy], offset=0, n_keypoints=0)
273
- except Exception as exc:
274
- raise RuntimeError(f"warmup failed, refusing to serve: {type(exc).__name__}: {exc}") from exc
275
- finally:
276
- self.strict_inference = original
277
-
278
- def predict_batch(self, batch_images, offset: int, n_keypoints: int) -> list[TVFrameResult]:
279
- _ = n_keypoints
280
- results: list[TVFrameResult] = []
281
- for index, image in enumerate(batch_images):
282
- frame_id = offset + index
283
- try:
284
- boxes = self._predict_single(image)
285
- except Exception as exc:
286
- if self.strict_inference:
287
- raise
288
- self.inference_failures += 1
289
- print(json.dumps({
290
- "event": "inference_failure",
291
- "frame_id": frame_id,
292
- "error_type": type(exc).__name__,
293
- "error": str(exc),
294
- "inference_failures": self.inference_failures,
295
- }, sort_keys=True))
296
- boxes = []
297
- results.append(TVFrameResult(frame_id=frame_id, boxes=boxes, keypoints=[]))
298
- return results
299
 
300
  def __repr__(self) -> str:
301
- return f"E2EONNXRuntime(providers={self.session.get_providers()})"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import json
 
 
2
  from pathlib import Path
 
 
3
  import cv2
4
  import numpy as np
5
  import onnxruntime as ort
6
  from numpy import ndarray
7
  from pydantic import BaseModel
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  class BoundingBox(BaseModel):
10
  x1: int
11
  y1: int
 
14
  cls_id: int
15
  conf: float
16
 
 
17
  class TVFrameResult(BaseModel):
18
  frame_id: int
19
  boxes: list[BoundingBox]
20
  keypoints: list[tuple[int, int]]
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  class Miner:
 
 
23
 
24
  def __init__(self, path_hf_repo: Path) -> None:
25
+ repo = Path(path_hf_repo)
26
+ cfg = json.loads((repo / 'calib.json').read_text())
27
+ self.calib_x = np.asarray(cfg['calib_x'], dtype=np.float32)
28
+ self.calib_y = np.asarray(cfg['calib_y'], dtype=np.float32)
29
+ self.k = float(cfg.get('k', 15.0))
30
+ self.miss_prior = float(cfg.get('miss_prior', 0.0))
31
+ self.p_floor = float(cfg.get('p_floor', 0.05))
32
+ self.min_side = float(cfg.get('min_side', 2.0))
33
+ self.nms_iou = float(cfg.get('nms_iou', 0.6))
34
+ self.conf_min = float(cfg.get('conf_min', 0.02))
35
+ self.max_det = int(cfg.get('max_det', 300))
36
+ self.raw_xyxy = bool(cfg.get('raw_xyxy', True))
37
+ self.warp = tuple(cfg['warp']) if cfg.get('warp') else None
38
+ so = ort.SessionOptions()
39
+ so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
40
+ so.intra_op_num_threads = int(cfg.get('threads', 2))
41
+ so.inter_op_num_threads = 1
42
+ so.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL
43
+ self.session = ort.InferenceSession(str(repo / 'weights.onnx'), sess_options=so, providers=['CPUExecutionProvider'])
44
+ inp = self.session.get_inputs()[0]
45
+ self.input_name = inp.name
46
+ self.uint8_nhwc = 'uint8' in inp.type
47
+ self.size = int(inp.shape[1] if self.uint8_nhwc else inp.shape[2])
48
+ dummy = np.zeros((1, self.size, self.size, 3), np.uint8) if self.uint8_nhwc else np.zeros((1, 3, self.size, self.size), np.float32)
49
+ for _ in range(3):
50
+ self.session.run(None, {self.input_name: dummy})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
  def __repr__(self) -> str:
53
+ return f'PersonMiner(size={self.size}, k={self.k}, providers={self.session.get_providers()})'
54
+
55
+ def _prep(self, img: ndarray):
56
+ h, w = img.shape[:2]
57
+ s = self.size
58
+ if h == w:
59
+ x = cv2.resize(img, (s, s), interpolation=cv2.INTER_LINEAR)
60
+ r, left, top = (s / w, 0, 0)
61
+ else:
62
+ r = min(s / h, s / w)
63
+ nh, nw = (int(round(h * r)), int(round(w * r)))
64
+ x = np.full((s, s, 3), 114, np.uint8)
65
+ top, left = ((s - nh) // 2, (s - nw) // 2)
66
+ x[top:top + nh, left:left + nw] = cv2.resize(img, (nw, nh), interpolation=cv2.INTER_LINEAR)
67
+ if self.uint8_nhwc:
68
+ x = x[None]
69
+ else:
70
+ x = x[:, :, ::-1].transpose(2, 0, 1)[None].astype(np.float32) * (1.0 / 255.0)
71
+ return (np.ascontiguousarray(x), r, left, top)
72
+
73
+ def _warp_prep(self, img: ndarray):
74
+ h, w = img.shape[:2]
75
+ s = self.size
76
+ hs, hd = (int(round(self.warp[0] * h)), int(round(self.warp[1] * s)))
77
+ x = np.concatenate([cv2.resize(img[:hs], (s, hd), interpolation=cv2.INTER_LINEAR), cv2.resize(img[hs:], (s, s - hd), interpolation=cv2.INTER_LINEAR)], 0)
78
+ return (np.ascontiguousarray(x[None]), hs, hd)
79
+
80
+ def _unwarp(self, b: ndarray, w: int, h: int, hs: int, hd: int) -> ndarray:
81
+ s = self.size
82
+ b = b.copy()
83
+ b[:, [0, 2]] *= w / s
84
+ for j in (1, 3):
85
+ y = b[:, j]
86
+ b[:, j] = np.where(y < hd, y * hs / hd, hs + (y - hd) * (h - hs) / (s - hd))
87
+ return b
88
+
89
+ def _nms_raw(self, y: ndarray, warp_args=None) -> ndarray:
90
+ y = y.T
91
+ sc = y[:, 4:].max(1)
92
+ m = sc >= self.conf_min
93
+ y, sc = (y[m], sc[m])
94
+ if sc.size == 0:
95
+ return np.zeros((0, 6), np.float32)
96
+ if self.raw_xyxy:
97
+ b = y[:, :4].copy()
98
+ else:
99
+ b = np.stack([y[:, 0] - y[:, 2] / 2, y[:, 1] - y[:, 3] / 2, y[:, 0] + y[:, 2] / 2, y[:, 1] + y[:, 3] / 2], 1)
100
+ if warp_args is not None:
101
+ b = self._unwarp(b, *warp_args)
102
+ xywh = np.concatenate([b[:, :2], b[:, 2:] - b[:, :2]], 1).astype(np.float64)
103
+ k = np.asarray(cv2.dnn.NMSBoxes(xywh.tolist(), sc.astype(np.float32).tolist(), self.conf_min, self.nms_iou), dtype=int).reshape(-1)[:self.max_det]
104
+ return np.concatenate([b[k], sc[k, None], np.zeros((k.size, 1), b.dtype)], 1)
105
+
106
+ def _decide(self, det: ndarray, w: int, h: int, r: float, left: int, top: int) -> list[BoundingBox]:
107
+ det = det[det[:, 4] > 0.0]
108
+ if det.size == 0:
109
+ return []
110
+ p = np.interp(det[:, 4], self.calib_x, self.calib_y).astype(np.float64)
111
+ n_hat = float(p.sum()) + self.miss_prior
112
+ thr = max(n_hat / (self.k + n_hat), self.p_floor)
113
+ keep = p > thr
114
+ det, p = (det[keep], p[keep])
115
+ order = np.argsort(-p, kind='stable')
116
+ det, p = (det[order], p[order])
117
+ b = det[:, :4].copy()
118
+ b[:, [0, 2]] = (b[:, [0, 2]] - left) / r
119
+ b[:, [1, 3]] = (b[:, [1, 3]] - top) / r
120
+ b[:, [0, 2]] = np.clip(b[:, [0, 2]], 0, w)
121
+ b[:, [1, 3]] = np.clip(b[:, [1, 3]], 0, h)
122
+ out = []
123
+ for (x1, y1, x2, y2), pi in zip(b, p):
124
+ if x2 - x1 < self.min_side or y2 - y1 < self.min_side:
125
+ continue
126
+ out.append(BoundingBox(x1=int(round(x1)), y1=int(round(y1)), x2=int(round(x2)), y2=int(round(y2)), cls_id=0, conf=float(round(pi, 5))))
127
+ return out
128
+
129
+ def predict_batch(self, batch_images: list[ndarray], offset: int, n_keypoints: int) -> list[TVFrameResult]:
130
+ results = []
131
+ for i, img in enumerate(batch_images):
132
+ h, w = img.shape[:2]
133
+ if self.warp is not None and h == w and self.uint8_nhwc:
134
+ x, hs, hd = self._warp_prep(img)
135
+ det = self.session.run(None, {self.input_name: x})[0][0]
136
+ det = self._nms_raw(det, (w, h, hs, hd))
137
+ boxes = self._decide(det, w, h, 1.0, 0, 0)
138
+ else:
139
+ x, r, left, top = self._prep(img)
140
+ det = self.session.run(None, {self.input_name: x})[0][0]
141
+ if det.shape[-1] != 6:
142
+ det = self._nms_raw(det)
143
+ boxes = self._decide(det, w, h, r, left, top)
144
+ results.append(TVFrameResult(frame_id=offset + i, boxes=boxes, keypoints=[]))
145
+ return results