Skip to content

Commit 5b9e10b

Browse files
committed
Handle async pose stop and validate POET frames
Makes pose processor shutdown non-throwing by returning a success flag from `reset()`/`shutdown()`, and wires that into the main window so users get a status message when stop is deferred during model initialization. It also refactors the worker loop to honor stop requests around backend init, cleanly close backend state in a single `finally` path, and avoid emitting init errors when a stop was intentional. In POET inference, it now validates input as 3-channel image data and enforces contiguous frame memory before inference to prevent backend/runtime issues.
1 parent bf119e6 commit 5b9e10b

3 files changed

Lines changed: 87 additions & 63 deletions

File tree

‎dlclivegui/gui/main_window.py‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2993,14 +2993,14 @@ def _reset_active_pose_processor(
29932993
self,
29942994
*,
29952995
reset_processor_plugin: bool,
2996-
) -> None:
2996+
) -> bool:
29972997
if self._backend_name == "dlc":
29982998
self._dlc.reset(
29992999
reset_processor_plugin=reset_processor_plugin,
30003000
)
3001-
return
3001+
return True
30023002

3003-
self._active_pose_processor.reset()
3003+
return self._active_pose_processor.reset()
30043004

30053005
def _stop_inference(self, show_message: bool = True) -> None:
30063006
if self._rec_manager.is_active and self._backend_name == "dlc":
@@ -3020,7 +3020,9 @@ def _stop_inference(self, show_message: bool = True) -> None:
30203020
self._dlc_active = False
30213021
self._dlc_initialized = False
30223022
# Does NOT invoke the normal rec-stop/save hooks. Persistence is processor-dependent.
3023-
self._reset_active_pose_processor(reset_processor_plugin=True)
3023+
stopped = self._reset_active_pose_processor(reset_processor_plugin=True)
3024+
if not stopped:
3025+
self.statusBar().showMessage("Stopping pose inference after model init completed...", 5000)
30243026
self._last_pose = None
30253027
self._overlay_renderer.clear_runtime_state()
30263028
self._last_processor_vid_recording = False

‎dlclivegui/services/inference/models/poet/poet_processor.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,10 +210,15 @@ def get_pose(
210210
) -> np.ndarray | None:
211211
del frame_time
212212

213+
frame = np.asarray(frame)
214+
if frame.ndim != 3 or frame.shape[2] != 3:
215+
# FIXME @C-Achard 2026/08/28 - optionally convert here as well
216+
raise ValueError(f"Incorrect frame format, expected 3-channel BGR, got {frame.shape!r}")
217+
213218
torch = _require_torch()
214219

215220
with torch.inference_mode():
216-
return self._infer(frame, torch)
221+
return self._infer(np.ascontiguousarray(frame), torch)
217222

218223
def _infer(
219224
self,

‎dlclivegui/services/inference/processor.py‎

Lines changed: 75 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,12 @@ def configure(self, backend_factory: Callable[[], PoseBackend]) -> None:
5656
def is_configured(self) -> bool:
5757
return self._backend_factory is not None
5858

59-
def reset(self) -> None:
59+
def reset(self) -> bool:
6060
if not self._stop_worker():
61-
raise RuntimeError("Failed to stop worker thread")
61+
return False
62+
6263
self._backend = None
64+
6365
with self._stats_lock:
6466
self._frames_enqueued = 0
6567
self._frames_processed = 0
@@ -71,8 +73,10 @@ def reset(self) -> None:
7173
self._signal_emit_times.clear()
7274
self._total_process_times.clear()
7375

74-
def shutdown(self) -> None:
75-
self.reset()
76+
return True
77+
78+
def shutdown(self) -> bool:
79+
return self.reset()
7680

7781
def enqueue_frame(self, frame: np.ndarray, timestamp: float) -> None:
7882
if self._worker_thread is None:
@@ -214,83 +218,96 @@ def _process_frame(
214218

215219
self.frame_processed.emit()
216220

217-
def _worker_loop(self, init_frame: np.ndarray, init_timestamp: float) -> None:
221+
def _worker_loop(
222+
self,
223+
init_frame: np.ndarray,
224+
init_timestamp: float,
225+
) -> None:
218226
try:
219227
if self._backend_factory is None:
220228
raise RuntimeError("No backend configured.")
229+
221230
self._backend = self._backend_factory()
222231

232+
if self._stop_event.is_set():
233+
return
234+
223235
self._backend.init_inference(init_frame)
236+
237+
if self._stop_event.is_set():
238+
return
239+
224240
self.initialized.emit(True)
225241

226-
self._process_frame(init_frame, init_timestamp, time.perf_counter(), queue_wait_time=0.0)
242+
self._process_frame(
243+
init_frame,
244+
init_timestamp,
245+
time.perf_counter(),
246+
queue_wait_time=0.0,
247+
)
248+
227249
with self._stats_lock:
228250
self._frames_enqueued += 1
229251

252+
while not self._stop_event.is_set():
253+
queue_ref = self._queue
254+
if queue_ref is None:
255+
break
256+
257+
try:
258+
frame, timestamp, enqueued_at = queue_ref.get(timeout=0.05)
259+
except queue.Empty:
260+
continue
261+
262+
queue_wait_time = max(
263+
0.0,
264+
time.perf_counter() - enqueued_at,
265+
)
266+
267+
try:
268+
self._process_frame(
269+
frame,
270+
timestamp,
271+
enqueued_at,
272+
queue_wait_time=queue_wait_time,
273+
)
274+
except Exception as exc:
275+
logger.exception(
276+
"Pose inference failed",
277+
exc_info=exc,
278+
)
279+
self.error.emit(str(exc))
280+
finally:
281+
try:
282+
queue_ref.task_done()
283+
except ValueError:
284+
pass
285+
230286
except Exception as exc:
231-
logger.exception("Failed to initialize pose backend", exc_info=exc)
232-
self.error.emit(str(exc))
233-
self.initialized.emit(False)
287+
if not self._stop_event.is_set():
288+
logger.exception(
289+
"Failed to initialize pose backend",
290+
exc_info=exc,
291+
)
292+
self.error.emit(str(exc))
293+
self.initialized.emit(False)
234294

295+
finally:
235296
backend = self._backend
236297
self._backend = None
237298

238299
if backend is not None:
239300
try:
240301
backend.close()
241-
except Exception as exc:
242-
logger.exception("Failed to close backend", exc_info=exc)
302+
except Exception:
303+
logger.exception("Failed to close pose backend.")
243304

244305
self._queue = None
245-
self._worker_thread = None
246-
return
247306

248-
while True:
249-
if self._stop_event.is_set():
250-
if self._queue is not None:
251-
try:
252-
frame, ts, enq = self._queue.get_nowait()
253-
except queue.Empty:
254-
break
255-
else:
256-
try:
257-
self._process_frame(frame, ts, enq, queue_wait_time=0.0)
258-
except Exception as exc:
259-
logger.exception("Pose inference failed", exc_info=exc)
260-
self.error.emit(str(exc))
261-
finally:
262-
try:
263-
self._queue.task_done()
264-
except ValueError:
265-
pass
266-
continue
267-
268-
try:
269-
frame, ts, enq = self._queue.get(
270-
timeout=0.05,
271-
)
272-
except queue.Empty:
273-
break
274-
else:
275-
queue_wait_time = max(0.0, time.perf_counter() - enq)
276-
277-
try:
278-
self._process_frame(
279-
frame,
280-
ts,
281-
enq,
282-
queue_wait_time=queue_wait_time,
283-
)
284-
except Exception as exc:
285-
logger.exception("Pose inference failed", exc_info=exc)
286-
self.error.emit(str(exc))
287-
finally:
288-
try:
289-
self._queue.task_done()
290-
except ValueError:
291-
pass
307+
if self._worker_thread is threading.current_thread():
308+
self._worker_thread = None
292309

293-
logger.info("Pose worker thread exiting")
310+
logger.info("Pose worker thread exiting")
294311

295312

296313
def create_pose_processor(

0 commit comments

Comments
 (0)