diff --git a/config.yaml b/config.yaml index 6162e80..b15c7ce 100644 --- a/config.yaml +++ b/config.yaml @@ -13,7 +13,7 @@ llm: common: # 工作时间段:支持多个时间段,格式为 [开始小时, 开始分钟, 结束小时, 结束分钟] # 8:30-11:00, 12:00-17:30 - working_hours: + working_hours: - [8, 30, 11, 0] # 8:30-11:00 - [12, 0, 17, 30] # 12:00-17:30 process_every_n_frames: 3 # 每3帧处理1帧(用于人员离岗) diff --git a/monitor.py b/monitor.py index b14053d..faa680d 100644 --- a/monitor.py +++ b/monitor.py @@ -33,11 +33,19 @@ def save_alert_image(frame, cam_id, roi_name, alert_type, alert_info=""): os.makedirs(alert_dir, exist_ok=True) - # 生成文件名:摄像头ID_ROI名称_时间戳.jpg + # 生成文件名:根据告警类型使用不同的命名方式 timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") # 清理文件名中的特殊字符 safe_roi_name = roi_name.replace("/", "_").replace("\\", "_").replace(":", "_") - filename = f"{cam_id}_{safe_roi_name}_{timestamp}.jpg" + + # 对于入侵告警,使用告警类型+ROI名称;对于离岗告警,使用ROI名称 + if alert_type == "入侵": + # 周界入侵:使用"入侵_区域名称"格式 + filename = f"{cam_id}_入侵_{safe_roi_name}_{timestamp}.jpg" + else: + # 离岗:使用原有格式 + filename = f"{cam_id}_{safe_roi_name}_{timestamp}.jpg" + filepath = os.path.join(alert_dir, filename) # 保存图片 @@ -139,12 +147,12 @@ class LLMClient: 请根据以下规则生成结构化响应: ### 【判定标准】 -✅ **本单位物业员工**需同时满足: -1. **清晰可见的正式工牌**(胸前佩戴,含照片/姓名/公司LOGO) -2. **穿着标准制服**(如:黄蓝工程服、白衬衫+黑领带、蓝色清洁装、浅色客服装等) +✅ **本单位物业员工**需满足下列条件之一: +1. **清晰可见的正式工牌**(胸前佩戴) +2. **穿着标准制服**(如:带有白色反光条的深色工程服、黄蓝工程服、白衬衫+黑领带、蓝色清洁装、浅色客服装等) 3. **行为符合岗位规范**(如巡检、维修、清洁,无徘徊、张望、翻越) -> ⚠️ 仅满足部分条件(如仅有安全帽、仅有类似颜色衣服、工牌不可见)→ 视为非员工。 +> 注意: 满足部分关键条件(如戴有安全帽、穿有工作人员服饰、带有工牌)→ 视为员工,不生成告警。 ### 【输出规则】 #### 情况1:ROI区域内**无人** @@ -174,11 +182,11 @@ class LLMClient: ▶ 示例2(员工): 🟢无异常:检测到本单位工作人员正常作业。 -1名工程人员穿黄蓝工服佩戴工牌在高配间巡检。 +1名工程人员穿带有反光条的深蓝色工服在高配间巡检。 ▶ 示例3(非员工): 🚨天台区域入侵告警:检测到疑似非工作人员,请立即核查。 -1人穿黑色外套未佩戴工牌进入天台区域并短暂停留。 +1人穿绿色外套未佩戴工牌进入天台区域。 --- 请分析摄像头{cam_id}的{roi_name}区域,按照上述格式输出结果。""" @@ -308,25 +316,53 @@ class ThreadedFrameReader: def __init__(self, cam_id, rtsp_url): self.cam_id = cam_id self.rtsp_url = rtsp_url - self.cap = cv2.VideoCapture(rtsp_url, cv2.CAP_FFMPEG) - self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) + self._lock = threading.Lock() # 添加锁保护VideoCapture访问 + self.cap = None + try: + self.cap = cv2.VideoCapture(rtsp_url, cv2.CAP_FFMPEG) + if self.cap.isOpened(): + self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) + else: + print(f"[{cam_id}] 警告:无法打开视频流: {rtsp_url}") + except Exception as e: + print(f"[{cam_id}] 初始化VideoCapture失败: {e}") self.q = queue.Queue(maxsize=2) self.running = True self.thread = threading.Thread(target=self._reader, daemon=True) self.thread.start() def _reader(self): - while self.running: - ret, frame = self.cap.read() - if not ret: - time.sleep(0.1) - continue - if self.q.full(): - try: - self.q.get_nowait() - except queue.Empty: - pass - self.q.put(frame) + """读取帧的线程函数""" + try: + while self.running: + with self._lock: + if self.cap is None or not self.cap.isOpened(): + break + ret, frame = self.cap.read() + + if not ret: + time.sleep(0.1) + continue + + if self.q.full(): + try: + self.q.get_nowait() + except queue.Empty: + pass + self.q.put(frame) + except Exception as e: + print(f"[{self.cam_id}] 读取帧线程异常: {e}") + finally: + # 确保资源释放(使用锁保护) + with self._lock: + if self.cap is not None: + try: + if self.cap.isOpened(): + self.cap.release() + except Exception as e: + print(f"[{self.cam_id}] 释放VideoCapture时出错: {e}") + finally: + self.cap = None def read(self): if not self.q.empty(): @@ -334,8 +370,26 @@ class ThreadedFrameReader: return False, None def release(self): + """释放资源,等待线程结束""" + if not self.running: + return # 已经释放过了 + self.running = False - self.cap.release() + + # 等待线程结束,最多等待3秒 + if self.thread.is_alive(): + self.thread.join(timeout=3.0) + if self.thread.is_alive(): + print(f"[{self.cam_id}] 警告:读取线程未能在3秒内结束") + + # 清空队列 + while not self.q.empty(): + try: + self.q.get_nowait() + except queue.Empty: + break + + # VideoCapture的释放由_reader线程的finally块处理,这里不再重复释放 class MultiCameraMonitor: @@ -497,10 +551,39 @@ class MultiCameraMonitor: self.stop() def stop(self): + """停止监控,清理所有资源""" + print("正在停止监控系统...") self.running = False - for reader in self.frame_readers.values(): - reader.release() - cv2.destroyAllWindows() + + # 等待推理线程结束 + if hasattr(self, 'inference_thread') and self.inference_thread.is_alive(): + self.inference_thread.join(timeout=2.0) + + if hasattr(self, 'perimeter_thread') and self.perimeter_thread.is_alive(): + self.perimeter_thread.join(timeout=2.0) + + # 释放所有摄像头资源 + for cam_id, reader in self.frame_readers.items(): + try: + print(f"正在释放摄像头 {cam_id}...") + reader.release() + except Exception as e: + print(f"释放摄像头 {cam_id} 时出错: {e}") + + # 关闭所有窗口 + try: + cv2.destroyAllWindows() + except: + pass + + # 强制清理(如果还有线程在运行) + import sys + import os + if sys.platform == 'win32': + # Windows下可能需要额外等待 + time.sleep(0.5) + + print("监控系统已停止") class ROILogic: @@ -558,7 +641,7 @@ class ROILogic: # 周界入侵相关状态(如果没有points,使用整张画面) if '周界入侵' in self.algorithms: self.perimeter_last_check_time = 0 - self.perimeter_alert_cooldown = 60 # 周界入侵告警冷却60秒 + self.perimeter_alert_cooldown = 120 # 周界入侵告警冷却120秒(2分钟) if self.use_full_frame: print(f"[{cam_id}] 提示:{self.roi_name} 周界入侵算法将使用整张画面进行检测") @@ -903,13 +986,13 @@ class CameraLogic: # 非工作人员 print(f"[{self.cam_id}] [{roi.roi_name}] 🚨 周界入侵告警!检测到非工作人员(检测区域:{area_desc})") print(f"大模型判断结果: {result}") - # 保存告警图片 + # 保存告警图片(使用区域描述作为名称,更清晰) save_alert_image( frame.copy(), self.cam_id, - roi.roi_name, + area_desc, # 使用area_desc而不是roi.roi_name "入侵", - f"检测区域: {area_desc}\n大模型判断结果:\n{result}" + f"检测区域: {area_desc}\nROI名称: {roi.roi_name}\n大模型判断结果:\n{result}" ) else: # 工作人员 @@ -919,13 +1002,13 @@ class CameraLogic: # 没有大模型时,直接告警 area_desc = "整张画面" if roi.use_full_frame else roi.roi_name print(f"[{self.cam_id}] [{roi.roi_name}] 🚨 周界入侵告警!检测到人员进入(检测区域:{area_desc})") - # 保存告警图片 + # 保存告警图片(使用区域描述作为名称,更清晰) save_alert_image( frame.copy(), self.cam_id, - roi.roi_name, + area_desc, # 使用area_desc而不是roi.roi_name "入侵", - f"检测区域: {area_desc}\n检测到人员进入" + f"检测区域: {area_desc}\nROI名称: {roi.roi_name}\n检测到人员进入" ) self.display_frame = frame.copy() @@ -1014,12 +1097,40 @@ class CameraLogic: def main(): + import signal + import sys + parser = argparse.ArgumentParser() parser.add_argument("--config", default="config.yaml", help="配置文件路径") args = parser.parse_args() - monitor = MultiCameraMonitor(args.config) - monitor.run() + monitor = None + try: + monitor = MultiCameraMonitor(args.config) + + # 注册信号处理,确保优雅退出 + def signal_handler(sig, frame): + print("\n收到退出信号,正在关闭...") + if monitor: + monitor.stop() + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + if sys.platform != 'win32': + signal.signal(signal.SIGTERM, signal_handler) + + monitor.run() + except KeyboardInterrupt: + print("\n收到键盘中断,正在关闭...") + except Exception as e: + print(f"程序异常: {e}") + import traceback + traceback.print_exc() + finally: + if monitor: + monitor.stop() + # 确保进程退出 + sys.exit(0) if __name__ == "__main__":