yolo11/main/test_main.py
2025-04-16 16:19:53 +08:00

593 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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("程序退出")