From 85bad4aa78f8451f06921f8e63ff3035fbde1b00 Mon Sep 17 00:00:00 2001 From: zqc <835569504@qq.com> Date: Sat, 28 Feb 2026 14:49:11 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8C=E6=AD=A5=E5=8D=A1=E7=82=B9=E7=AE=97?= =?UTF-8?q?=E6=B3=95=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- biz/checkpoint/checkpoint_biz.py | 206 +++++++++++++++++++------------ 1 file changed, 127 insertions(+), 79 deletions(-) diff --git a/biz/checkpoint/checkpoint_biz.py b/biz/checkpoint/checkpoint_biz.py index 0aa4d93..1d0e43c 100644 --- a/biz/checkpoint/checkpoint_biz.py +++ b/biz/checkpoint/checkpoint_biz.py @@ -52,18 +52,19 @@ class KadianDetector: self.params = params if params is not None else {} # 模型加载 - self.detector = YOLOv8_ONNX(DETECT_MODEL_PATH, conf_threshold=0.25, iou_threshold=0.45, + self.detector = YOLOv8_ONNX(DETECT_MODEL_PATH, conf_threshold=0.15, iou_threshold=0.65, input_size=PERSON_CAR_INPUT_SIZE) - self.pose_detector = YOLOv8_Pose_ONNX(POSE_MODEL_PATH, conf_threshold=0.7, iou_threshold=0.6, + self.pose_detector = YOLOv8_Pose_ONNX(POSE_MODEL_PATH, conf_threshold=0.45, iou_threshold=0.6, input_size=POSE_INPUT_SIZE) # Tracker class TrackerArgs: - track_thresh = 0.25 - track_buffer = 30 - match_thresh = 0.8 - mot20 = False - self.tracker = BYTETracker(TrackerArgs(), frame_rate=10.0) + track_thresh = 0.2 + track_buffer = 60 + match_thresh = 0.9 + mot20 = True + + self.tracker = BYTETracker(TrackerArgs(), frame_rate=RTSP_TARGET_FPS) self.track_role = {} @@ -78,28 +79,31 @@ class KadianDetector: # ========================================== # 1. 业务判定时间阈值 - self.TIME_THRESHOLD_ONLY_ONE = 3.0 # 单人单检判定时长 - self.TIME_THRESHOLD_NOBODY = 3.0 # 无人检查判定时长 + self.TIME_THRESHOLD_ONLY_ONE = 10.0 # 单人单检判定时长 + self.TIME_THRESHOLD_NOBODY = 10.0 # 无人检查判定时长 # 后备箱检查判定阈值 - self.TIME_THRESHOLD_TRUNK_OPEN = 0.5 + self.TIME_THRESHOLD_TRUNK_OPEN = 0.1 # 新增:手机检测判定阈值 - self.TIME_THRESHOLD_PHONE = 1.0 # 手机检测持续1秒(30帧 @30fps) - self.TIME_TOLERANCE_PHONE = 0.5 # 手机丢失缓冲时间(防抖动) + self.TIME_THRESHOLD_PHONE = 3.0 # 手机检测持续1秒(30帧 @30fps) + self.TIME_TOLERANCE_PHONE = 1.5 # 手机丢失缓冲时间(防抖动) # 新增:制服检测判定阈值 - self.TIME_THRESHOLD_UNIFORM = 1.0 # 制服不合规判定时长 - self.TIME_TOLERANCE_UNIFORM = 0.5 # 制服合规恢复缓冲时间 - - # 车辆最小停留时间阈值 (小于此时间视为无人检查/直接通过) - self.TIME_THRESHOLD_CAR_MIN_DURATION = 3.0 + self.TIME_THRESHOLD_UNIFORM = 2.0 # 制服不合规判定时长 + self.TIME_TOLERANCE_UNIFORM = 1.0 # 制服合规恢复缓冲时间 # 2. Person 丢帧缓冲 - self.TIME_TOLERANCE_PERSON = 1.0 + self.TIME_TOLERANCE_PERSON = 3.0 + + # 车辆最小停留时间阈值 (小于此时间视为无人检查/直接通过) + self.TIME_THRESHOLD_CAR_MIN_DURATION = 10.0 # 3. Car 丢帧/ID维持缓冲 - self.TIME_TOLERANCE_CAR = 0.5 + self.TIME_TOLERANCE_CAR = 10.0 + + # 4 OnlyOne 丢帧缓冲 + self.TIME_TOLERANCE_ONLY_ONE_DURATION = 3.0 # --- 计算对应的帧数阈值 --- self.frame_thresh_one = int(self.TIME_THRESHOLD_ONLY_ONE * self.fps) @@ -118,6 +122,7 @@ class KadianDetector: self.frame_buffer_limit_person = int(self.TIME_TOLERANCE_PERSON * self.fps) self.frame_buffer_limit_car = int(self.TIME_TOLERANCE_CAR * self.fps) + self.frame_buffer_limit_onlyOne = int(self.TIME_TOLERANCE_ONLY_ONE_DURATION * self.fps) logger.info(f"\n超参数设置:") logger.info(f" FPS: {self.fps:.2f}") @@ -127,22 +132,22 @@ class KadianDetector: logger.info(f" 手机丢失缓冲帧数: {self.frame_buffer_phone} 帧") logger.info(f" 判定 'Uniform Invalid' 需连续检测: {self.frame_thresh_uniform} 帧") logger.info(f" 制服合规恢复缓冲帧数: {self.frame_buffer_uniform} 帧") - logger.info(f" 判定 'Too Fast' (视为Nobody) 最小停留: {self.frame_thresh_car_min_duration} 帧") + logger.info(f" 判定 'Too Fast' 最小停留: {self.frame_thresh_car_min_duration} 帧") self.onlyone_counter = 0 - self.onlyone_lost_counter = 0 - self.onlyone_buffer_limit = self.frame_buffer_limit_person # 10帧(1秒) + # self.onlyone_lost_counter = 0 + # self.onlyone_buffer_limit = self.frame_buffer_limit_person # 10帧(1秒) self.onlyone_thresh = self.frame_thresh_one # 30帧(3秒) self.nobody_counter = 0 self.nobody_present_counter = 0 - self.nobody_buffer_limit = 10 # 10帧(1秒) + self.nobody_buffer_limit = self.frame_buffer_limit_onlyOne self.nobody_thresh = self.frame_thresh_nobody # 20帧(2秒) self.current_frame_idx = 0 - self.cnt_frame_one_person = 0 - self.cnt_frame_nobody = 0 - self.cnt_missing_buffer_person = 0 + + self.ignore_show_seconds = 0.5 # 未检测的警告显示时长 + self.openTrunk_show_seconds = 0.5 # 打开后备箱的警告显示时长 # 手机检测状态变量(独立于车辆) self.phone_detection_frames = 0 # 连续检测到手机的帧数 @@ -161,7 +166,7 @@ class KadianDetector: # 违规车辆记录 (后备箱未检) self.unchecked_trunk_alerts = {} - # 违规车辆记录 (通过过快 -> 归类为 Nobody) + # 违规车辆记录 (通过过快 -> 归类为 Ignore) self.fast_pass_alerts = {} def _get_roi_points(self, frame_width: int, frame_height: int): @@ -182,10 +187,10 @@ class KadianDetector: # 强制转为 int32(关键!解决 OpenCV 断言错误) return roi_abs.astype(np.int32) - def check_point_in_roi(self,roi_points, point): + def check_point_in_roi(self, roi_points, point): return cv2.pointPolygonTest(roi_points, point, False) >= 0 - def compute_iou(self,boxA, boxB): + def compute_iou(self, boxA, boxB): # box = [x1, y1, x2, y2] xA = max(boxA[0], boxB[0]) yA = max(boxA[1], boxB[1]) @@ -226,6 +231,53 @@ class KadianDetector: x1, y1, x2, y2 = box return x1 < px < x2 and y1 < py < y2 + def is_pose_inside_detector_person(self, pose_bbox, dets_xyxy, dets_roles): + """ + 判断一个 pose 人是否位于 detector 的 person 框内部(中心点匹配) + + 参数: + pose_bbox: [x1, y1, x2, y2] + dets_xyxy: detector 输出的所有 bbox 列表 + dets_roles: 对应的类别列表(如 "person", "car"...) + + 返回: + True -> 在某个人体框内部 + False -> 不在任何人体框内部 + """ + + px1, py1, px2, py2 = pose_bbox + cx, cy = (px1 + px2) // 2, (py1 + py2) // 2 + + for box, role in zip(dets_xyxy, dets_roles): + if role != "person": + continue + + dx1, dy1, dx2, dy2 = map(int, box) + + # 判断中心点是否在 detector person 框内 + if dx1 <= cx <= dx2 and dy1 <= cy <= dy2: + return True + + return False + + def count_pose_inside_detector_person(self, pose_results, dets_xyxy, dets_roles): + """ + 统计有多少个pose框在detector person框内部 + + 参数: + pose_results: pose检测结果列表,每个元素为字典,包含'bbox'键,值为[x1, y1, x2, y2] + dets_xyxy: detector输出的所有bbox列表 + dets_roles: 对应的类别列表(如 "person", "car"...) + + 返回: + int: 在detector person框内部的pose框数量 + """ + count = 0 + for pose in pose_results: + pose_bbox = pose['bbox'] # [x1, y1, x2, y2] + if self.is_pose_inside_detector_person(pose_bbox, dets_xyxy, dets_roles): + count += 1 + return count def process_frame(self, frame, camera_id: int, timestamp: float) -> Dict[str, Any]: h, w = frame.shape[:2] @@ -309,8 +361,8 @@ class KadianDetector: # IoU 匹配角色 # if tid not in track_role and dets_xyxy: - REVALIDATE_FRAME_INTERVAL = 10 - #if tid not in self.track_role: + REVALIDATE_FRAME_INTERVAL = 10 + # if tid not in self.track_role: if (self.current_frame_idx % REVALIDATE_FRAME_INTERVAL == 0) or (tid not in self.track_role): best_iou = 0 best_role = "unknown" @@ -346,7 +398,7 @@ class KadianDetector: color = None label = None - if self.check_point_in_roi(roi_points_int32,(cx, cy)): + if self.check_point_in_roi(roi_points_int32, (cx, cy)): if cls_id == 0: # Car color = (0, 255, 0) @@ -418,23 +470,26 @@ class KadianDetector: x1, y1, x2, y2 = pose['bbox'][0], pose['bbox'][1], pose['bbox'][2], pose['bbox'][3] cx, cy = (x1 + x2) // 2, (y1 + y2) // 2 # 判断中心点是否在ROI内 - if self.check_point_in_roi(roi_points_int32,(cx, cy)): + if self.check_point_in_roi(roi_points_int32, (cx, cy)): self.pose_person_count += 1 + # 统计pose框在detector person框内部的数量 + pose_inside_count = self.count_pose_inside_detector_person(pose_results, dets_xyxy, dets_roles) + # ========================================== # 5. 关联分析: 哪个后备箱属于哪辆车? # ========================================== for car_info in current_cars: - c_id = car_info['id'] - c_box = car_info['box'] + c_id = car_info['id'] # 车的id + c_box = car_info['box'] # 车的框 - trunk_found_for_this_car = False + trunk_found_for_this_car = False # 开后备箱标记 for t_pt in current_trunks: - if self.is_point_in_box(t_pt, c_box): + if self.is_point_in_box(t_pt, c_box): # 如果开后备箱的框在车的框内,就设置开后备箱标记为true trunk_found_for_this_car = True break - if trunk_found_for_this_car: + if trunk_found_for_this_car: # 如果当前车辆的开后备箱标记为true了,就设置开了后备箱的帧数+1,凑够了判断“开后备箱”这个动作的帧数之后,就设置该车"已检查" self.roi_car_registry[c_id]['trunk_frames'] += 1 if self.roi_car_registry[c_id]['trunk_frames'] >= self.frame_thresh_trunk_valid: self.roi_car_registry[c_id]['is_checked'] = True @@ -501,6 +556,7 @@ class KadianDetector: cars_to_remove = [] for car_id, info in self.roi_car_registry.items(): + # 遍历所有车辆,如果当前帧时间-该车辆最后可见的时间得到的值大于车辆消失时间阈值的话,就把该车添加到移除列表中,否则添加到活跃列表中 last_seen = info['last_seen'] if (self.current_frame_idx - last_seen) <= self.frame_buffer_limit_car: @@ -510,19 +566,25 @@ class KadianDetector: # 执行删除 并 检查违规 for car_id in cars_to_remove: + # 遍历所有移除列表中的车辆, + # 如果该车辆最后出现时间-最早出现时间的值小于车辆最小存在时间,则判断为ignore, + # 如果该车辆的“已检查”标记为true,则 + # 最后在所有车辆列表中删除该车辆 + car_info = self.roi_car_registry[car_id] duration_frames = car_info['last_seen'] - car_info['first_seen'] - # 情况1:通过时间太短 -> 归类为 Nobody (Too Fast) + # 情况1:通过时间太短 -> 归类为 Ignore (Too Fast) if duration_frames < self.frame_thresh_car_min_duration: - logger.warning(f"ALARM: Car {car_id} passed too fast -> Regarded as Nobody Checked!") - self.fast_pass_alerts[car_id] = self.current_frame_idx + int(3.0 * self.fps) + logger.warning(f"ALARM: Car {car_id} passed too fast -> Regarded as Ignore Checked!") + self.fast_pass_alerts[car_id] = self.current_frame_idx + int(self.ignore_show_seconds * self.fps) # 情况2:时间够长,但没检查后备箱 -> Unchecked Trunk elif not car_info['is_checked']: logger.warning(f"ALARM: Car {car_id} left without checking trunk!") - self.unchecked_trunk_alerts[car_id] = self.current_frame_idx + int(3.0 * self.fps) + self.unchecked_trunk_alerts[car_id] = self.current_frame_idx + int( + self.openTrunk_show_seconds * self.fps) del self.roi_car_registry[car_id] @@ -563,53 +625,39 @@ class KadianDetector: # ========================================== # 9. 业务逻辑判定 (Only One / Nobody) - 重构版 # ========================================== - if effective_car_count >= 0: # 只要没人就检测,不用等到来了车再检测 + if effective_car_count >= 0: # 只要没人就检测,不用等到来了车再检测 # ----- 定义条件 ----- - onlyone_condition = (current_roi_person_count == 1 and self.pose_person_count == 1) + onlyone_condition = (pose_inside_count == 1) nobody_condition = (current_roi_person_count == 0 and self.pose_person_count == 0) # ----- Onlyone 计数器更新 ----- - if onlyone_condition: + if onlyone_condition: # 如果骨骼点和检测框都检测到了只有一个人时,onlyone+1,当onlyone累计够了之后触发报警 self.onlyone_counter += 1 - self.onlyone_lost_counter = 0 - else: - if self.onlyone_counter > 0: - self.onlyone_lost_counter += 1 - if self.onlyone_lost_counter > self.onlyone_buffer_limit: - self.onlyone_counter = 0 - self.onlyone_lost_counter = 0 - # 没有累积的 Onlyone 则不做任何事 + # self.onlyone_lost_counter = 0 + elif current_roi_person_count > 1 or self.pose_person_count > 1: + self.onlyone_counter = 0 + # if self.onlyone_counter > 0: + # self.onlyone_lost_counter += 1 + # if self.onlyone_lost_counter > self.onlyone_buffer_limit: + # self.onlyone_counter = 0 + # self.onlyone_lost_counter = 0 # ----- Nobody 计数器更新 ----- if nobody_condition: self.nobody_counter += 1 - self.nobody_present_counter = 0 - else: - if self.nobody_counter > 0: - self.nobody_present_counter += 1 - if self.nobody_present_counter > self.nobody_buffer_limit: - self.nobody_counter = 0 - self.nobody_present_counter = 0 - # 没有累积的 Nobody 则不做任何事 - - # ----- 准备显示状态文字(可选)----- - if self.onlyone_counter > 0: - if onlyone_condition: - status_text = f"OnlyOne: {self.onlyone_counter}/{self.onlyone_thresh}" - else: - status_text = f"OnlyOne Lost: {self.onlyone_lost_counter}/{self.onlyone_buffer_limit}" - elif self.nobody_counter > 0: - if nobody_condition: - status_text = f"Nobody: {self.nobody_counter}/{self.nobody_thresh}" - else: - status_text = f"Nobody Interrupted: {self.nobody_present_counter}/{self.nobody_buffer_limit}" - else: - status_text = "" + # self.nobody_present_counter = 0 + elif current_roi_person_count > 0 or self.pose_person_count > 0: + self.nobody_counter = 0 + # if self.nobody_counter > 0: + # self.nobody_present_counter += 1 + # if self.nobody_present_counter > self.nobody_buffer_limit: + # self.nobody_counter = 0 + # self.nobody_present_counter = 0 else: # 无活跃车辆,清零所有计数器 self.onlyone_counter = 0 - self.onlyone_lost_counter = 0 + # self.onlyone_lost_counter = 0 self.nobody_counter = 0 self.nobody_present_counter = 0 @@ -686,7 +734,7 @@ class KadianDetector: if self.phone_alert_active: # 可以显示检测的持续时间 duration_seconds = self.phone_detection_frames / self.fps - #sub_text = f"Detected for {duration_seconds:.1f}s" + # sub_text = f"Detected for {duration_seconds:.1f}s" current_frame_alerts.append( { 'time': current_time_sec, @@ -699,7 +747,7 @@ class KadianDetector: # E. 新增:显示 Unvaild Uniform!! if self.uniform_alert_active: # 显示具体数量差异 - diff = self.pose_person_count - current_roi_person_count + # diff = self.pose_person_count - current_roi_person_count #sub_text = f"Missing {diff} uniform(s)" current_frame_alerts.append( { @@ -757,7 +805,7 @@ class KadianDetector: return { "image": frame, - "alerts":current_frame_alerts + "alerts": current_frame_alerts, }