video_monitor/app/analysis/remote_detector.py

184 lines
6.7 KiB
Python
Raw Normal View History

2026-08-30 22:22:11 +08:00
"""远程推理代理 — 摄像头子进程通过 Queue 向主进程 InferenceProcessPool 发起推理"""
import logging
import threading
import uuid
logger = logging.getLogger("analysis.remote_detector")
# 同一 resp_queue 只能有一个 drain 线程,否则多 RemoteDetector 会抢响应导致丢包
_drainers = {}
_drainers_lock = threading.Lock()
class _SharedResponseDrainer(object):
def __init__(self, resp_queue):
self._resp_q = resp_queue
self._pending = {}
self._lock = threading.Lock()
self._started = False
def ensure_started(self):
if self._started:
return
self._started = True
threading.Thread(
target=self._loop, name="remote-det-drain-%s" % id(self._resp_q), daemon=True,
).start()
def register(self, req_id):
evt = {"event": threading.Event(), "resp": None}
with self._lock:
self._pending[req_id] = evt
return evt
def unregister(self, req_id):
with self._lock:
self._pending.pop(req_id, None)
def _loop(self):
import queue as _q
while True:
try:
msg = self._resp_q.get(timeout=1.0)
except _q.Empty:
continue
if not msg:
continue
req_id = msg.get("req_id")
with self._lock:
item = self._pending.pop(req_id, None)
if item:
item["resp"] = msg
item["event"].set()
elif req_id:
logger.debug("remote_detector: 无匹配 pending req_id=%s", req_id)
def _get_drainer(resp_queue):
key = id(resp_queue)
with _drainers_lock:
drainer = _drainers.get(key)
if drainer is None:
drainer = _SharedResponseDrainer(resp_queue)
_drainers[key] = drainer
return drainer
class RemoteDetector(object):
ENGINE_NAME = "remote_pool"
2026-09-04 18:16:14 +08:00
def __init__(self, algorithm_spec, req_queue, resp_queue, timeout=35.0,
response_channel=None):
2026-08-30 22:22:11 +08:00
self._spec = algorithm_spec
self._req_q = req_queue
self._resp_q = resp_queue
self._timeout = timeout
2026-09-04 18:16:14 +08:00
self._response_channel = response_channel
2026-08-30 22:22:11 +08:00
self._drainer = _get_drainer(resp_queue)
def ready(self):
return self._req_q is not None and self._resp_q is not None
def load(self):
return True
def detect(self, frame):
if not self.ready():
2026-09-04 18:16:14 +08:00
raise RuntimeError("共享推理队列未就绪")
2026-08-30 22:22:11 +08:00
self._drainer.ensure_started()
try:
import cv2
2026-09-04 18:16:14 +08:00
orig_h, orig_w = frame.shape[:2]
inference_frame = self._maybe_downscale(frame)
infer_h, infer_w = inference_frame.shape[:2]
# 远处人员框很小,低质量 JPEG 会在 YOLO 前抹掉轮廓95 在当前单路 CPU
# 场景仍可控,并显著缩小与直接单图推理的差异。
ok, buf = cv2.imencode(".jpg", inference_frame, [int(cv2.IMWRITE_JPEG_QUALITY), 95])
2026-08-30 22:22:11 +08:00
if not ok:
2026-09-04 18:16:14 +08:00
raise RuntimeError("推理帧编码失败")
2026-08-30 22:22:11 +08:00
jpeg = buf.tobytes()
2026-09-04 18:16:14 +08:00
except Exception as e:
raise RuntimeError("推理帧编码失败: %s" % e) from e
2026-08-30 22:22:11 +08:00
req_id = str(uuid.uuid4())
evt = self._drainer.register(req_id)
try:
self._req_q.put({
"req_id": req_id,
"algorithm": self._spec,
"jpeg": jpeg,
2026-09-04 18:16:14 +08:00
"response_channel": self._response_channel,
2026-08-30 22:22:11 +08:00
}, timeout=2.0)
2026-09-04 18:16:14 +08:00
except Exception as e:
2026-08-30 22:22:11 +08:00
self._drainer.unregister(req_id)
2026-09-04 18:16:14 +08:00
raise RuntimeError("推理请求入队失败: %s" % e) from e
2026-08-30 22:22:11 +08:00
if not evt["event"].wait(timeout=self._timeout):
self._drainer.unregister(req_id)
logger.warning("RemoteDetector 推理超时 algo=%s", self._spec.get("name"))
2026-09-04 18:16:14 +08:00
raise TimeoutError("共享推理响应超时(%ss" % self._timeout)
2026-08-30 22:22:11 +08:00
resp = evt.get("resp") or {}
if not resp.get("ok"):
2026-09-04 18:16:14 +08:00
raise RuntimeError(resp.get("error") or "共享推理失败")
detections = resp.get("detections") or []
if infer_w != orig_w or infer_h != orig_h:
detections = self._restore_coordinates(
detections, float(orig_w) / infer_w, float(orig_h) / infer_h,
)
return detections
@staticmethod
def _restore_coordinates(detections, scale_x, scale_y):
"""把共享推理缩图坐标恢复到解码原帧坐标系。"""
result = []
for source in detections or []:
item = dict(source)
box = item.get("box")
if isinstance(box, (list, tuple)) and len(box) >= 4:
item["box"] = [float(box[0]) * scale_x, float(box[1]) * scale_y,
float(box[2]) * scale_x, float(box[3]) * scale_y]
keypoints = item.get("keypoints")
if isinstance(keypoints, list):
item["keypoints"] = [
[float(p[0]) * scale_x, float(p[1]) * scale_y] + list(p[2:])
if isinstance(p, (list, tuple)) and len(p) >= 2 else p
for p in keypoints
]
for polygon_key in ("polygon", "mask", "segments"):
polygon = item.get(polygon_key)
if isinstance(polygon, list):
item[polygon_key] = RemoteDetector._scale_polygon(polygon, scale_x, scale_y)
result.append(item)
return result
@staticmethod
def _scale_polygon(value, scale_x, scale_y):
scaled = []
for point in value:
if (isinstance(point, (list, tuple)) and len(point) >= 2 and
isinstance(point[0], (int, float)) and isinstance(point[1], (int, float))):
scaled.append([float(point[0]) * scale_x, float(point[1]) * scale_y] + list(point[2:]))
elif isinstance(point, list):
scaled.append(RemoteDetector._scale_polygon(point, scale_x, scale_y))
else:
scaled.append(point)
return scaled
2026-08-30 22:22:11 +08:00
def _maybe_downscale(self, frame):
try:
import cv2
h, w = frame.shape[:2]
max_side = max(
int(self._spec.get("input_width", 640) or 640),
int(self._spec.get("input_height", 640) or 640),
640,
2026-09-04 18:16:14 +08:00
) * 4
2026-08-30 22:22:11 +08:00
longest = max(h, w)
if longest <= max_side:
return frame
scale = float(max_side) / float(longest)
nw, nh = max(1, int(w * scale)), max(1, int(h * scale))
return cv2.resize(frame, (nw, nh), interpolation=cv2.INTER_AREA)
except Exception:
return frame