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
|