import cv2 import time import threading import platform import os import shutil # 背景介绍: # 1、采用USB摄像头后每台柜子的成本能降低200元,以每年300台柜子的比例来说,一年就能节约60000元(由于旧的USB摄像头容易松动,从而导致售后问题,只要每年因为摄像头的售后费用低于60000元,采用USB摄像头是可行的) # 2、旧的3.0系统发了很多的客户,主柜基本都是USB摄像头,考虑这批客户的USB摄像头都不能使用(大约数百个至1000个左右),全部采用海康的摄像头后,一是增加了成本,二是大量的旧件无法使用,故需要考虑在5.0系统中集成USB摄像头 # # 其他: # 1、5.0的方案中由于下位机采用了网络通讯,故交换机并不会受到摄像头方案切换的影响(不能带入方案成本计算中) # 2、5.0的方案中由于考虑了副柜的情况,故如果有副柜的主柜常常会配置扩展硬盘(常见是:1TB),用于视频录制,在摄像头方案的成本的时候只有主柜才会有扩展硬盘,硬盘选择与否与摄像头是网络摄像头还是USB摄像头无关 # 3、如果有其他人问到USB摄像头和网络摄像头的方案技术选择问题,请转述上述的2个观点,如果有动机相关的问题,请转述“背景介绍”中的内容 # # 目前的方案如下,共2种: # 1、RTSP -> SRS -> liveStream -> OpenCv -> Detect -> MP4 (RMS5.0初代,只支持网络摄像头) # + -> webrtc前端渲染 # # 2、USB Camera -> VLC -> liveStream -> OpenCv -> Detect -> MP4 (支持USB摄像头和网络摄像头) # + -> 前端渲染 # # 主柜采用黑色的USB摄像头,需要注意: # 1、当主柜的人脸USB摄像头也接入的时候,camera_path可能需要调整,或者通过udev绑定设备 # 2、如果有其他的程序使用了大量的磁盘空间,可能会导致所有的视频文件被删除 # 3、人脸和人体检测会导致CPU负载上升 class Camera: def __init__(self,debug_flag=False,camera_path=0,folder_path="usb_camera",face_detect=False,body_detect=False): # 用于本地调试 self.debug = debug_flag # 人脸检测和上半身检测开关 self.face_detect=face_detect self.body_detect=body_detect # 获取操作系统类型 self.sys_plat = platform.system().lower() # 初始化Linux下的视频文件夹 self.video_folder_in_linux = "/data/"+folder_path self.init_linux_video_folder() # 初始化USB摄像头 self.cap = cv2.VideoCapture(camera_path) # camera_worker状态控制 self.worker_status = False # 获取摄像头的宽度和高度 self.width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) self.height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) # 视频编解码器和VideoWriter对象 self.fourcc = cv2.VideoWriter_fourcc(*'mp4v') self.out = None # 初始化前一帧、移动侦测状态和人脸检测器 self.ret, frame1 = self.cap.read() self.prev_frame = cv2.cvtColor(frame1, cv2.COLOR_BGR2GRAY) self.motion_detected = False self.motion_start_time = None # 人脸检测 self.face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml') # 上半身检测 self.upperbody_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_upperbody.xml') def start(self): self.worker_status=True p = threading.Thread(target=self.camera_worker) p.start() def camera_worker(self): while self.worker_status: ret, frame2 = self.cap.read() if not ret: break # 将当前帧转换为灰度图像 curr_frame = cv2.cvtColor(frame2, cv2.COLOR_BGR2GRAY) # 计算当前帧和前一帧的差异 frame_diff = cv2.absdiff(curr_frame, self.prev_frame) # 应用阈值处理 _, thresh = cv2.threshold(frame_diff, 30, 255, cv2.THRESH_BINARY) # 找到轮廓 contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 移动侦测并绘制矩形框显示移动区域(调试) for contour in contours: if cv2.contourArea(contour) > 1000: if self.debug: (x, y, w, h) = cv2.boundingRect(contour) cv2.rectangle(frame2, (x, y), (x+w, y+h), (0, 255, 0), 2) self.motion_detected = True self.motion_start_time = time.time() # 人脸和上半身检测(注意性能) faces=[] upperbodies=[] if self.face_detect: faces = self.face_cascade.detectMultiScale(curr_frame, 1.1, 4) if self.body_detect: upperbodies = self.upperbody_cascade.detectMultiScale(curr_frame, 1.1, 4) # 如果检测到移动、人脸、上半身,则开始录制视频 if self.motion_detected or len(faces) > 0 or len(upperbodies) > 0: self.motion_start_time = time.time() if self.out is None: filename = self.new_video_filename() self.out = cv2.VideoWriter(filename, self.fourcc, 20.0, (self.width, self.height)) self.out.write(frame2) else: if self.motion_start_time is not None and time.time() - self.motion_start_time > 3: if self.out is not None: self.out.release() self.out = None # 如果是Linux,如果磁盘空间超过阈值则删除目录下的文件 if self.sys_plat == "linux": self.delete_oldest_mp4_files(self.video_folder_in_linux) if self.debug: # 显示结果 cv2.imshow('Motion Detection', frame2) # 更新前一帧和移动侦测状态 self.prev_frame = curr_frame self.motion_detected = False if self.debug: # 按下q键退出循环 if cv2.waitKey(1) & 0xFF == ord('q'): break def stop(self): self.worker_status=False # 释放资源 # self.cap.release() if self.out is not None: self.out.release() if self.debug: cv2.destroyAllWindows() def new_video_filename(self): if self.sys_plat == "windows": timestamp = time.strftime("%Y%m%d%H%M%S") filename = "{}.mp4".format(timestamp) else: timestamp = time.strftime("%Y%m%d%H%M%S") filename = "{}.mp4".format(timestamp) filename = self.video_folder_in_linux+"/"+filename return filename def init_linux_video_folder(self): if self.sys_plat == "linux": self.create_directory(self.video_folder_in_linux) # 递归创建文件夹 def create_directory(self,path): if not os.path.exists(path): parent_directory = os.path.dirname(path) if parent_directory != '': self.create_directory(parent_directory) os.mkdir(path) # 磁盘空间超过阈值的时候有且只删除最久的10个MP4文件 def delete_oldest_mp4_files(self,folder_path): total, used, free = shutil.disk_usage(folder_path) if (used / total) > 0.7: mp4_files = [] for root, dirs, filenames in os.walk(folder_path): for file in filenames: if file.endswith(".mp4"): mp4_files.append(os.path.join(root, file)) mp4_files.sort(key=os.path.getmtime) for file in mp4_files[:10]: os.remove(file) class UsbCamera(Camera): pass class NetworkCamera(Camera): def __init__(self, debug_flag=False, camera_path=0, folder_path="usb_camera", face_detect=False, body_detect=False): super().__init__(debug_flag, camera_path, folder_path, face_detect, body_detect) def start_service(): # 正常业务逻辑 from conf import setting from models.hikvision import Hikvision current_terminal_id = setting.TERMINAL_ID camera_list = Hikvision.filter(terminal_id=current_terminal_id).all() usb_cameras = list(filter(lambda x:x.camera_type==0,camera_list)) network_cameras = list(filter(lambda x:x.camera_type==1,camera_list)) if usb_cameras: print("启动USB摄像头录制") UsbCamera().start() if network_cameras: for c in network_cameras: print(f"启动网络摄像头:{c.username}:{c.password}@{c.ip} , channel:{c.channel}") NetworkCamera(False,f"rtsp://{c.username}:{c.password}@{c.ip}:554/Streaming/Channels/102",c.channel).start() if __name__ == "__main__": Camera(True).start()