import time import copy import importlib.util import datetime import time import threading import cv2 import ffmpeg import subprocess import select import multiprocessing from collections import deque from myutils.MyLogger_logger import LogHandler from core.CapManager import mCap from core.ACLModelManager import ACLModeManger from model.plugins.ModelBase import ModelBase from core.WarnManager import WarnData from core.DataStruct import ModelinData,ModeloutData from myutils.MyDeque import MyDeque from myutils.ConfigManager import myCongif from myutils.mydvpp import bgr_to_yuv420 class ChannelData: def __init__(self,channel_id,deque_length,icount_max,warnM): self.logger = LogHandler().get_logger("ChannelDat") self.model_platform = myCongif.get_data("model_platform") self.channel_id = channel_id #该通道的通道ID self.warnM = warnM #报警线程管理对象--MQ self.ffprocess = self.start_h264_encoder(myCongif.get_data("mywidth"),myCongif.get_data("myheight"))#基于frame进行编码 #视频采集相关 self.cap = None #该通道视频采集对象 还是vcf对象 self.source = None #rtsp 路径 self.frame_rate = myCongif.get_data("frame_rate") self.frame_interval = 1.0 / int(myCongif.get_data("verify_rate")) #模型采集相关 self.model = None #模型对象 -- 一通道只关联一个模型 self.work_th = None #该通道工作线程句柄 self.b_model = False #是否有运行模型线程 self.bool_run = True # 线程运行标识 self.lock = threading.RLock() # 用于保证线程安全 self.icount_max = icount_max # 帧序列号上限 self.max_len = myCongif.get_data("buffer_len") self.deque_frame = deque(maxlen=self.max_len) #视频缓冲区用于保存录像 self.last_frame = None # 保存图片数据 #self.frame_queue = queue.Queue(maxsize=1) self.frame_queue = MyDeque(15) #分析画面MQ self.counter = 0 #帧序列号--保存报警录像使用 #model独立线程相关 self.per_th = None #预处理线程句柄 self.per_status= False #预处理线程状态 self.post_th = None #后处理线程句柄 self.post_status = False #后处理线程状态 self.model_nM = None # 模型管理对象 self.model_node= None #模型对象 -- inmq,outmq self.out_mq = MyDeque(20) #分析结果存放MQ #设置JPEG压缩基本 self.encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), myCongif.get_data("encode_param")] #压缩质量(0到100) #添加一帧图片 def add_deque(self, value): if len(self.deque_frame) == self.max_len: removed_frame = self.deque_frame.popleft() del removed_frame removed_frame = None self.deque_frame.append(value) #deque 满了以后会把前面的数据移除 #拷贝一份数据 def copy_deque(self): return copy.deepcopy(self.deque_frame) #获取最后一帧图片 def get_last_frame(self): if self.b_model: # with self.lock: # frame = self.last_frame # return frame frame = self.frame_queue.mypopleft() return frame else: #如果没有运行,直接从cap获取画面 if self.cap: ret, frame = self.cap.read() # 除了第一帧,其它应该都是有画面的 if not ret: self.logger.debug(f"{self.channel_id}--web--获取原画失败,队列空") return None ret,buffer_bgr_webp = self._encode_frame(frame) return buffer_bgr_webp return None def update_last_frame(self,buffer): if buffer: #print(f"{time.time()}--{self.channel_id}--数据入frame_queue队列\r") self.frame_queue.myappend(buffer) # with self.lock: # self.last_frame = None # self.last_frame = buffer #------------h264编码相关--------------- def start_h264_encoder(self,width, height): #宽高一样,初步定全进程一个 libx264 h264_ascend process = subprocess.Popen( ['ffmpeg', '-f', 'rawvideo', '-pix_fmt', 'yuv420p', '-s', f'{width}x{height}', '-i', '-', # Take input from stdin '-an', # No audio '-vcodec', 'libx264', '-f', 'flv', '-'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) return process def close_h264_encoder(self): self.ffprocess.stdin.close() self.ffprocess.wait() def encode_frame_h264_bak(self,frame): try: process = ( ffmpeg .input('pipe:', format='rawvideo', pix_fmt='bgr24', s=f'{frame.shape[1]}x{frame.shape[0]}') .output('pipe:', format='flv',vcodec='h264_ascend') .run_async(pipe_stdin=True, pipe_stdout=True, pipe_stderr=True) ) out, err = process.communicate(input=frame.tobytes()) if process.returncode != 0: raise RuntimeError(f"FFmpeg encoding failed: {err.decode('utf-8')}") return out except Exception as e: print(f"Error during frame encoding: {e}") return None def encode_frame_h264(self, frame,timeout=1): yuv_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2YUV_I420) if self.ffprocess.poll() is not None: raise RuntimeError("FFmpeg process has exited unexpectedly.") # Write frame to stdin of the FFmpeg process try: self.ffprocess.stdin.write(yuv_frame.tobytes()) self.ffprocess.stdin.flush() # Capture the encoded output buffer_size = 1024 * 1 # Adjust this based on the size of the encoded frame encoded_frame = bytearray() start_time = time.time() while True: # Use select to handle non-blocking reads from both stdout and stderr #ready_to_read, _, _ = select.select([self.ffprocess.stdout, self.ffprocess.stderr], [], [], timeout) ready_to_read, _, _ = select.select([self.ffprocess.stdout.fileno(), self.ffprocess.stderr.fileno()], [], [], timeout) # Check if there's data in stdout (encoded frame) if self.ffprocess.stdout.fileno() in ready_to_read: chunk = self.ffprocess.stdout.read(buffer_size) if chunk: encoded_frame.extend(chunk) else: break # No more data to read from stdout # Check if there's an error in stderr if self.ffprocess.stderr.fileno() in ready_to_read: error = self.ffprocess.stderr.read(buffer_size).decode('utf-8') raise RuntimeError(f"FFmpeg error: {error}") # Timeout handling to avoid infinite blocking if time.time() - start_time > timeout: raise RuntimeError("FFmpeg encoding timed out.") if not encoded_frame: raise RuntimeError("No encoded data received from FFmpeg.") # Optional: Check for errors in stderr # stderr_output = self.process.stderr.read() # if "error" in stderr_output.lower(): # raise RuntimeError(f"FFmpeg error: {stderr_output}") return bytes(encoded_frame) except Exception as e: print(f"Error during frame encoding: {e}") return None def _frame_pre_work(self, frame): ''' 对采集到的图片数据进行预处理,需要确保是在原图上进行的修改 :param frame: :return: ''' # ----------添加时间戳------------- # 获取当前时间 current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") # 设置字体和位置 font = cv2.FONT_HERSHEY_SIMPLEX position = (10, 30) # 时间戳在左上角 font_scale = 1 color = (255, 255, 255) # 白色 thickness = 2 # 在帧上绘制时间戳 cv2.putText(frame, current_time, position, font, font_scale, color, thickness, cv2.LINE_AA) return frame def _encode_frame(self,frame,itype=0): ret = False buffer_bgr_webp = None if frame is not None: if itype == 0: #jpg self._frame_pre_work(frame) #对图片添加一些信息,目前是添加时间戳(model是入队列的时间,cap是发送前取帧的时间) ret, frame_bgr_webp = cv2.imencode('.jpg', frame, self.encode_param) if ret: buffer_bgr_webp = frame_bgr_webp.tobytes() elif itype == 1: #H264 try: buffer_bgr_webp = self.encode_frame_h264(frame) ret = True except Exception as e: print(e) else: print("错误的参数!!") return ret,buffer_bgr_webp #帧序列号自增 一个线程中处理,不用加锁 def increment_counter(self): self.counter += 1 if self.counter > self.icount_max: self.counter = 0 def get_counter(self): return self.counter def _start_cap_th(self,source,type=1): '''开始cap采集线程 type = 打开摄像头 0--USB摄像头,1-RTSP,2-海康SDK ''' ret = False if self.cap: mCap.stop_get_video(self.source) self.cap = None self.cap = mCap.start_get_video(source,type) self.source = source if self.cap: ret = True return ret def _stop_cap_th(self): '''停止cap采集线程 重要约束:停止cap线程前,必须先停止model线程 ''' if self.b_model: self.logger.error("停止采集线程前,请先停止model线程") return False else: if self.cap: mCap.stop_get_video(self.source) self.cap = None return True #一般不会没有cap def _pre_work_th(self,schedule): '''一个通道一个线程,关联的模型在一个线程检测,局部变量都是一个通道独有''' if not self.cap: self.logger.error("采集线程未正常启动,不进行工作") return while self.model_node.m_p_status.value == 0: #避免模型没启动成功,模型线程在运行 time.sleep(1) if self.model_node.m_p_status.value == 1: # 开始循环处理业务 last_frame_time = time.time() # 初始化个读帧时间 self.per_status = True self.b_model = True while self.bool_run: # 基于tag 作为运行标识。 线程里只是读,住线程更新,最多晚一轮,应该不用线程锁。需验证 # 帧率控制帧率 current_time = time.time() elapsed_time = current_time - last_frame_time if elapsed_time < self.frame_interval: time.sleep(self.frame_interval - elapsed_time) # 若小于间隔时间则休眠 last_frame_time = time.time() # *********取画面************* ret, frame = self.cap.read() # 除了第一帧,其它应该都是有画面的 if not ret: self.logger.debug(f"{self.channel_id}--model--获取cap画面失败,队列空") continue # 没读到画面继续 # 验证检测计划,是否在布防时间内 now = datetime.datetime.now() # 获取当前日期和时间 weekday = now.weekday() # 获取星期几,星期一是0,星期天是6 hour = now.hour if schedule[weekday][hour] == 1: #图片预处理 img,scale_ratio, pad_size = self.model_node.model.prework(frame) indata = ModelinData(self.channel_id,img,frame,scale_ratio, pad_size) self.model_node.pro_add_data(indata) #数据入队列 else:# 不在计划则不进行验证,直接返回图片 --存在问题是:result 漏数据 ret, frame_bgr_webp = cv2.imencode('.jpg', frame,self.encode_param) if not ret: buffer_bgr_webp = None else: buffer_bgr_webp = frame_bgr_webp.tobytes() self.update_last_frame(buffer_bgr_webp) else: self.logger.error("模型线程为启动成功,不进行工作") return self.b_model = False self.per_status = False def _post_work_th(self,duration_time,proportion,verify_rate,warn_interval,model_name,check_area,polygon,conf_threshold,iou_thres): # 初始化业务数据 result = [0 for _ in range(duration_time * verify_rate)] # 初始化时间*验证帧率数量的结果list warn_last_time = time.time() tmpcount = 0 while self.bool_run: out_data = self.out_mq.mypopleft() #(image,scale_ratio, pad_size,outputs): if not out_data: tmpcount += 1 if tmpcount == 10: print(f"{time.time()}--{self.channel_id}--model-outmq 连续10次没有数据\r") time.sleep(1) continue tmpcount = 0 #开始后处理 bwarn, warn_text = self.model_node.model.postwork(out_data.image,out_data.outputs,out_data.scale_ratio,out_data.pad_size, check_area,polygon,conf_threshold,iou_thres) # 对识别结果要部要进行处理 if bwarn: # 绘制报警文本 cv2.putText(out_data.image, warn_text, (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) result.append(1) # 要验证数组修改,是地址修改吗? else: # 没有产生报警也需要记录,统一计算占比 result.append(0) #分析画面保存 # ret, frame_bgr_webp = cv2.imencode('.jpg', out_data.image,self.encode_param) # buffer_bgr_webp = None # if ret: # buffer_bgr_webp = frame_bgr_webp.tobytes() ret, buffer_bgr_webp = self._encode_frame(out_data.image) # 分析图片放入缓冲区内存中 self.add_deque(out_data.image) # 缓冲区大小由maxlen控制 超上限后,删除最前的数据 # 分析画面一直更新最新帧,提供网页端显示 self.update_last_frame(buffer_bgr_webp) if bwarn: # 验证result -是否触发报警要求 --遍历每个模型执行的result count_one = float(sum(result)) # 1,0 把1累加的和就是1的数量 ratio_of_ones = count_one / len(result) # self.logger.debug(result) if ratio_of_ones >= proportion: # 触发报警 # 基于时间间隔判断 current_time = time.time() elapsed_time = current_time - warn_last_time if elapsed_time < warn_interval: continue warn_last_time = current_time # 处理报警 warn_data = WarnData() warn_data.model_name = model_name warn_data.warn_text = warn_text warn_data.img_buffer = self.copy_deque() # 深度复制缓冲区 warn_data.width = self.cap.width warn_data.height = self.cap.height warn_data.channel_id = self.channel_id self.warnM.add_warn_data(warn_data) # 结果记录要清空 for i in range(len(result)): result[i] = 0 def _verify(self,frame,model,model_data,schedule,result,isdraw=1): '''验证执行主函数,实现遍历通道关联的模型,调用对应模型执行验证''' #img = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) img = frame #验证检测计划,是否在布防时间内 now = datetime.datetime.now() # 获取当前日期和时间 weekday = now.weekday() # 获取星期几,星期一是0,星期天是6 hour = now.hour result.pop(0) # 保障结果数组定长 --先把最早的结果推出数组 warntext = "" if model and schedule[weekday][hour] == 1: #不在计划则不进行验证,直接返回图片 # 调用模型,进行检测,model是动态加载的,具体的判断标准由模型内执行 ---- ********* # bwarn = False # warntext = "" # time.sleep(2) bwarn, warntext = model.verify(img, model_data,isdraw) #****************重要 # 对识别结果要部要进行处理 if bwarn: # 绘制报警文本 cv2.putText(img, warntext, (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) result.append(1) #要验证数组修改,是地址修改吗? else: #没有产生报警也需要记录,统一计算占比 warntext = "" result.append(0) else: result.append(0) # 将检测结果图像转换为帧--暂时用不到AVFrame--2024-7-5 # new_frame_rgb_avframe = av.VideoFrame.from_ndarray(img, format="rgb24") # AVFrame # new_frame_rgb_avframe.pts = None # 添加此行确保有pts属性 # if isinstance(img, np.ndarray): -- 留个纪念 #处理完的图片后返回-bgr模式 #img_bgr_ndarray = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) ret,buffer_bgr_webp = self._encode_frame(img) #buffer_bgr_webp = self.encode_frame_to_flv(img) return buffer_bgr_webp,img,warntext def _dowork_thread(self,channel_id,model_data,schedule,verify_rate,warn_interval): '''一个通道一个线程,关联的模型在一个线程检测,局部变量都是一个通道独有''' if not self.cap: self.logger.error("采集线程未正常启动,退出model线程") return #加载自定义模型文件 model = self._import_model(str(channel_id), model_data[5], model_data[8], model_data[9]) # 动态加载模型处理文件py if not model: self.logger.error("自定义模型文件加载失败,退出model线程") return #初始化模型运行资源 context = None device_id = myCongif.get_data("device_id") if self.model_platform == "acl": # ACL线程中初始化内容 context = ACLModeManger.th_inti_acl(device_id) #创建context #初始化模型资源 -- 加载模型文件 ret = model.init_acl_resource() #加载和初始化离线模型文件--om文件 if not ret: print("初始化模型资源出错,退出线程!") return #初始化业务数据 result = [0 for _ in range(model_data[3] * verify_rate)] # 初始化时间*验证帧率数量的结果list proportion = model_data[4] # 判断是否报警的占比 warn_last_time = time.time() #warn_save_count = 0 #保存录像的最新帧初始化为0 #开始循环处理业务 last_frame_time = time.time() #初始化个读帧时间 self.b_model = True while self.bool_run: #基于tag 作为运行标识。 线程里只是读,住线程更新,最多晚一轮,应该不用线程锁。需验证 # 帧率控制帧率 current_time = time.time() elapsed_time = current_time - last_frame_time if elapsed_time < self.frame_interval: time.sleep(self.frame_interval - elapsed_time) #若小于间隔时间则休眠 last_frame_time = time.time() #*********取画面************* ret,frame = self.cap.read() #除了第一帧,其它应该都是有画面的 if not ret: #self.logger.debug(f"{self.channel_id}--model--获取cap画面失败,队列空") continue #没读到画面继续 #执行图片推理 buffer_bgr_webp,img_bgr_ndarray,warn_text = self._verify(frame,model,model_data,schedule,result) #分析图片放入内存中 self.add_deque(img_bgr_ndarray) # 缓冲区大小由maxlen控制 超上限后,删除最前的数据 # 分析画面一直更新最新帧,提供网页端显示 self.update_last_frame(buffer_bgr_webp) if warn_text: #验证result -是否触发报警要求 --遍历每个模型执行的result count_one = float(sum(result)) #1,0 把1累加的和就是1的数量 ratio_of_ones = count_one / len(result) #self.logger.debug(result) if ratio_of_ones >= proportion: #触发报警 # 基于时间间隔判断 current_time = time.time() elapsed_time = current_time - warn_last_time if elapsed_time < warn_interval: continue warn_last_time = current_time # 处理报警 warn_data = WarnData() warn_data.model_name = model_data[7] warn_data.warn_text = warn_text warn_data.img_buffer = self.copy_deque() # 深度复制缓冲区 warn_data.width = self.cap.width warn_data.height = self.cap.height warn_data.channel_id = channel_id self.warnM.add_warn_data(warn_data) #结果记录要清空 for i in range(len(result)): result[i] = 0 # end_time = time.time() # 结束时间 # print(f"Processing time: {end_time - start_time} seconds") # 本地显示---测试使用 # if channel_id == 2: # cv2.imshow(str(channel_id), img) # if cv2.waitKey(1) & 0xFF == ord('q'): # break #结束线程 print("开始结束工作线程") self.b_model = False #反初始化 if self.model_platform == "acl": try: model.release() #释放模型资源资源 # 删除模型对象 del model #释放context if context: # ACL线程中反初始化内容 -- 若线程异常退出,这些资源就不能正常释放了 # 再释放context ACLModeManger.th_del_acl(context) except Exception as e: print(e) #cv2.destroyAllWindows() print("线程结束!!!!") #2024-9-9 新增兼容独立model线程 根据self.model_node判断,None:1通道1线程,not None:独立线程 #2024-10-14 model调整为独立子进程执行 def _start_model_th(self,model_data,schedule,type=1): verify_rate = myCongif.get_data("verify_rate") warn_interval = myCongif.get_data("warn_interval") self.bool_run = True if self.model_nM:#要起个预处理线程,和一个后处理线程 #启动后处理线程 self.post_th = threading.Thread(target=self._post_work_th, args=(model_data[3],model_data[4],verify_rate,warn_interval,model_data[7], model_data[1],model_data[2],model_data[8],model_data[9])) self.post_th.start() # 启动模型线程,若线程已启动,则+1 self.model_nM.start_model_th(self.channel_id,self.out_mq) self.model_node = self.model_nM.getModle_Nodel(self.channel_id) if not self.model_node: self.logger.error("model_node 不应该为空!!") self.bool_run = False return #启动预处理线程 self.per_th = threading.Thread(target=self._pre_work_th,args=(schedule,)) self.per_th.start() else: self.work_th = threading.Thread(target=self._dowork_thread, args=(self.channel_id, model_data, schedule, verify_rate, warn_interval)) # 一个视频通道一个线程 self.work_th.start() def _stop_model_th(self): if self.model_nM: #独立线程,需要停止预处理线程,和后处理线程 self.bool_run = False #停止预处理线程 if self.per_th: self.per_th.join() self.per_th = None #停止model线程 -1 self.model_nM.stop_model_th(self.channel_id) #停止后处理线程 if self.post_th: self.post_th.join() self.post_th = None #清空MQ self.out_mq.myclear()#清空后处理mq中未处理的数据 else: if self.work_th: if self.b_model: self.bool_run = False self.work_th.join() #线程结束时,会把b_model置False self.logger.debug(f"{self.channel_id}停止工作线程") self.work_th = None def _import_model(self,model_name,model_path,threshold,iou_thres): ''' 根据路径,动态导入模块 :param model_name: 模块名称 --用通道ID代替 :param model_path: 模块路径 :param threshold: 置信阈值 :param iou_thres: iou阈值 :return: ''' try: module_path = model_path.replace("/", ".").rsplit(".", 1)[0] print(module_path) # 动态导入模块 module = importlib.import_module(module_path) # 从模块中获取指定的类 Model = getattr(module, "Model") # 使用 Model 类 model_instance = Model(model_path,threshold,iou_thres) return model_instance except ModuleNotFoundError as e: print(f"Module not found: {e}") return None except AttributeError as e: print(f"Class not found in module: {e}") return None except Exception as e: print(f"An unexpected error occurred: {e}") return None # if os.path.exists(model_path): # module_spec = importlib.util.spec_from_file_location(model_name, model_path) # if module_spec is None: # self.logger.error(f"{model_path} 加载错误") # return None # module = importlib.util.module_from_spec(module_spec) # start_time = time.time() # print(f"通道{self.channel_id},开始exec_module自定义模型文件") # module_spec.loader.exec_module(module) # end_time = time.time() # print(f"通道{self.channel_id},完成exec_module模型文件实例化耗时{end_time - start_time}") # # try: # md = getattr(module, "Model")(model_path,threshold,iou_thres) #实例化类 # except Exception as e: # self.logger.error(f"{model_path} 实例化错误,退出模型线程") # return None # # if not isinstance(md, ModelBase): # self.logger.error("{} not zf_model".format(md)) # return None # else: # self.logger.error("{}文件不存在".format(model_path)) # return None # self.logger.debug(f"{model_path} 加载成功!!!!") # return md def start_work(self,cap_data,model_data,schedule,type,model_NodeM=None): ''' 开始工作线程,包括视频通道采集和模型处理 :param cap_data: [source,type] :param model_data: 跟通道关联的模型数据 strsql = ( f"select t1.model_id,t1.check_area,t1.polygon ,t2.duration_time,t2.proportion,t2.model_path,t1.ID," f"t2.model_name,t1.conf_thres,t1.iou_thres " f"from channel2model t1 left join model t2 on t1.model_id = t2.ID where t1.channel_id ={channel_id};") :param schedule :param type: 0-启动所有线程,1-只启动cap采集线程,2-只启动model模型处理线程 :return: True,False ''' ret = False self.model_nM = model_NodeM if type==0: self._start_cap_th(cap_data[0],cap_data[1]) #先cap,再model self._start_model_th(model_data,schedule) ret = True elif type == 1: self._start_cap_th(cap_data[0],cap_data[1]) ret = True elif type == 2: self._start_model_th(model_data,schedule) ret = True else: self.logger.error("暂时还不支持该类型工作!") return ret def stop_work(self,type=0): ''' 清空数据,停止工作线程(若有 ,并删除deque 和 last_frame) :param type: 0-停止所有线程,1-只停止cap采集线程,2-只停止model模型处理线程 :return: True,False ''' ret = False if type == 0: self._stop_model_th() self._stop_cap_th() ret = True elif type == 1: self._stop_cap_th() ret = True elif type == 2: self.logger.debug("单独停止工作线程") self._stop_model_th() ret = True else: self.logger.error("暂时还不支持该类型工作!") return ret