# coding=utf-8 import os import platform import tkinter from tkinter import * from HCNetSDK import * from PlayCtrl import * from datetime import datetime import json from yolo_detector import YOLODetector import cv2 import numpy as np from PIL import Image, ImageTk import redis from PIL import ImageFont, ImageDraw, Image import base64 from threading import Thread from queue import Queue import queue import time import threading from yolo_processor import process_frame_with_yolo class DISPLAY_INFO(Structure): _fields_ = [ ("pBuf", POINTER(c_ubyte)), ("nSize", c_ulong), ("nWidth", c_long), ("nHeight", c_long), ("nStamp", c_ulong), ("nType", c_ulong), ("nReserved", c_ulong), ] # 读取配置文件 def load_nvr_config(config_path='roi_config.json'): with open(config_path, 'r') as f: config = json.load(f) return config['nvr'], config['cameraCfgs'] # 获取NVR配置和摄像头配置 nvr_config, camera_configs = load_nvr_config() # 登录的设备信息 DEV_IP = create_string_buffer(nvr_config['ip'].encode()) DEV_PORT = nvr_config['port'] DEV_USER_NAME = create_string_buffer(nvr_config['username'].encode()) DEV_PASSWORD = create_string_buffer(nvr_config['password'].encode()) YOLO_FPS = 2 # 每秒处理的帧数 WINDOWS_FLAG = True win = None # 预览窗口 funcRealDataCallBack_V30 = None # 实时预览回调函数,需要定义为全局的 PlayCtrl_Port = c_long(-1) # 播放句柄 Playctrldll = None # 播放库 FuncDecCB = None # 播放库解码回调函数,需要定义为全局的 max_channels = 9 # 最大支持9个通道 windows = [None] * max_channels # 预览窗口数组 canvases = [None] * max_channels # 画布数组 PlayCtrl_Ports = [c_long(-1)] * max_channels # 播放句柄数组 funcRealDataCallBack_V30_array = [None] * max_channels # 预览回调函数数组 funcDecCB_array = [None] * max_channels # 预览回调函数数组 frame_queues = [Queue(maxsize=2) for _ in range(max_channels)] # 线程控制参数 WORKER_THREADS = 4 # 根据CPU核心数调整(建议物理核心数×2) EXIT_SIGNAL = "EXIT" consumers = [] # 消费者线程池 # 初始化队列 frame_queue = queue.Queue(maxsize=200) # 原始数据队列 result_queue = queue.Queue() # GUI更新队列 processing_threads = [None] * max_channels enable_yolo = True last_process_times = [datetime.now()] * max_channels # 每个通道上次处理的时间 redis_client = redis.Redis(host='192.168.110.229', port=36379, db=0, password="yunda123") # 获取当前系统环境 def GetPlatform(): sysstr = platform.system() print('' + sysstr) if sysstr != "Windows": global WINDOWS_FLAG WINDOWS_FLAG = False # 设置SDK初始化依赖库路径 def SetSDKInitCfg(): # 设置HCNetSDKCom组件库和SSL库加载路径 # print(os.getcwd()) if WINDOWS_FLAG: strPath = os.getcwd().encode('gbk') sdk_ComPath = NET_DVR_LOCAL_SDK_PATH() sdk_ComPath.sPath = strPath Objdll.NET_DVR_SetSDKInitCfg(2, byref(sdk_ComPath)) Objdll.NET_DVR_SetSDKInitCfg(3, create_string_buffer(strPath + b'\\libcrypto-1_1-x64.dll')) Objdll.NET_DVR_SetSDKInitCfg(4, create_string_buffer(strPath + b'\\libssl-1_1-x64.dll')) else: strPath = os.getcwd().encode('utf-8') sdk_ComPath = NET_DVR_LOCAL_SDK_PATH() sdk_ComPath.sPath = strPath Objdll.NET_DVR_SetSDKInitCfg(2, byref(sdk_ComPath)) Objdll.NET_DVR_SetSDKInitCfg(3, create_string_buffer(strPath + b'/libcrypto.so.1.1')) Objdll.NET_DVR_SetSDKInitCfg(4, create_string_buffer(strPath + b'/libssl.so.1.1')) def LoginDev(Objdll): # 登录注册设备 device_info = NET_DVR_DEVICEINFO_V30() lUserId = Objdll.NET_DVR_Login_V30(DEV_IP, DEV_PORT, DEV_USER_NAME, DEV_PASSWORD, byref(device_info)) return (lUserId, device_info) def start_processing_thread(channel_index): def run(): while True: frame_data = frame_queues[channel_index].get() print("out") if frame_data is None: # 终止信号 break # 在此处执行YOLO处理和Redis发布 # process_frame_with_yolo(frame_data, channel_index) process_frame_with_yolo(frame_data, channel_index, camera_configs, yolo_detector, redis_client) thread = Thread(target=run, daemon=True) thread.start() return thread def on_closing(): for i in range(num_channels): frame_queues[i].put(None) # 发送终止信号 processing_threads[i].join() root.destroy() def DecCBFun(nPort, pBuf, nSize, pFrameInfo, nUser, nReserved2): # 解码回调函数 channel_index = nUser - 1 # print(f"回调函数被调用 - 通道: {channel_index + 1}, 数据类型: {dwDataType}, 数据大小: {dwBufSize}") if channel_index < 0 or channel_index >= max_channels: print(f"无效的通道索引: {channel_index}") return port = PlayCtrl_Ports[channel_index] canvas = canvases[channel_index] if pFrameInfo.contents.nType == 3: # 获取当前时间 now = datetime.now() # 格式化为 HH:mm:ss:fff 格式 formatted_time = now.strftime("%H:%M:%S:%f")[:-3] sFileName = ('../../pic/test_stamp[%s].jpg' % pFrameInfo.contents.nStamp) nWidth = pFrameInfo.contents.nWidth nHeight = pFrameInfo.contents.nHeight nType = pFrameInfo.contents.nType dwFrameNum = pFrameInfo.contents.dwFrameNum nStamp = pFrameInfo.contents.nStamp #print(nWidth, nHeight, nType, dwFrameNum, nStamp, sFileName) if yolo_detector is not None: try: if nWidth > 0 and nHeight > 0: try: # 提取YUV数据并转换为RGB yuv_data = np.frombuffer(pBuf[:nSize], dtype=np.uint8) height = pFrameInfo.contents.nHeight width = pFrameInfo.contents.nWidth # YUV420转RGB yuv_frame = yuv_data.reshape((height * 3 // 2, width)) # rgb_frame = cv2.cvtColor(yuv_frame, cv2.COLOR_YUV2RGB_I420) try: # 仅传递原始数据到处理队列 if not frame_queue.full(): frame_queue.put((channel_index, yuv_frame), block=False) except Exception as e: print(f"回调异常: {str(e)}") # cv2.imshow("frame", frame) # cv2.waitKey(0) # cv2.destroyAllWindows() except Exception as e: print(f"YUV转RGB失败: {str(e)}") except Exception as e: print(f"YOLO处理异常: {str(e)}") import traceback traceback.print_exc() def save_detections(detections, frame, output_dir="."): os.makedirs(output_dir, exist_ok=True) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") output_image = os.path.join(output_dir, f"detection_{timestamp}.jpg") output_json = os.path.join(output_dir, f"detection_{timestamp}.json") # 保存可视化图片 cv2.imwrite(output_image, frame) # 保存结构化数据 with open(output_json, 'w') as f: json.dump(detections, f, indent=2) # 在全局变量部分修改 # 删除原有的win1, win2, cv1, cv2等变量 # 替换为通道数组 # 修改RealDataCallBack_V30函数,支持多通道 def RealDataCallBack_V30(lPlayHandle, dwDataType, pBuffer, dwBufSize, pUser): try: channel_index = pUser - 1 # print(f"回调函数被调用 - 通道: {channel_index + 1}, 数据类型: {dwDataType}, 数据大小: {dwBufSize}") if channel_index < 0 or channel_index >= max_channels: print(f"无效的通道索引: {channel_index}") return port = PlayCtrl_Ports[channel_index] canvas = canvases[channel_index] if dwDataType == NET_DVR_SYSHEAD: print(f"通道 {channel_index + 1} 收到系统头数据") Playctrldll.PlayM4_SetStreamOpenMode(port, 0) if Playctrldll.PlayM4_OpenStream(port, pBuffer, dwBufSize, 1024 * 1024): canvas_id = canvas.winfo_id() print(f"通道 {pUser} 尝试使用画布ID: {canvas_id}") # 确保画布仍然有效 if not canvas.winfo_exists(): print(f"通道 {channel_index + 1} 的画布已经不存在") return # 设置解码回调,可以返回解码后YUV视频数据 funcDecCB_array[channel_index] = DECCBFUNWIN(DecCBFun) Playctrldll.PlayM4_SetDecCallBackExMend(port, funcDecCB_array[channel_index], None, 0, pUser) Playctrldll.PlayM4_Stop(port) if Playctrldll.PlayM4_Play(port, canvas_id): print(f'通道 {pUser} 播放库播放成功,使用画布ID: {canvas_id}') Playctrldll.PlayM4_SetDisplayBuf(port, 15) Playctrldll.PlayM4_SetOverlayMode(port, 0, 0) # 禁用自动显示 Playctrldll.PlayM4_SetDisplayCallBack(port, None) else: err = Playctrldll.PlayM4_GetLastError(port) print(f'通道 {pUser} 播放库播放失败,错误码: {err}') else: err = Playctrldll.PlayM4_GetLastError(port) print(f'通道 {pUser} 播放库打开流失败,错误码: {err}') elif dwDataType == NET_DVR_STREAMDATA: try: # 检查端口是否有效 if port.value < 0: print(f"通道 {channel_index + 1} 的播放端口无效") return # 直接输入数据,但不显示 if not Playctrldll.PlayM4_InputData(port, pBuffer, dwBufSize): err = Playctrldll.PlayM4_GetLastError(port) # print(f"通道 {channel_index + 1} 输入数据失败,错误码: {err}") # return except Exception as e: print(f"通道 {channel_index + 1} 数据处理异常: {str(e)}") import traceback traceback.print_exc() except Exception as e: print(f"回调函数发生未知异常: {str(e)}") import traceback traceback.print_exc() # 添加一个计算布局的函数 def calculate_layout(num_channels): """根据通道数计算最佳布局""" if num_channels <= 1: return 1, 1 elif num_channels <= 4: return 2, 2 elif num_channels <= 6: return 2, 3 else: # 7-9通道 return 3, 3 def OpenPreview(Objdll, lUserId, callbackFun, channel_index): ''' 打开预览 ''' preview_info = NET_DVR_PREVIEWINFO() preview_info.hPlayWnd = 0 preview_info.lChannel = camera_configs[channel_index]['channel'] # 根据索引获取对应通道号 preview_info.dwStreamType = 0 preview_info.dwLinkMode = 0 preview_info.bBlocked = 1 lRealPlayHandle = Objdll.NET_DVR_RealPlay_V40(lUserId, byref(preview_info), callbackFun, channel_index + 1) return lRealPlayHandle def InputData(fileMp4, Playctrldll): while True: pFileData = fileMp4.read(4096) if pFileData is None: break if not Playctrldll.PlayM4_InputData(PlayCtrl_Port, pFileData, len(pFileData)): break # ------------------ 线程管理函数 ------------------ def start_consumers(): """启动消费者线程池""" global consumers # 清理残留线程 consumers = [t for t in consumers if t.is_alive()] # 创建新线程 for _ in range(WORKER_THREADS): t = threading.Thread( target=consumer_worker, daemon=True # 主退出时自动结束 ) t.start() consumers.append(t) print(f"已启动 {len(consumers)} 个消费者线程") def stop_consumers(): """安全停止所有消费者线程""" for _ in range(WORKER_THREADS): frame_queue.put((EXIT_SIGNAL, None)) # 发送退出信号 # 等待线程结束 for t in consumers: t.join(timeout=5) if t.is_alive(): print(f"警告: 线程 {t.ident} 未正常退出") print("所有消费者线程已停止") # 修改消费者工作线程 def consumer_worker(): while True: try: channel, yuv_array = frame_queue.get(timeout=5) if channel is EXIT_SIGNAL: break # YUV转RGB(实际分辨率从配置读取) config = camera_configs[channel] rgb_frame = cv2.cvtColor(yuv_array, cv2.COLOR_YUV2RGB_I420) # # 执行推理 # process_frame_with_yolo(rgb_frame, channel) process_frame_with_yolo(rgb_frame, channel, camera_configs, yolo_detector, redis_client) # 准备GUI更新数据 resized_frame = cv2.resize(rgb_frame, (320, 240)) img = cv2.cvtColor(resized_frame, cv2.COLOR_RGB2BGR) except queue.Empty: continue # 增强型GUI更新函数 def update_gui(): while not result_queue.empty(): try: channel, img_tk, status = result_queue.get_nowait() # 安全更新GUI组件 canvases[channel].configure(image=img_tk) canvases[channel].image = img_tk # 保持引用 canvases[channel].itemconfigure("status", text=status) except Exception as e: print(f"GUI更新异常: {str(e)}") root.after(50, update_gui) # 在界面初始化时预加载画布ID def create_canvas_grid(): global canvas_ids canvas_ids = {} for i in range(num_channels): canvases[i].update() canvas_ids[i] = canvases[i].winfo_id() # 预获取窗口ID print(f"通道 {i} 画布ID: {canvas_ids[i]}") # 在设备初始化时设置播放参数 def set_play_params(port, channel): # 使用预存储的画布ID Playctrldll.PlayM4_SetDisplayWindow( port, canvas_ids[channel], # 使用预获取的ID 0, 0, single_width, single_height ) # 修改SDK初始化部分 def Initcustom(): # 预加载所有画布ID create_canvas_grid() # 启动消费者线程 start_consumers() # 启动GUI更新循环 root.after(0, update_gui) # 启动主循环 root.mainloop() # 程序退出时清理 stop_consumers() if __name__ == '__main__': try: print("程序启动...") # YOLO初始化 print("正在初始化YOLO检测器...") try: yolo_detector = YOLODetector() if enable_yolo else None print("YOLO检测器初始化成功") except Exception as e: print(f"YOLO检测器初始化失败: {str(e)}") raise # 创建窗口 print("正在创建主窗口...") root = tkinter.Tk() root.title("多通道预览") # 加载配置 print("正在加载配置文件...") num_channels = min(len(camera_configs), max_channels) print(f"检测到 {len(camera_configs)} 个摄像头配置,将显示 {num_channels} 个通道") # 计算布局 rows, cols = calculate_layout(num_channels) # 设置单个视频窗口的大小 single_width, single_height = 320, 240 # 设置主窗口大小 window_width = cols * single_width window_height = rows * single_height root.geometry(f"{window_width}x{window_height}+100+100") # 创建帧来容纳所有画布 frame = tkinter.Frame(root, width=window_width, height=window_height) frame.pack() # 创建画布网格 for i in range(num_channels): row = i // cols col = i % cols # 创建每个通道的画布,并确保它们有唯一的标识 # 使用Frame作为容器,可能有助于解决显示问题 frame_container = tkinter.Frame(frame, width=single_width, height=single_height) frame_container.grid(row=row, column=col, padx=2, pady=2) frame_container.grid_propagate(False) # 防止frame自动调整大小 canvases[i] = tkinter.Canvas(frame_container, bg='black', width=single_width, height=single_height, name=f"canvas_{i}") canvases[i].pack(fill=tkinter.BOTH, expand=True) # 添加通道标签 canvases[i].create_text(10, 10, text=f"通道 {i + 1}", fill="white", anchor="nw") # 确保画布已经更新并有有效的窗口ID canvases[i].update() # 获取系统平台 print("\n系统环境检测:") GetPlatform() print(f"当前系统平台标志: {'Windows' if WINDOWS_FLAG else 'Linux'}") # 加载库 print("\n正在加载SDK库...") try: if WINDOWS_FLAG: print("当前工作目录:", os.getcwd()) print("尝试切换到SDK目录: ./lib/win") os.chdir(r'./lib/win') print("加载 HCNetSDK.dll...") Objdll = ctypes.CDLL(r'./HCNetSDK.dll') print("加载 PlayCtrl.dll...") Playctrldll = ctypes.CDLL(r'./PlayCtrl.dll') else: # Linux加载代码保持不变... pass print("SDK库加载成功") except Exception as e: print(f"SDK库加载失败: {str(e)}") raise print("\n正在初始化SDK...") SetSDKInitCfg() # 初始化DLL if Objdll.NET_DVR_Init() == 0: print(f"SDK初始化失败,错误码: {Objdll.NET_DVR_GetLastError()}") raise Exception("SDK初始化失败") print("SDK初始化成功") # 登录设备 print(f"\n正在登录设备 {DEV_IP.value.decode()}:{DEV_PORT}...") (lUserId, device_info) = LoginDev(Objdll) if lUserId < 0: err = Objdll.NET_DVR_GetLastError() print(f'设备登录失败,错误码: {err}') Objdll.NET_DVR_Cleanup() raise Exception("设备登录失败") print(f"设备登录成功,用户ID: {lUserId}") # 预览通道初始化 print("\n开始初始化预览通道...") lRealPlayHandles = [] for i in range(num_channels): PlayCtrl_Ports[i] = c_long(-1) if not Playctrldll.PlayM4_GetPort(byref(PlayCtrl_Ports[i])): error_code = Playctrldll.PlayM4_GetLastError(PlayCtrl_Ports[i]) print(f'通道 {i + 1} 获取播放库句柄失败,错误码: {error_code}') PlayCtrl_Ports[i] = c_long(-1) # 明确标记为无效 continue print(f'通道 {i + 1} 获取播放库端口成功: {PlayCtrl_Ports[i].value}') # 创建回调函数 print(f'正在为通道 {i + 1} 创建回调函数...') funcRealDataCallBack_V30_array[i] = REALDATACALLBACK(RealDataCallBack_V30) # 开启预览 print(f'正在开启通道 {i + 1} 的预览...') lRealPlayHandle = OpenPreview(Objdll, lUserId, funcRealDataCallBack_V30_array[i], i) lRealPlayHandles.append(lRealPlayHandle) if lRealPlayHandle < 0: print(f'通道 {i + 1} 预览失败,错误码: {Objdll.NET_DVR_GetLastError()}') else: print(f'通道 {i + 1} 预览成功,句柄: {lRealPlayHandle}') Initcustom() root.mainloop() except Exception as e: print(f"\n程序发生异常: {str(e)}") print("错误详细信息:") import traceback traceback.print_exc() finally: print("\n开始清理资源...") # 关闭预览 if 'lRealPlayHandles' in locals(): for handle in lRealPlayHandles: if handle >= 0: print(f"正在关闭预览句柄: {handle}") Objdll.NET_DVR_StopRealPlay(handle) # 释放播放库资源 if 'PlayCtrl_Ports' in locals(): for i in range(num_channels): if PlayCtrl_Ports[i].value > -1: print(f"正在释放通道 {i + 1} 的播放库资源") Playctrldll.PlayM4_Stop(PlayCtrl_Ports[i]) Playctrldll.PlayM4_CloseStream(PlayCtrl_Ports[i]) Playctrldll.PlayM4_FreePort(PlayCtrl_Ports[i]) # 登出设备 if 'lUserId' in locals() and lUserId >= 0: print("正在登出设备...") Objdll.NET_DVR_Logout(lUserId) # 清理SDK资源 if 'Objdll' in locals(): print("正在清理SDK资源...") Objdll.NET_DVR_Cleanup() print("程序退出")