第一版主程序.py
· 33 KiB · Python
Sin formato
import cv2
import threading
import numpy as np
import time
import os
import serial
if 'DISPLAY' not in os.environ:
os.environ['DISPLAY'] = ':0'
print(f"Set DISPLAY to: {os.environ['DISPLAY']}")
import pyautogui
# ---------- 1. 固定参数区 ----------
MODEL_PATH = './yolov5s-640-640.rknn' # 模型路径
TARGET = 'rk3588' # 目标设备
IMG_SHOW = True # 是否弹窗显示结果
IMG_SAVE = False # 是否保存结果图片
COCO_MAP_TEST = False # 是否跑COCO mAP测试
FULLSCREEN = True # 是否全屏显示
SOURCES_FILE = './sources.json' # 视频源配置文件
ANCHORS_FILE = './model/anchors_yolov5.txt' # anchor文件
# 优化参数
DETECTION_INTERVAL = 5 # 检测间隔(每5帧检测一次)
DISPLAY_REFRESH_RATE = 30 # 显示刷新率(Hz)
# 屏幕尺寸
SCREEN_WIDTH = 1024
SCREEN_HEIGHT = 600
# 显示区域尺寸
VIDEO_WIDTH = int(SCREEN_WIDTH * 0.75) # 左侧四分之三显示视频
RADIO_WIDTH = SCREEN_WIDTH - VIDEO_WIDTH # 右侧四分之一显示雷达
# ---------- 2. 雷达触发参数 ----------
ALARM_DISTANCE = 1.5 # 报警距离(米)
SWITCH_INTERVAL = 3.0 # 切换间隔(秒)
# ---------- 3. 其他全局常量 ----------
OBJ_THRESH = 0.6
NMS_THRESH = 0.6
IMG_SIZE = (640, 640) # (width, height)
CLASSES = ("person", "bicycle", "car", "motorbike ", "aeroplane ", "bus ", "train", "truck ", "boat", "traffic light")
# --------------------------
# RTSP流配置部分 - 简化版本
# --------------------------
RTSP_URLS = [
"rtsp://admin:Admin888@192.168.112.200:554/streaming/channels/102",
"rtsp://admin:Admin888@192.168.112.201:554/streaming/channels/102",
"rtsp://admin:Admin888@192.168.112.202:554/streaming/channels/102",
"rtsp://admin:Admin888@192.168.112.203:554/streaming/channels/102"
]
# 全局变量
frames = [None] * 4
locks = [threading.Lock() for _ in range(4)]
stop_threads = False
combined_frame = None
combined_lock = threading.Lock()
# 显示模式控制
display_mode = 0 # 0:四分屏全屏, 1-4:单画面分屏
last_switch_time = time.time()
current_alarm_index = 0
# 雷达数据相关
radar_distances = [999.0] * 4 # 存储4路雷达距离(米)
radar_alarm_status = [False] * 4 # 雷达报警状态
radar_lock = threading.Lock()
# 雷达图片相关
radio_images = {} # 存储4个雷达图片
current_radio_image = None
# 简化的流状态管理
stream_status = [False] * 4 # 每路流的连接状态
stream_last_frame_time = [0] * 4 # 每路流最后收到帧的时间
# ---------- 简化的RTSP流处理 ----------
def read_rtsp_stream(index, rtsp_url):
"""简化的RTSP流读取函数"""
global stop_threads
cap = None
reconnect_count = 0
max_reconnect = 20
print(f"[Stream {index}] 启动RTSP流: {rtsp_url}")
while not stop_threads and reconnect_count < max_reconnect:
try:
# 释放之前的连接
if cap is not None:
cap.release()
cap = None
# 创建新的连接
print(f"[Stream {index}] 尝试连接...")
cap = cv2.VideoCapture(rtsp_url)
# 设置连接参数
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
cap.set(cv2.CAP_PROP_FPS, 15)
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*'H264'))
cap.set(cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 5000)
cap.set(cv2.CAP_PROP_READ_TIMEOUT_MSEC, 3000)
if not cap.isOpened():
print(f"[Stream {index}] 连接失败")
reconnect_count += 1
time.sleep(3)
continue
print(f"[Stream {index}] ✓ 连接成功")
stream_status[index] = True
reconnect_count = 0
# 持续读取帧
while not stop_threads and cap.isOpened():
ret, frame = cap.read()
if ret:
with locks[index]:
frames[index] = frame.copy()
stream_last_frame_time[index] = time.time()
stream_status[index] = True
else:
print(f"[Stream {index}] ✗ 读取帧失败")
stream_status[index] = False
break
# 控制读取频率
time.sleep(0.03)
except Exception as e:
print(f"[Stream {index}] 异常: {e}")
stream_status[index] = False
# 连接断开,准备重连
if cap is not None:
cap.release()
cap = None
reconnect_count += 1
print(f"[Stream {index}] 等待重连 ({reconnect_count}/{max_reconnect})...")
time.sleep(3)
print(f"[Stream {index}] 线程退出")
# ---------- 简化的画面合成函数 ----------
def combine_frames():
global combined_frame, stop_threads
while not stop_threads:
frame_list = []
current_time = time.time()
for i in range(4):
with locks[i]:
# 检查流是否超时(5秒无数据认为超时)
if current_time - stream_last_frame_time[i] > 5.0:
stream_status[i] = False
frame_list.append(None)
elif frames[i] is not None:
frame_list.append(frames[i])
else:
frame_list.append(None)
# 检查是否有有效帧
valid_frames = [frame for frame in frame_list if frame is not None]
if len(valid_frames) == 0:
time.sleep(0.1)
continue
# 获取参考尺寸(使用第一个有效帧)
first_valid_frame = next(frame for frame in frame_list if frame is not None)
h, w, _ = first_valid_frame.shape
# 创建合成画面
combined = np.zeros((h * 2, w * 2, 3), dtype=np.uint8)
positions = [
(0, 0), # 左上 - Stream 0
(0, w), # 右上 - Stream 1
(h, 0), # 左下 - Stream 2
(h, w) # 右下 - Stream 3
]
for i, (y, x) in enumerate(positions):
if frame_list[i] is not None and stream_status[i]:
resized_frame = cv2.resize(frame_list[i], (w, h))
combined[y:y + h, x:x + w] = resized_frame
# 添加状态指示器
status_color = (0, 255, 0) # 绿色 - 在线
status_text = f"Cam{i + 1} ✓"
else:
# 显示无信号画面
no_signal = create_single_no_signal(i + 1, w, h)
combined[y:y + h, x:x + w] = no_signal
status_color = (0, 0, 255) # 红色 - 离线
status_text = f"Cam{i + 1} ✗"
# 在画面角落添加状态指示
cv2.putText(combined, status_text, (x + 10, y + 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, status_color, 2)
with combined_lock:
combined_frame = combined.copy()
time.sleep(0.03)
# ---------- 雷达串口读取类 ----------
class RadarUARTReader:
def __init__(self, port: str = '/dev/ttyS0', baudrate: int = 115200):
self.port = port
self.baudrate = baudrate
self.ser = None
self.running = False
self.receive_thread = None
def start(self) -> bool:
"""启动雷达UART读取"""
try:
self.ser = serial.Serial(
port=self.port,
baudrate=self.baudrate,
bytesize=8,
parity='N',
stopbits=1,
timeout=0.1
)
self.running = True
self.receive_thread = threading.Thread(target=self._receive_worker)
self.receive_thread.daemon = True
self.receive_thread.start()
print(f"雷达数据读取器已启动: {self.port} @ {self.baudrate}bps")
return True
except Exception as e:
print(f"雷达启动失败: {e}")
return False
def _parse_frame(self, frame_data: bytearray):
"""解析雷达数据帧"""
if len(frame_data) != 16:
return None
# 验证帧头帧尾
if frame_data[0] != 0xA5 or frame_data[1] != 0x5A or frame_data[14] != 0x5A or frame_data[15] != 0xA5:
return None
radar_data_list = []
for i in range(4):
start_idx = 2 + i * 3
radar_id = frame_data[start_idx]
distance_high = frame_data[start_idx + 1]
distance_low = frame_data[start_idx + 2]
distance_mm = (distance_high << 8) | distance_low
distance_m = distance_mm / 1000.0
radar_data = {
'radar_id': radar_id,
'distance_mm': distance_mm,
'distance_m': distance_m
}
radar_data_list.append(radar_data)
return radar_data_list
def _receive_worker(self):
"""接收数据工作线程"""
state = 0 # 0:等待头, 1:接收数据
frame_buffer = bytearray()
while self.running and self.ser and self.ser.is_open:
try:
bytes_to_read = min(self.ser.in_waiting, 1024)
if bytes_to_read > 0:
data = self.ser.read(bytes_to_read)
for byte in data:
if state == 0:
if byte == 0xA5:
frame_buffer = bytearray([byte])
state = 1
elif state == 1:
frame_buffer.append(byte)
if len(frame_buffer) == 16:
# 完整帧接收完成
radar_data = self._parse_frame(frame_buffer)
if radar_data:
self._update_radar_distances(radar_data)
state = 0
frame_buffer = bytearray()
elif len(frame_buffer) > 16:
state = 0
frame_buffer = bytearray()
except Exception as e:
print(f"雷达接收错误: {e}")
time.sleep(0.01)
def _update_radar_distances(self, radar_data_list):
"""更新雷达距离数据"""
global radar_distances, radar_alarm_status
with radar_lock:
for data in radar_data_list:
radar_id = data['radar_id']
if 1 <= radar_id <= 4:
distance = data['distance_m']
radar_distances[radar_id - 1] = distance
radar_alarm_status[radar_id - 1] = (distance <= ALARM_DISTANCE and distance > 0)
# 打印报警信息
if radar_alarm_status[radar_id - 1]:
print(f"🚨 雷达{radar_id}报警! 距离: {distance:.2f}m")
def stop(self):
"""停止读取"""
self.running = False
if self.ser and self.ser.is_open:
self.ser.close()
print("雷达数据读取器已停止")
# ---------- 核心函数 ----------
def filter_boxes(boxes, box_confidences, box_class_probs):
box_confidences = box_confidences.reshape(-1)
class_max_score = np.max(box_class_probs, axis=-1)
classes = np.argmax(box_class_probs, axis=-1)
person_mask = (classes == 0)
_class_pos = np.where((class_max_score * box_confidences >= OBJ_THRESH) & person_mask)
scores = (class_max_score * box_confidences)[_class_pos]
boxes = boxes[_class_pos]
classes = classes[_class_pos]
return boxes, classes, scores
def nms_boxes(boxes, scores):
x = boxes[:, 0]
y = boxes[:, 1]
w = boxes[:, 2] - boxes[:, 0]
h = boxes[:, 3] - boxes[:, 1]
areas = w * h
order = scores.argsort()[::-1]
keep = []
while order.size > 0:
i = order[0]
keep.append(i)
xx1 = np.maximum(x[i], x[order[1:]])
yy1 = np.maximum(y[i], y[order[1:]])
xx2 = np.minimum(x[i] + w[i], x[order[1:]] + w[order[1:]])
yy2 = np.minimum(y[i] + h[i], y[order[1:]] + h[order[1:]])
w1 = np.maximum(0.0, xx2 - xx1 + 0.00001)
h1 = np.maximum(0.0, yy2 - yy1 + 0.00001)
inter = w1 * h1
ovr = inter / (areas[i] + areas[order[1:]] - inter)
inds = np.where(ovr <= NMS_THRESH)[0]
order = order[inds + 1]
keep = np.array(keep)
return keep
def box_process(position, anchors):
grid_h, grid_w = position.shape[2:4]
col, row = np.meshgrid(np.arange(0, grid_w), np.arange(0, grid_h))
col = col.reshape(1, 1, grid_h, grid_w)
row = row.reshape(1, 1, grid_h, grid_w)
grid = np.concatenate((col, row), axis=1)
stride = np.array([IMG_SIZE[1] // grid_h, IMG_SIZE[0] // grid_w]).reshape(1, 2, 1, 1)
col = col.repeat(len(anchors), axis=0)
row = row.repeat(len(anchors), axis=0)
anchors = np.array(anchors)
anchors = anchors.reshape(*anchors.shape, 1, 1)
box_xy = position[:, :2, :, :] * 2 - 0.5
box_wh = pow(position[:, 2:4, :, :] * 2, 2) * anchors
box_xy += grid
box_xy *= stride
box = np.concatenate((box_xy, box_wh), axis=1)
xyxy = np.copy(box)
xyxy[:, 0, :, :] = box[:, 0, :, :] - box[:, 2, :, :] / 2
xyxy[:, 1, :, :] = box[:, 1, :, :] - box[:, 3, :, :] / 2
xyxy[:, 2, :, :] = box[:, 0, :, :] + box[:, 2, :, :] / 2
xyxy[:, 3, :, :] = box[:, 1, :, :] + box[:, 3, :, :] / 2
return xyxy
def post_process(input_data, anchors):
boxes, scores, classes_conf = [], [], []
input_data = [_in.reshape([len(anchors[0]), -1] + list(_in.shape[-2:])) for _in in input_data]
for i in range(len(input_data)):
boxes.append(box_process(input_data[i][:, :4, :, :], anchors[i]))
scores.append(input_data[i][:, 4:5, :, :])
classes_conf.append(input_data[i][:, 5:, :, :])
def sp_flatten(_in):
ch = _in.shape[1]
_in = _in.transpose(0, 2, 3, 1)
return _in.reshape(-1, ch)
boxes = [sp_flatten(_v) for _v in boxes]
classes_conf = [sp_flatten(_v) for _v in classes_conf]
scores = [sp_flatten(_v) for _v in scores]
boxes = np.concatenate(boxes)
classes_conf = np.concatenate(classes_conf)
scores = np.concatenate(scores)
boxes, classes, scores = filter_boxes(boxes, scores, classes_conf)
nboxes, nclasses, nscores = [], [], []
for c in set(classes):
inds = np.where(classes == c)
b = boxes[inds]
c = classes[inds]
s = scores[inds]
keep = nms_boxes(b, s)
if len(keep) != 0:
nboxes.append(b[keep])
nclasses.append(c[keep])
nscores.append(s[keep])
if not nclasses and not nscores:
return None, None, None
boxes = np.concatenate(nboxes)
classes = np.concatenate(nclasses)
scores = np.concatenate(nscores)
return boxes, classes, scores
def draw(image, boxes, scores, classes):
"""正确的人体检测框绘制函数"""
if boxes is None or len(boxes) == 0:
return image
box_color = (0, 0, 255)
box_thickness = 3
text_color = (0, 0, 255)
text_thickness = 2
for box, score, cl in zip(boxes, scores, classes):
x1, y1, x2, y2 = [int(_b) for _b in box]
x1 = max(0, min(x1, image.shape[1] - 1))
y1 = max(0, min(y1, image.shape[0] - 1))
x2 = max(0, min(x2, image.shape[1] - 1))
y2 = max(0, min(y2, image.shape[0] - 1))
cv2.rectangle(image, (x1, y1), (x2, y2), box_color, box_thickness)
text = f'{CLASSES[cl]} {score:.2f}'
cv2.putText(image, text, (x1, y1 - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, text_color, text_thickness)
return image
def setup_model(model_path, device_id=None):
"""为每个NPU核心创建独立模型实例"""
if model_path.endswith('.pt') or model_path.endswith('.torchscript'):
platform = 'pytorch'
from py_utils.pytorch_executor import Torch_model_container
model = Torch_model_container(model_path)
elif model_path.endswith('.rknn'):
platform = 'rknn'
from py_utils.rknn_executor import RKNN_model_container
model = RKNN_model_container(model_path, TARGET, device_id)
elif model_path.endswith('onnx'):
platform = 'onnx'
from py_utils.onnx_executor import ONNX_model_container
model = ONNX_model_container(model_path)
else:
raise RuntimeError(f"{model_path} is not rknn/pytorch/onnx model")
print(f'Model {model_path} initialized for device {device_id}')
return model, platform
# ---------- COCO_test_helper类 ----------
class COCO_test_helper:
def __init__(self, enable_letter_box=True):
self.enable_letter_box = enable_letter_box
self.image_ids = []
self.category_ids = []
self.bboxes = []
self.scores = []
self.pad_top = 0
self.pad_left = 0
self.scale_ratio = 1.0
self.original_shape = (0, 0)
self.new_shape = (0, 0)
def letter_box(self, im, new_shape, pad_color=(0, 0, 0)):
shape = im.shape[:2]
if isinstance(new_shape, int):
new_shape = (new_shape, new_shape)
r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])
new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r))
dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1]
dw /= 2
dh /= 2
if shape[::-1] != new_unpad:
im = cv2.resize(im, new_unpad, interpolation=cv2.INTER_LINEAR)
top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))
left, right = int(round(dw - 0.1)), int(round(dw + 0.1))
self.pad_top = top
self.pad_left = left
self.scale_ratio = r
self.original_shape = shape
self.new_shape = new_shape
im = cv2.copyMakeBorder(im, top, bottom, left, right, cv2.BORDER_CONSTANT, value=pad_color)
return im
def get_real_box(self, boxes):
if boxes is None or len(boxes) == 0:
return boxes
real_boxes = []
for box in boxes:
x1, y1, x2, y2 = box
x1 = (x1 - self.pad_left) / self.scale_ratio
y1 = (y1 - self.pad_top) / self.scale_ratio
x2 = (x2 - self.pad_left) / self.scale_ratio
y2 = (y2 - self.pad_top) / self.scale_ratio
x1 = max(0, min(x1, self.original_shape[1]))
y1 = max(0, min(y1, self.original_shape[0]))
x2 = max(0, min(x2, self.original_shape[1]))
y2 = max(0, min(y2, self.original_shape[0]))
real_boxes.append([x1, y1, x2, y2])
return np.array(real_boxes)
# ---------- 加载雷达图片 ----------
def load_radio_images():
"""加载1-4号雷达图片"""
global radio_images
for i in range(1, 5):
try:
img_path = f'{i}.png'
radio_img = cv2.imread(img_path)
if radio_img is None:
print(f"Warning: Cannot load {img_path}, creating default radar image")
radio_img = create_default_radio(i)
else:
print(f"✓ Loaded radar image: {img_path}")
radio_images[i] = cv2.resize(radio_img, (RADIO_WIDTH, SCREEN_HEIGHT))
except Exception as e:
print(f"Failed to load radar image {i}.png: {e}")
radio_images[i] = create_default_radio(i)
return len(radio_images) > 0
def create_default_radio(radar_id):
"""创建默认雷达图片"""
radio_img = np.zeros((SCREEN_HEIGHT, RADIO_WIDTH, 3), dtype=np.uint8)
radio_img[:] = (20, 20, 20)
center_x = RADIO_WIDTH // 2
center_y = SCREEN_HEIGHT // 2
radius = min(RADIO_WIDTH, SCREEN_HEIGHT) // 3
# 绘制雷达圆圈
for r in range(radius, 0, -radius // 4):
color = (0, 255, 0)
cv2.circle(radio_img, (center_x, center_y), r, color, 2)
# 绘制雷达线
for angle in range(0, 360, 30):
rad = np.deg2rad(angle)
end_x = int(center_x + radius * np.cos(rad))
end_y = int(center_y + radius * np.sin(rad))
cv2.line(radio_img, (center_x, center_y), (end_x, end_y), (0, 255, 0), 1)
# 显示雷达编号
title = f"Radar {radar_id}"
title_size = cv2.getTextSize(title, cv2.FONT_HERSHEY_SIMPLEX, 0.8, 2)[0]
title_x = (RADIO_WIDTH - title_size[0]) // 2
cv2.putText(radio_img, title, (title_x, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 255, 255), 2)
return radio_img
# ---------- 雷达触发显示逻辑 ----------
def update_display_mode_by_radar():
"""根据雷达数据更新显示模式"""
global display_mode, last_switch_time, current_alarm_index, current_radio_image
current_time = time.time()
with radar_lock:
# 获取报警的雷达列表
alarm_radars = [i for i in range(4) if radar_alarm_status[i]]
if not alarm_radars:
# 没有报警,显示四分屏
if display_mode != 0:
display_mode = 0
current_radio_image = None
print("✓ No alarm, switching to Quad View")
return
# 有报警,检查是否需要切换
if current_time - last_switch_time >= SWITCH_INTERVAL:
if len(alarm_radars) == 1:
# 单路报警,直接显示
new_mode = alarm_radars[0] + 1
if display_mode != new_mode:
display_mode = new_mode
current_radio_image = radio_images.get(new_mode)
print(f"✓ Single alarm: Switching to Camera {new_mode}")
else:
# 多路报警,循环显示
current_alarm_index = (current_alarm_index + 1) % len(alarm_radars)
new_mode = alarm_radars[current_alarm_index] + 1
display_mode = new_mode
current_radio_image = radio_images.get(new_mode)
print(f"✓ Multiple alarms: Cycling to Camera {new_mode}")
last_switch_time = current_time
# ---------- 人体检测处理 ----------
def process_frame_for_detection(frame, model, platform, co_helper, anchors):
if frame is None:
return None, None, None
original_h, original_w = frame.shape[:2]
pad_color = (0, 0, 0)
img = co_helper.letter_box(im=frame.copy(), new_shape=(IMG_SIZE[1], IMG_SIZE[0]), pad_color=pad_color)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
if platform in ['pytorch', 'onnx']:
input_data = img.transpose(2, 0, 1)
input_data = input_data.reshape(1, *input_data.shape).astype(np.float32) / 255.
else:
input_data = img
outputs = model.run([np.expand_dims(input_data, 0)])
boxes, classes, scores = post_process(outputs, anchors)
if boxes is not None:
boxes = co_helper.get_real_box(boxes)
return boxes, classes, scores
# ---------- 窗口设置函数 ----------
def setup_borderless_fullscreen_window():
window_name = 'RTSP Surveillance System'
cv2.setUseOptimized(True)
cv2.namedWindow(window_name, cv2.WND_PROP_FULLSCREEN)
cv2.resizeWindow(window_name, SCREEN_WIDTH, SCREEN_HEIGHT)
cv2.moveWindow(window_name, 0, 0)
cv2.setWindowProperty(window_name, cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
cv2.setWindowProperty(window_name, cv2.WND_PROP_TOPMOST, 1)
print(f"✓ Borderless fullscreen window created: {SCREEN_WIDTH}x{SCREEN_HEIGHT}")
print("✓ Window set to topmost and borderless")
return window_name
def hide_mouse():
try:
pyautogui.moveTo(1024, 600)
pyautogui.moveRel(0, 0)
except Exception as e:
print(f"Note: Could not hide mouse cursor: {e}")
# ---------- 创建显示帧 ----------
def create_display_frame(display_mode, current_combined, detection_cache, frame_counter):
"""根据显示模式创建显示帧"""
global current_radio_image, radar_distances
if display_mode == 0:
# 四分屏模式 - 全屏显示
if current_combined is None:
display_frame = create_no_signal_frame(SCREEN_WIDTH, SCREEN_HEIGHT)
else:
display_frame = cv2.resize(current_combined, (SCREEN_WIDTH, SCREEN_HEIGHT))
if detection_cache['boxes'] is not None:
original_h, original_w = current_combined.shape[:2]
scale_x = SCREEN_WIDTH / original_w
scale_y = SCREEN_HEIGHT / original_h
scaled_boxes = []
for box in detection_cache['boxes']:
x1, y1, x2, y2 = box
scaled_boxes.append([
x1 * scale_x, y1 * scale_y,
x2 * scale_x, y2 * scale_y
])
display_frame = draw(display_frame,
np.array(scaled_boxes),
detection_cache['scores'],
detection_cache['classes'])
# 显示雷达状态
with radar_lock:
alarm_count = sum(radar_alarm_status)
radar_text = f"雷达警告: {alarm_count}/4"
cv2.putText(display_frame, radar_text, (10, 60),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 255), 2)
else:
# 单画面模式 - 分屏显示
camera_id = display_mode
# 创建空白画布
display_frame = np.zeros((SCREEN_HEIGHT, SCREEN_WIDTH, 3), dtype=np.uint8)
# 左侧视频区域
with locks[camera_id - 1]:
single_frame = frames[camera_id - 1]
if single_frame is None or not stream_status[camera_id - 1]:
video_frame = create_single_no_signal(camera_id, VIDEO_WIDTH, SCREEN_HEIGHT)
else:
video_frame = cv2.resize(single_frame, (VIDEO_WIDTH, SCREEN_HEIGHT))
display_frame[0:SCREEN_HEIGHT, 0:VIDEO_WIDTH] = video_frame
# 右侧雷达区域
if current_radio_image is not None:
radar_display = current_radio_image.copy()
# 在雷达图片上叠加距离信息
with radar_lock:
distance = radar_distances[camera_id - 1]
alarm_status = radar_alarm_status[camera_id - 1]
# 显示距离信息
distance_text = f" {distance:.2f}m"
if alarm_status:
alarm_text = "接近预警!"
cv2.putText(radar_display, alarm_text + distance_text, (20, SCREEN_HEIGHT - 20),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 3)
display_frame[0:SCREEN_HEIGHT, VIDEO_WIDTH:SCREEN_WIDTH] = radar_display
# 显示报警状态
with radar_lock:
alarm_count = sum(radar_alarm_status)
alarm_text = f"雷达编号: {alarm_count}"
cv2.putText(display_frame, alarm_text, (10, 60),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
return display_frame
def create_no_signal_frame(width, height):
frame = np.zeros((height, width, 3), dtype=np.uint8)
frame[:] = (40, 40, 40)
text = "无信号"
font_scale = 1.5
thickness = 3
color = (100, 100, 255)
text_size = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, font_scale, thickness)[0]
text_x = (width - text_size[0]) // 2
text_y = (height + text_size[1]) // 2
cv2.putText(frame, text, (text_x, text_y),
cv2.FONT_HERSHEY_SIMPLEX, font_scale, color, thickness)
return frame
def create_single_no_signal(camera_id, width, height):
frame = np.zeros((height, width, 3), dtype=np.uint8)
frame[:] = (40, 40, 40)
camera_text = f"相机 {camera_id}"
no_signal_text = "无信号"
camera_size = cv2.getTextSize(camera_text, cv2.FONT_HERSHEY_SIMPLEX, 1.2, 2)[0]
camera_x = (width - camera_size[0]) // 2
camera_y = height // 2 - 40
cv2.putText(frame, camera_text, (camera_x, camera_y),
cv2.FONT_HERSHEY_SIMPLEX, 1.2, (100, 255, 100), 2)
no_signal_size = cv2.getTextSize(no_signal_text, cv2.FONT_HERSHEY_SIMPLEX, 1.5, 3)[0]
no_signal_x = (width - no_signal_size[0]) // 2
no_signal_y = camera_y + 60
cv2.putText(frame, no_signal_text, (no_signal_x, no_signal_y),
cv2.FONT_HERSHEY_SIMPLEX, 1.5, (100, 100, 255), 3)
status_text = "等待信号接入..."
status_size = cv2.getTextSize(status_text, cv2.FONT_HERSHEY_SIMPLEX, 0.7, 2)[0]
status_x = (width - status_size[0]) // 2
status_y = no_signal_y + 50
cv2.putText(frame, status_text, (status_x, status_y),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (100, 200, 255), 2)
return frame
# ---------- 主显示和检测循环 ----------
def main_detection_loop():
global stop_threads, combined_frame, display_mode
# 加载雷达图片
if not load_radio_images():
print("Warning: Radar images not loaded properly")
# 启动雷达读取器
radar_reader = RadarUARTReader('/dev/ttyS0', 115200)
if not radar_reader.start():
print("Warning: Radar reader failed to start")
# 加载 anchor
try:
with open(ANCHORS_FILE, 'r') as f:
values = [float(_v) for _v in f.readlines()]
anchors = np.array(values).reshape(3, -1, 2).tolist()
print(f"Using anchor file: {ANCHORS_FILE}")
except:
print("Warning: Cannot load anchor file, using default anchors")
anchors = []
# 初始化COCO帮助类
co_helper = COCO_test_helper(enable_letter_box=True)
# 加载模型
model, platform = setup_model(MODEL_PATH, device_id=0)
print("Model loaded, starting detection...")
# 设置无边框全屏窗口
if IMG_SHOW:
window_name = setup_borderless_fullscreen_window()
hide_mouse()
# 初始显示画面
initial_frame = np.zeros((SCREEN_HEIGHT, SCREEN_WIDTH, 3), dtype=np.uint8)
cv2.putText(initial_frame, "Initializing...", (SCREEN_WIDTH // 2 - 100, SCREEN_HEIGHT // 2),
cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
cv2.imshow(window_name, initial_frame)
cv2.waitKey(100)
frame_counter = 0
detection_cache = {
'boxes': None,
'classes': None,
'scores': None,
'last_frame': -1
}
try:
print("Entering main display loop...")
while not stop_threads:
# 根据雷达数据更新显示模式
update_display_mode_by_radar()
# 获取最新的合成帧
with combined_lock:
current_combined = combined_frame.copy() if combined_frame is not None else None
# 只在四分屏模式下进行人体检测
if display_mode == 0 and frame_counter % DETECTION_INTERVAL == 0:
if current_combined is not None:
boxes, classes, scores = process_frame_for_detection(
current_combined, model, platform, co_helper, anchors)
detection_cache.update({
'boxes': boxes,
'classes': classes,
'scores': scores,
'last_frame': frame_counter
})
# 创建并显示帧
if IMG_SHOW:
try:
display_frame = create_display_frame(display_mode, current_combined,
detection_cache, frame_counter)
cv2.imshow(window_name, display_frame)
# 只处理ESC键退出
key = cv2.waitKey(1) & 0xFF
if key == 27: # ESC键退出
stop_threads = True
break
except Exception as e:
print(f"Display error: {e}")
frame_counter += 1
time.sleep(0.01)
finally:
if IMG_SHOW:
cv2.destroyAllWindows()
if hasattr(model, 'release'):
model.release()
radar_reader.stop()
print("Detection loop ended")
# ---------- 主函数 ----------
if __name__ == "__main__":
os.environ['DISPLAY'] = ':0'
print("=" * 60)
print("RTSP监控系统 - 简化版本")
print("=" * 60)
print(f"RTSP URLs:")
for i, url in enumerate(RTSP_URLS):
print(f" Camera {i + 1}: {url}")
print("=" * 60)
# 初始化时间戳
current_time = time.time()
for i in range(4):
stream_last_frame_time[i] = current_time
# 启动四个RTSP流读取线程
rtsp_threads = []
for i in range(4):
t = threading.Thread(target=read_rtsp_stream, args=(i, RTSP_URLS[i]))
t.daemon = True
t.start()
rtsp_threads.append(t)
print(f"✓ 启动RTSP线程 {i}")
time.sleep(1) # 错开连接时间
# 启动画面合成线程
combine_thread = threading.Thread(target=combine_frames)
combine_thread.daemon = True
combine_thread.start()
print("✓ 启动画面合成线程")
try:
# 在主线程中启动检测循环
main_detection_loop()
except KeyboardInterrupt:
print("用户中断")
except Exception as e:
print(f"程序错误: {e}")
finally:
stop_threads = True
time.sleep(2)
print("所有线程已停止,程序退出")
| 1 | import cv2 |
| 2 | import threading |
| 3 | import numpy as np |
| 4 | import time |
| 5 | import os |
| 6 | import serial |
| 7 | |
| 8 | if 'DISPLAY' not in os.environ: |
| 9 | os.environ['DISPLAY'] = ':0' |
| 10 | print(f"Set DISPLAY to: {os.environ['DISPLAY']}") |
| 11 | import pyautogui |
| 12 | |
| 13 | # ---------- 1. 固定参数区 ---------- |
| 14 | MODEL_PATH = './yolov5s-640-640.rknn' # 模型路径 |
| 15 | TARGET = 'rk3588' # 目标设备 |
| 16 | |
| 17 | IMG_SHOW = True # 是否弹窗显示结果 |
| 18 | IMG_SAVE = False # 是否保存结果图片 |
| 19 | COCO_MAP_TEST = False # 是否跑COCO mAP测试 |
| 20 | FULLSCREEN = True # 是否全屏显示 |
| 21 | |
| 22 | SOURCES_FILE = './sources.json' # 视频源配置文件 |
| 23 | ANCHORS_FILE = './model/anchors_yolov5.txt' # anchor文件 |
| 24 | |
| 25 | # 优化参数 |
| 26 | DETECTION_INTERVAL = 5 # 检测间隔(每5帧检测一次) |
| 27 | DISPLAY_REFRESH_RATE = 30 # 显示刷新率(Hz) |
| 28 | |
| 29 | # 屏幕尺寸 |
| 30 | SCREEN_WIDTH = 1024 |
| 31 | SCREEN_HEIGHT = 600 |
| 32 | |
| 33 | # 显示区域尺寸 |
| 34 | VIDEO_WIDTH = int(SCREEN_WIDTH * 0.75) # 左侧四分之三显示视频 |
| 35 | RADIO_WIDTH = SCREEN_WIDTH - VIDEO_WIDTH # 右侧四分之一显示雷达 |
| 36 | |
| 37 | # ---------- 2. 雷达触发参数 ---------- |
| 38 | ALARM_DISTANCE = 1.5 # 报警距离(米) |
| 39 | SWITCH_INTERVAL = 3.0 # 切换间隔(秒) |
| 40 | |
| 41 | # ---------- 3. 其他全局常量 ---------- |
| 42 | OBJ_THRESH = 0.6 |
| 43 | NMS_THRESH = 0.6 |
| 44 | IMG_SIZE = (640, 640) # (width, height) |
| 45 | |
| 46 | CLASSES = ("person", "bicycle", "car", "motorbike ", "aeroplane ", "bus ", "train", "truck ", "boat", "traffic light") |
| 47 | |
| 48 | # -------------------------- |
| 49 | # RTSP流配置部分 - 简化版本 |
| 50 | # -------------------------- |
| 51 | RTSP_URLS = [ |
| 52 | "rtsp://admin:Admin888@192.168.112.200:554/streaming/channels/102", |
| 53 | "rtsp://admin:Admin888@192.168.112.201:554/streaming/channels/102", |
| 54 | "rtsp://admin:Admin888@192.168.112.202:554/streaming/channels/102", |
| 55 | "rtsp://admin:Admin888@192.168.112.203:554/streaming/channels/102" |
| 56 | ] |
| 57 | |
| 58 | # 全局变量 |
| 59 | frames = [None] * 4 |
| 60 | locks = [threading.Lock() for _ in range(4)] |
| 61 | stop_threads = False |
| 62 | combined_frame = None |
| 63 | combined_lock = threading.Lock() |
| 64 | |
| 65 | # 显示模式控制 |
| 66 | display_mode = 0 # 0:四分屏全屏, 1-4:单画面分屏 |
| 67 | last_switch_time = time.time() |
| 68 | current_alarm_index = 0 |
| 69 | |
| 70 | # 雷达数据相关 |
| 71 | radar_distances = [999.0] * 4 # 存储4路雷达距离(米) |
| 72 | radar_alarm_status = [False] * 4 # 雷达报警状态 |
| 73 | radar_lock = threading.Lock() |
| 74 | |
| 75 | # 雷达图片相关 |
| 76 | radio_images = {} # 存储4个雷达图片 |
| 77 | current_radio_image = None |
| 78 | |
| 79 | # 简化的流状态管理 |
| 80 | stream_status = [False] * 4 # 每路流的连接状态 |
| 81 | stream_last_frame_time = [0] * 4 # 每路流最后收到帧的时间 |
| 82 | |
| 83 | |
| 84 | # ---------- 简化的RTSP流处理 ---------- |
| 85 | def read_rtsp_stream(index, rtsp_url): |
| 86 | """简化的RTSP流读取函数""" |
| 87 | global stop_threads |
| 88 | |
| 89 | cap = None |
| 90 | reconnect_count = 0 |
| 91 | max_reconnect = 20 |
| 92 | |
| 93 | print(f"[Stream {index}] 启动RTSP流: {rtsp_url}") |
| 94 | |
| 95 | while not stop_threads and reconnect_count < max_reconnect: |
| 96 | try: |
| 97 | # 释放之前的连接 |
| 98 | if cap is not None: |
| 99 | cap.release() |
| 100 | cap = None |
| 101 | |
| 102 | # 创建新的连接 |
| 103 | print(f"[Stream {index}] 尝试连接...") |
| 104 | cap = cv2.VideoCapture(rtsp_url) |
| 105 | |
| 106 | # 设置连接参数 |
| 107 | cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) |
| 108 | cap.set(cv2.CAP_PROP_FPS, 15) |
| 109 | cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*'H264')) |
| 110 | cap.set(cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 5000) |
| 111 | cap.set(cv2.CAP_PROP_READ_TIMEOUT_MSEC, 3000) |
| 112 | |
| 113 | if not cap.isOpened(): |
| 114 | print(f"[Stream {index}] 连接失败") |
| 115 | reconnect_count += 1 |
| 116 | time.sleep(3) |
| 117 | continue |
| 118 | |
| 119 | print(f"[Stream {index}] ✓ 连接成功") |
| 120 | stream_status[index] = True |
| 121 | reconnect_count = 0 |
| 122 | |
| 123 | # 持续读取帧 |
| 124 | while not stop_threads and cap.isOpened(): |
| 125 | ret, frame = cap.read() |
| 126 | |
| 127 | if ret: |
| 128 | with locks[index]: |
| 129 | frames[index] = frame.copy() |
| 130 | stream_last_frame_time[index] = time.time() |
| 131 | stream_status[index] = True |
| 132 | else: |
| 133 | print(f"[Stream {index}] ✗ 读取帧失败") |
| 134 | stream_status[index] = False |
| 135 | break |
| 136 | |
| 137 | # 控制读取频率 |
| 138 | time.sleep(0.03) |
| 139 | |
| 140 | except Exception as e: |
| 141 | print(f"[Stream {index}] 异常: {e}") |
| 142 | stream_status[index] = False |
| 143 | |
| 144 | # 连接断开,准备重连 |
| 145 | if cap is not None: |
| 146 | cap.release() |
| 147 | cap = None |
| 148 | |
| 149 | reconnect_count += 1 |
| 150 | print(f"[Stream {index}] 等待重连 ({reconnect_count}/{max_reconnect})...") |
| 151 | time.sleep(3) |
| 152 | |
| 153 | print(f"[Stream {index}] 线程退出") |
| 154 | |
| 155 | |
| 156 | # ---------- 简化的画面合成函数 ---------- |
| 157 | def combine_frames(): |
| 158 | global combined_frame, stop_threads |
| 159 | |
| 160 | while not stop_threads: |
| 161 | frame_list = [] |
| 162 | current_time = time.time() |
| 163 | |
| 164 | for i in range(4): |
| 165 | with locks[i]: |
| 166 | # 检查流是否超时(5秒无数据认为超时) |
| 167 | if current_time - stream_last_frame_time[i] > 5.0: |
| 168 | stream_status[i] = False |
| 169 | frame_list.append(None) |
| 170 | elif frames[i] is not None: |
| 171 | frame_list.append(frames[i]) |
| 172 | else: |
| 173 | frame_list.append(None) |
| 174 | |
| 175 | # 检查是否有有效帧 |
| 176 | valid_frames = [frame for frame in frame_list if frame is not None] |
| 177 | if len(valid_frames) == 0: |
| 178 | time.sleep(0.1) |
| 179 | continue |
| 180 | |
| 181 | # 获取参考尺寸(使用第一个有效帧) |
| 182 | first_valid_frame = next(frame for frame in frame_list if frame is not None) |
| 183 | h, w, _ = first_valid_frame.shape |
| 184 | |
| 185 | # 创建合成画面 |
| 186 | combined = np.zeros((h * 2, w * 2, 3), dtype=np.uint8) |
| 187 | |
| 188 | positions = [ |
| 189 | (0, 0), # 左上 - Stream 0 |
| 190 | (0, w), # 右上 - Stream 1 |
| 191 | (h, 0), # 左下 - Stream 2 |
| 192 | (h, w) # 右下 - Stream 3 |
| 193 | ] |
| 194 | |
| 195 | for i, (y, x) in enumerate(positions): |
| 196 | if frame_list[i] is not None and stream_status[i]: |
| 197 | resized_frame = cv2.resize(frame_list[i], (w, h)) |
| 198 | combined[y:y + h, x:x + w] = resized_frame |
| 199 | |
| 200 | # 添加状态指示器 |
| 201 | status_color = (0, 255, 0) # 绿色 - 在线 |
| 202 | status_text = f"Cam{i + 1} ✓" |
| 203 | else: |
| 204 | # 显示无信号画面 |
| 205 | no_signal = create_single_no_signal(i + 1, w, h) |
| 206 | combined[y:y + h, x:x + w] = no_signal |
| 207 | status_color = (0, 0, 255) # 红色 - 离线 |
| 208 | status_text = f"Cam{i + 1} ✗" |
| 209 | |
| 210 | # 在画面角落添加状态指示 |
| 211 | cv2.putText(combined, status_text, (x + 10, y + 30), |
| 212 | cv2.FONT_HERSHEY_SIMPLEX, 0.6, status_color, 2) |
| 213 | |
| 214 | with combined_lock: |
| 215 | combined_frame = combined.copy() |
| 216 | |
| 217 | time.sleep(0.03) |
| 218 | |
| 219 | |
| 220 | # ---------- 雷达串口读取类 ---------- |
| 221 | class RadarUARTReader: |
| 222 | def __init__(self, port: str = '/dev/ttyS0', baudrate: int = 115200): |
| 223 | self.port = port |
| 224 | self.baudrate = baudrate |
| 225 | self.ser = None |
| 226 | self.running = False |
| 227 | self.receive_thread = None |
| 228 | |
| 229 | def start(self) -> bool: |
| 230 | """启动雷达UART读取""" |
| 231 | try: |
| 232 | self.ser = serial.Serial( |
| 233 | port=self.port, |
| 234 | baudrate=self.baudrate, |
| 235 | bytesize=8, |
| 236 | parity='N', |
| 237 | stopbits=1, |
| 238 | timeout=0.1 |
| 239 | ) |
| 240 | |
| 241 | self.running = True |
| 242 | self.receive_thread = threading.Thread(target=self._receive_worker) |
| 243 | self.receive_thread.daemon = True |
| 244 | self.receive_thread.start() |
| 245 | |
| 246 | print(f"雷达数据读取器已启动: {self.port} @ {self.baudrate}bps") |
| 247 | return True |
| 248 | |
| 249 | except Exception as e: |
| 250 | print(f"雷达启动失败: {e}") |
| 251 | return False |
| 252 | |
| 253 | def _parse_frame(self, frame_data: bytearray): |
| 254 | """解析雷达数据帧""" |
| 255 | if len(frame_data) != 16: |
| 256 | return None |
| 257 | |
| 258 | # 验证帧头帧尾 |
| 259 | if frame_data[0] != 0xA5 or frame_data[1] != 0x5A or frame_data[14] != 0x5A or frame_data[15] != 0xA5: |
| 260 | return None |
| 261 | |
| 262 | radar_data_list = [] |
| 263 | for i in range(4): |
| 264 | start_idx = 2 + i * 3 |
| 265 | radar_id = frame_data[start_idx] |
| 266 | distance_high = frame_data[start_idx + 1] |
| 267 | distance_low = frame_data[start_idx + 2] |
| 268 | |
| 269 | distance_mm = (distance_high << 8) | distance_low |
| 270 | distance_m = distance_mm / 1000.0 |
| 271 | |
| 272 | radar_data = { |
| 273 | 'radar_id': radar_id, |
| 274 | 'distance_mm': distance_mm, |
| 275 | 'distance_m': distance_m |
| 276 | } |
| 277 | radar_data_list.append(radar_data) |
| 278 | |
| 279 | return radar_data_list |
| 280 | |
| 281 | def _receive_worker(self): |
| 282 | """接收数据工作线程""" |
| 283 | state = 0 # 0:等待头, 1:接收数据 |
| 284 | frame_buffer = bytearray() |
| 285 | |
| 286 | while self.running and self.ser and self.ser.is_open: |
| 287 | try: |
| 288 | bytes_to_read = min(self.ser.in_waiting, 1024) |
| 289 | if bytes_to_read > 0: |
| 290 | data = self.ser.read(bytes_to_read) |
| 291 | |
| 292 | for byte in data: |
| 293 | if state == 0: |
| 294 | if byte == 0xA5: |
| 295 | frame_buffer = bytearray([byte]) |
| 296 | state = 1 |
| 297 | elif state == 1: |
| 298 | frame_buffer.append(byte) |
| 299 | if len(frame_buffer) == 16: |
| 300 | # 完整帧接收完成 |
| 301 | radar_data = self._parse_frame(frame_buffer) |
| 302 | if radar_data: |
| 303 | self._update_radar_distances(radar_data) |
| 304 | state = 0 |
| 305 | frame_buffer = bytearray() |
| 306 | elif len(frame_buffer) > 16: |
| 307 | state = 0 |
| 308 | frame_buffer = bytearray() |
| 309 | |
| 310 | except Exception as e: |
| 311 | print(f"雷达接收错误: {e}") |
| 312 | time.sleep(0.01) |
| 313 | |
| 314 | def _update_radar_distances(self, radar_data_list): |
| 315 | """更新雷达距离数据""" |
| 316 | global radar_distances, radar_alarm_status |
| 317 | |
| 318 | with radar_lock: |
| 319 | for data in radar_data_list: |
| 320 | radar_id = data['radar_id'] |
| 321 | if 1 <= radar_id <= 4: |
| 322 | distance = data['distance_m'] |
| 323 | radar_distances[radar_id - 1] = distance |
| 324 | radar_alarm_status[radar_id - 1] = (distance <= ALARM_DISTANCE and distance > 0) |
| 325 | |
| 326 | # 打印报警信息 |
| 327 | if radar_alarm_status[radar_id - 1]: |
| 328 | print(f"🚨 雷达{radar_id}报警! 距离: {distance:.2f}m") |
| 329 | |
| 330 | def stop(self): |
| 331 | """停止读取""" |
| 332 | self.running = False |
| 333 | if self.ser and self.ser.is_open: |
| 334 | self.ser.close() |
| 335 | print("雷达数据读取器已停止") |
| 336 | |
| 337 | |
| 338 | # ---------- 核心函数 ---------- |
| 339 | def filter_boxes(boxes, box_confidences, box_class_probs): |
| 340 | box_confidences = box_confidences.reshape(-1) |
| 341 | class_max_score = np.max(box_class_probs, axis=-1) |
| 342 | classes = np.argmax(box_class_probs, axis=-1) |
| 343 | |
| 344 | person_mask = (classes == 0) |
| 345 | _class_pos = np.where((class_max_score * box_confidences >= OBJ_THRESH) & person_mask) |
| 346 | scores = (class_max_score * box_confidences)[_class_pos] |
| 347 | |
| 348 | boxes = boxes[_class_pos] |
| 349 | classes = classes[_class_pos] |
| 350 | |
| 351 | return boxes, classes, scores |
| 352 | |
| 353 | |
| 354 | def nms_boxes(boxes, scores): |
| 355 | x = boxes[:, 0] |
| 356 | y = boxes[:, 1] |
| 357 | w = boxes[:, 2] - boxes[:, 0] |
| 358 | h = boxes[:, 3] - boxes[:, 1] |
| 359 | |
| 360 | areas = w * h |
| 361 | order = scores.argsort()[::-1] |
| 362 | |
| 363 | keep = [] |
| 364 | while order.size > 0: |
| 365 | i = order[0] |
| 366 | keep.append(i) |
| 367 | |
| 368 | xx1 = np.maximum(x[i], x[order[1:]]) |
| 369 | yy1 = np.maximum(y[i], y[order[1:]]) |
| 370 | xx2 = np.minimum(x[i] + w[i], x[order[1:]] + w[order[1:]]) |
| 371 | yy2 = np.minimum(y[i] + h[i], y[order[1:]] + h[order[1:]]) |
| 372 | |
| 373 | w1 = np.maximum(0.0, xx2 - xx1 + 0.00001) |
| 374 | h1 = np.maximum(0.0, yy2 - yy1 + 0.00001) |
| 375 | inter = w1 * h1 |
| 376 | |
| 377 | ovr = inter / (areas[i] + areas[order[1:]] - inter) |
| 378 | inds = np.where(ovr <= NMS_THRESH)[0] |
| 379 | order = order[inds + 1] |
| 380 | keep = np.array(keep) |
| 381 | return keep |
| 382 | |
| 383 | |
| 384 | def box_process(position, anchors): |
| 385 | grid_h, grid_w = position.shape[2:4] |
| 386 | col, row = np.meshgrid(np.arange(0, grid_w), np.arange(0, grid_h)) |
| 387 | col = col.reshape(1, 1, grid_h, grid_w) |
| 388 | row = row.reshape(1, 1, grid_h, grid_w) |
| 389 | grid = np.concatenate((col, row), axis=1) |
| 390 | stride = np.array([IMG_SIZE[1] // grid_h, IMG_SIZE[0] // grid_w]).reshape(1, 2, 1, 1) |
| 391 | |
| 392 | col = col.repeat(len(anchors), axis=0) |
| 393 | row = row.repeat(len(anchors), axis=0) |
| 394 | anchors = np.array(anchors) |
| 395 | anchors = anchors.reshape(*anchors.shape, 1, 1) |
| 396 | |
| 397 | box_xy = position[:, :2, :, :] * 2 - 0.5 |
| 398 | box_wh = pow(position[:, 2:4, :, :] * 2, 2) * anchors |
| 399 | |
| 400 | box_xy += grid |
| 401 | box_xy *= stride |
| 402 | box = np.concatenate((box_xy, box_wh), axis=1) |
| 403 | |
| 404 | xyxy = np.copy(box) |
| 405 | xyxy[:, 0, :, :] = box[:, 0, :, :] - box[:, 2, :, :] / 2 |
| 406 | xyxy[:, 1, :, :] = box[:, 1, :, :] - box[:, 3, :, :] / 2 |
| 407 | xyxy[:, 2, :, :] = box[:, 0, :, :] + box[:, 2, :, :] / 2 |
| 408 | xyxy[:, 3, :, :] = box[:, 1, :, :] + box[:, 3, :, :] / 2 |
| 409 | |
| 410 | return xyxy |
| 411 | |
| 412 | |
| 413 | def post_process(input_data, anchors): |
| 414 | boxes, scores, classes_conf = [], [], [] |
| 415 | input_data = [_in.reshape([len(anchors[0]), -1] + list(_in.shape[-2:])) for _in in input_data] |
| 416 | for i in range(len(input_data)): |
| 417 | boxes.append(box_process(input_data[i][:, :4, :, :], anchors[i])) |
| 418 | scores.append(input_data[i][:, 4:5, :, :]) |
| 419 | classes_conf.append(input_data[i][:, 5:, :, :]) |
| 420 | |
| 421 | def sp_flatten(_in): |
| 422 | ch = _in.shape[1] |
| 423 | _in = _in.transpose(0, 2, 3, 1) |
| 424 | return _in.reshape(-1, ch) |
| 425 | |
| 426 | boxes = [sp_flatten(_v) for _v in boxes] |
| 427 | classes_conf = [sp_flatten(_v) for _v in classes_conf] |
| 428 | scores = [sp_flatten(_v) for _v in scores] |
| 429 | |
| 430 | boxes = np.concatenate(boxes) |
| 431 | classes_conf = np.concatenate(classes_conf) |
| 432 | scores = np.concatenate(scores) |
| 433 | |
| 434 | boxes, classes, scores = filter_boxes(boxes, scores, classes_conf) |
| 435 | |
| 436 | nboxes, nclasses, nscores = [], [], [] |
| 437 | for c in set(classes): |
| 438 | inds = np.where(classes == c) |
| 439 | b = boxes[inds] |
| 440 | c = classes[inds] |
| 441 | s = scores[inds] |
| 442 | keep = nms_boxes(b, s) |
| 443 | |
| 444 | if len(keep) != 0: |
| 445 | nboxes.append(b[keep]) |
| 446 | nclasses.append(c[keep]) |
| 447 | nscores.append(s[keep]) |
| 448 | |
| 449 | if not nclasses and not nscores: |
| 450 | return None, None, None |
| 451 | |
| 452 | boxes = np.concatenate(nboxes) |
| 453 | classes = np.concatenate(nclasses) |
| 454 | scores = np.concatenate(nscores) |
| 455 | |
| 456 | return boxes, classes, scores |
| 457 | |
| 458 | |
| 459 | def draw(image, boxes, scores, classes): |
| 460 | """正确的人体检测框绘制函数""" |
| 461 | if boxes is None or len(boxes) == 0: |
| 462 | return image |
| 463 | |
| 464 | box_color = (0, 0, 255) |
| 465 | box_thickness = 3 |
| 466 | text_color = (0, 0, 255) |
| 467 | text_thickness = 2 |
| 468 | |
| 469 | for box, score, cl in zip(boxes, scores, classes): |
| 470 | x1, y1, x2, y2 = [int(_b) for _b in box] |
| 471 | |
| 472 | x1 = max(0, min(x1, image.shape[1] - 1)) |
| 473 | y1 = max(0, min(y1, image.shape[0] - 1)) |
| 474 | x2 = max(0, min(x2, image.shape[1] - 1)) |
| 475 | y2 = max(0, min(y2, image.shape[0] - 1)) |
| 476 | |
| 477 | cv2.rectangle(image, (x1, y1), (x2, y2), box_color, box_thickness) |
| 478 | |
| 479 | text = f'{CLASSES[cl]} {score:.2f}' |
| 480 | cv2.putText(image, text, (x1, y1 - 10), |
| 481 | cv2.FONT_HERSHEY_SIMPLEX, 0.6, text_color, text_thickness) |
| 482 | |
| 483 | return image |
| 484 | |
| 485 | |
| 486 | def setup_model(model_path, device_id=None): |
| 487 | """为每个NPU核心创建独立模型实例""" |
| 488 | if model_path.endswith('.pt') or model_path.endswith('.torchscript'): |
| 489 | platform = 'pytorch' |
| 490 | from py_utils.pytorch_executor import Torch_model_container |
| 491 | model = Torch_model_container(model_path) |
| 492 | elif model_path.endswith('.rknn'): |
| 493 | platform = 'rknn' |
| 494 | from py_utils.rknn_executor import RKNN_model_container |
| 495 | model = RKNN_model_container(model_path, TARGET, device_id) |
| 496 | elif model_path.endswith('onnx'): |
| 497 | platform = 'onnx' |
| 498 | from py_utils.onnx_executor import ONNX_model_container |
| 499 | model = ONNX_model_container(model_path) |
| 500 | else: |
| 501 | raise RuntimeError(f"{model_path} is not rknn/pytorch/onnx model") |
| 502 | print(f'Model {model_path} initialized for device {device_id}') |
| 503 | return model, platform |
| 504 | |
| 505 | |
| 506 | # ---------- COCO_test_helper类 ---------- |
| 507 | class COCO_test_helper: |
| 508 | def __init__(self, enable_letter_box=True): |
| 509 | self.enable_letter_box = enable_letter_box |
| 510 | self.image_ids = [] |
| 511 | self.category_ids = [] |
| 512 | self.bboxes = [] |
| 513 | self.scores = [] |
| 514 | self.pad_top = 0 |
| 515 | self.pad_left = 0 |
| 516 | self.scale_ratio = 1.0 |
| 517 | self.original_shape = (0, 0) |
| 518 | self.new_shape = (0, 0) |
| 519 | |
| 520 | def letter_box(self, im, new_shape, pad_color=(0, 0, 0)): |
| 521 | shape = im.shape[:2] |
| 522 | if isinstance(new_shape, int): |
| 523 | new_shape = (new_shape, new_shape) |
| 524 | |
| 525 | r = min(new_shape[0] / shape[0], new_shape[1] / shape[1]) |
| 526 | new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r)) |
| 527 | dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1] |
| 528 | |
| 529 | dw /= 2 |
| 530 | dh /= 2 |
| 531 | |
| 532 | if shape[::-1] != new_unpad: |
| 533 | im = cv2.resize(im, new_unpad, interpolation=cv2.INTER_LINEAR) |
| 534 | |
| 535 | top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1)) |
| 536 | left, right = int(round(dw - 0.1)), int(round(dw + 0.1)) |
| 537 | |
| 538 | self.pad_top = top |
| 539 | self.pad_left = left |
| 540 | self.scale_ratio = r |
| 541 | self.original_shape = shape |
| 542 | self.new_shape = new_shape |
| 543 | |
| 544 | im = cv2.copyMakeBorder(im, top, bottom, left, right, cv2.BORDER_CONSTANT, value=pad_color) |
| 545 | return im |
| 546 | |
| 547 | def get_real_box(self, boxes): |
| 548 | if boxes is None or len(boxes) == 0: |
| 549 | return boxes |
| 550 | |
| 551 | real_boxes = [] |
| 552 | for box in boxes: |
| 553 | x1, y1, x2, y2 = box |
| 554 | |
| 555 | x1 = (x1 - self.pad_left) / self.scale_ratio |
| 556 | y1 = (y1 - self.pad_top) / self.scale_ratio |
| 557 | x2 = (x2 - self.pad_left) / self.scale_ratio |
| 558 | y2 = (y2 - self.pad_top) / self.scale_ratio |
| 559 | |
| 560 | x1 = max(0, min(x1, self.original_shape[1])) |
| 561 | y1 = max(0, min(y1, self.original_shape[0])) |
| 562 | x2 = max(0, min(x2, self.original_shape[1])) |
| 563 | y2 = max(0, min(y2, self.original_shape[0])) |
| 564 | |
| 565 | real_boxes.append([x1, y1, x2, y2]) |
| 566 | |
| 567 | return np.array(real_boxes) |
| 568 | |
| 569 | |
| 570 | # ---------- 加载雷达图片 ---------- |
| 571 | def load_radio_images(): |
| 572 | """加载1-4号雷达图片""" |
| 573 | global radio_images |
| 574 | |
| 575 | for i in range(1, 5): |
| 576 | try: |
| 577 | img_path = f'{i}.png' |
| 578 | radio_img = cv2.imread(img_path) |
| 579 | if radio_img is None: |
| 580 | print(f"Warning: Cannot load {img_path}, creating default radar image") |
| 581 | radio_img = create_default_radio(i) |
| 582 | else: |
| 583 | print(f"✓ Loaded radar image: {img_path}") |
| 584 | |
| 585 | radio_images[i] = cv2.resize(radio_img, (RADIO_WIDTH, SCREEN_HEIGHT)) |
| 586 | except Exception as e: |
| 587 | print(f"Failed to load radar image {i}.png: {e}") |
| 588 | radio_images[i] = create_default_radio(i) |
| 589 | |
| 590 | return len(radio_images) > 0 |
| 591 | |
| 592 | |
| 593 | def create_default_radio(radar_id): |
| 594 | """创建默认雷达图片""" |
| 595 | radio_img = np.zeros((SCREEN_HEIGHT, RADIO_WIDTH, 3), dtype=np.uint8) |
| 596 | radio_img[:] = (20, 20, 20) |
| 597 | |
| 598 | center_x = RADIO_WIDTH // 2 |
| 599 | center_y = SCREEN_HEIGHT // 2 |
| 600 | radius = min(RADIO_WIDTH, SCREEN_HEIGHT) // 3 |
| 601 | |
| 602 | # 绘制雷达圆圈 |
| 603 | for r in range(radius, 0, -radius // 4): |
| 604 | color = (0, 255, 0) |
| 605 | cv2.circle(radio_img, (center_x, center_y), r, color, 2) |
| 606 | |
| 607 | # 绘制雷达线 |
| 608 | for angle in range(0, 360, 30): |
| 609 | rad = np.deg2rad(angle) |
| 610 | end_x = int(center_x + radius * np.cos(rad)) |
| 611 | end_y = int(center_y + radius * np.sin(rad)) |
| 612 | cv2.line(radio_img, (center_x, center_y), (end_x, end_y), (0, 255, 0), 1) |
| 613 | |
| 614 | # 显示雷达编号 |
| 615 | title = f"Radar {radar_id}" |
| 616 | title_size = cv2.getTextSize(title, cv2.FONT_HERSHEY_SIMPLEX, 0.8, 2)[0] |
| 617 | title_x = (RADIO_WIDTH - title_size[0]) // 2 |
| 618 | cv2.putText(radio_img, title, (title_x, 30), |
| 619 | cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 255, 255), 2) |
| 620 | |
| 621 | return radio_img |
| 622 | |
| 623 | |
| 624 | # ---------- 雷达触发显示逻辑 ---------- |
| 625 | def update_display_mode_by_radar(): |
| 626 | """根据雷达数据更新显示模式""" |
| 627 | global display_mode, last_switch_time, current_alarm_index, current_radio_image |
| 628 | |
| 629 | current_time = time.time() |
| 630 | |
| 631 | with radar_lock: |
| 632 | # 获取报警的雷达列表 |
| 633 | alarm_radars = [i for i in range(4) if radar_alarm_status[i]] |
| 634 | |
| 635 | if not alarm_radars: |
| 636 | # 没有报警,显示四分屏 |
| 637 | if display_mode != 0: |
| 638 | display_mode = 0 |
| 639 | current_radio_image = None |
| 640 | print("✓ No alarm, switching to Quad View") |
| 641 | return |
| 642 | |
| 643 | # 有报警,检查是否需要切换 |
| 644 | if current_time - last_switch_time >= SWITCH_INTERVAL: |
| 645 | if len(alarm_radars) == 1: |
| 646 | # 单路报警,直接显示 |
| 647 | new_mode = alarm_radars[0] + 1 |
| 648 | if display_mode != new_mode: |
| 649 | display_mode = new_mode |
| 650 | current_radio_image = radio_images.get(new_mode) |
| 651 | print(f"✓ Single alarm: Switching to Camera {new_mode}") |
| 652 | else: |
| 653 | # 多路报警,循环显示 |
| 654 | current_alarm_index = (current_alarm_index + 1) % len(alarm_radars) |
| 655 | new_mode = alarm_radars[current_alarm_index] + 1 |
| 656 | display_mode = new_mode |
| 657 | current_radio_image = radio_images.get(new_mode) |
| 658 | print(f"✓ Multiple alarms: Cycling to Camera {new_mode}") |
| 659 | |
| 660 | last_switch_time = current_time |
| 661 | |
| 662 | |
| 663 | # ---------- 人体检测处理 ---------- |
| 664 | def process_frame_for_detection(frame, model, platform, co_helper, anchors): |
| 665 | if frame is None: |
| 666 | return None, None, None |
| 667 | |
| 668 | original_h, original_w = frame.shape[:2] |
| 669 | pad_color = (0, 0, 0) |
| 670 | img = co_helper.letter_box(im=frame.copy(), new_shape=(IMG_SIZE[1], IMG_SIZE[0]), pad_color=pad_color) |
| 671 | img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) |
| 672 | |
| 673 | if platform in ['pytorch', 'onnx']: |
| 674 | input_data = img.transpose(2, 0, 1) |
| 675 | input_data = input_data.reshape(1, *input_data.shape).astype(np.float32) / 255. |
| 676 | else: |
| 677 | input_data = img |
| 678 | |
| 679 | outputs = model.run([np.expand_dims(input_data, 0)]) |
| 680 | boxes, classes, scores = post_process(outputs, anchors) |
| 681 | |
| 682 | if boxes is not None: |
| 683 | boxes = co_helper.get_real_box(boxes) |
| 684 | |
| 685 | return boxes, classes, scores |
| 686 | |
| 687 | |
| 688 | # ---------- 窗口设置函数 ---------- |
| 689 | def setup_borderless_fullscreen_window(): |
| 690 | window_name = 'RTSP Surveillance System' |
| 691 | cv2.setUseOptimized(True) |
| 692 | cv2.namedWindow(window_name, cv2.WND_PROP_FULLSCREEN) |
| 693 | cv2.resizeWindow(window_name, SCREEN_WIDTH, SCREEN_HEIGHT) |
| 694 | cv2.moveWindow(window_name, 0, 0) |
| 695 | cv2.setWindowProperty(window_name, cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN) |
| 696 | cv2.setWindowProperty(window_name, cv2.WND_PROP_TOPMOST, 1) |
| 697 | |
| 698 | print(f"✓ Borderless fullscreen window created: {SCREEN_WIDTH}x{SCREEN_HEIGHT}") |
| 699 | print("✓ Window set to topmost and borderless") |
| 700 | return window_name |
| 701 | |
| 702 | |
| 703 | def hide_mouse(): |
| 704 | try: |
| 705 | pyautogui.moveTo(1024, 600) |
| 706 | pyautogui.moveRel(0, 0) |
| 707 | except Exception as e: |
| 708 | print(f"Note: Could not hide mouse cursor: {e}") |
| 709 | |
| 710 | |
| 711 | # ---------- 创建显示帧 ---------- |
| 712 | def create_display_frame(display_mode, current_combined, detection_cache, frame_counter): |
| 713 | """根据显示模式创建显示帧""" |
| 714 | global current_radio_image, radar_distances |
| 715 | |
| 716 | if display_mode == 0: |
| 717 | # 四分屏模式 - 全屏显示 |
| 718 | if current_combined is None: |
| 719 | display_frame = create_no_signal_frame(SCREEN_WIDTH, SCREEN_HEIGHT) |
| 720 | else: |
| 721 | display_frame = cv2.resize(current_combined, (SCREEN_WIDTH, SCREEN_HEIGHT)) |
| 722 | |
| 723 | if detection_cache['boxes'] is not None: |
| 724 | original_h, original_w = current_combined.shape[:2] |
| 725 | scale_x = SCREEN_WIDTH / original_w |
| 726 | scale_y = SCREEN_HEIGHT / original_h |
| 727 | |
| 728 | scaled_boxes = [] |
| 729 | for box in detection_cache['boxes']: |
| 730 | x1, y1, x2, y2 = box |
| 731 | scaled_boxes.append([ |
| 732 | x1 * scale_x, y1 * scale_y, |
| 733 | x2 * scale_x, y2 * scale_y |
| 734 | ]) |
| 735 | |
| 736 | display_frame = draw(display_frame, |
| 737 | np.array(scaled_boxes), |
| 738 | detection_cache['scores'], |
| 739 | detection_cache['classes']) |
| 740 | |
| 741 | # 显示雷达状态 |
| 742 | with radar_lock: |
| 743 | alarm_count = sum(radar_alarm_status) |
| 744 | radar_text = f"雷达警告: {alarm_count}/4" |
| 745 | cv2.putText(display_frame, radar_text, (10, 60), |
| 746 | cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 255), 2) |
| 747 | |
| 748 | else: |
| 749 | # 单画面模式 - 分屏显示 |
| 750 | camera_id = display_mode |
| 751 | |
| 752 | # 创建空白画布 |
| 753 | display_frame = np.zeros((SCREEN_HEIGHT, SCREEN_WIDTH, 3), dtype=np.uint8) |
| 754 | |
| 755 | # 左侧视频区域 |
| 756 | with locks[camera_id - 1]: |
| 757 | single_frame = frames[camera_id - 1] |
| 758 | |
| 759 | if single_frame is None or not stream_status[camera_id - 1]: |
| 760 | video_frame = create_single_no_signal(camera_id, VIDEO_WIDTH, SCREEN_HEIGHT) |
| 761 | else: |
| 762 | video_frame = cv2.resize(single_frame, (VIDEO_WIDTH, SCREEN_HEIGHT)) |
| 763 | |
| 764 | display_frame[0:SCREEN_HEIGHT, 0:VIDEO_WIDTH] = video_frame |
| 765 | |
| 766 | # 右侧雷达区域 |
| 767 | if current_radio_image is not None: |
| 768 | radar_display = current_radio_image.copy() |
| 769 | |
| 770 | # 在雷达图片上叠加距离信息 |
| 771 | with radar_lock: |
| 772 | distance = radar_distances[camera_id - 1] |
| 773 | alarm_status = radar_alarm_status[camera_id - 1] |
| 774 | |
| 775 | # 显示距离信息 |
| 776 | distance_text = f" {distance:.2f}m" |
| 777 | |
| 778 | if alarm_status: |
| 779 | alarm_text = "接近预警!" |
| 780 | cv2.putText(radar_display, alarm_text + distance_text, (20, SCREEN_HEIGHT - 20), |
| 781 | cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 3) |
| 782 | |
| 783 | display_frame[0:SCREEN_HEIGHT, VIDEO_WIDTH:SCREEN_WIDTH] = radar_display |
| 784 | |
| 785 | # 显示报警状态 |
| 786 | with radar_lock: |
| 787 | alarm_count = sum(radar_alarm_status) |
| 788 | alarm_text = f"雷达编号: {alarm_count}" |
| 789 | cv2.putText(display_frame, alarm_text, (10, 60), |
| 790 | cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2) |
| 791 | |
| 792 | return display_frame |
| 793 | |
| 794 | |
| 795 | def create_no_signal_frame(width, height): |
| 796 | frame = np.zeros((height, width, 3), dtype=np.uint8) |
| 797 | frame[:] = (40, 40, 40) |
| 798 | |
| 799 | text = "无信号" |
| 800 | font_scale = 1.5 |
| 801 | thickness = 3 |
| 802 | color = (100, 100, 255) |
| 803 | |
| 804 | text_size = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, font_scale, thickness)[0] |
| 805 | text_x = (width - text_size[0]) // 2 |
| 806 | text_y = (height + text_size[1]) // 2 |
| 807 | |
| 808 | cv2.putText(frame, text, (text_x, text_y), |
| 809 | cv2.FONT_HERSHEY_SIMPLEX, font_scale, color, thickness) |
| 810 | |
| 811 | return frame |
| 812 | |
| 813 | |
| 814 | def create_single_no_signal(camera_id, width, height): |
| 815 | frame = np.zeros((height, width, 3), dtype=np.uint8) |
| 816 | frame[:] = (40, 40, 40) |
| 817 | |
| 818 | camera_text = f"相机 {camera_id}" |
| 819 | no_signal_text = "无信号" |
| 820 | |
| 821 | camera_size = cv2.getTextSize(camera_text, cv2.FONT_HERSHEY_SIMPLEX, 1.2, 2)[0] |
| 822 | camera_x = (width - camera_size[0]) // 2 |
| 823 | camera_y = height // 2 - 40 |
| 824 | |
| 825 | cv2.putText(frame, camera_text, (camera_x, camera_y), |
| 826 | cv2.FONT_HERSHEY_SIMPLEX, 1.2, (100, 255, 100), 2) |
| 827 | |
| 828 | no_signal_size = cv2.getTextSize(no_signal_text, cv2.FONT_HERSHEY_SIMPLEX, 1.5, 3)[0] |
| 829 | no_signal_x = (width - no_signal_size[0]) // 2 |
| 830 | no_signal_y = camera_y + 60 |
| 831 | |
| 832 | cv2.putText(frame, no_signal_text, (no_signal_x, no_signal_y), |
| 833 | cv2.FONT_HERSHEY_SIMPLEX, 1.5, (100, 100, 255), 3) |
| 834 | |
| 835 | status_text = "等待信号接入..." |
| 836 | status_size = cv2.getTextSize(status_text, cv2.FONT_HERSHEY_SIMPLEX, 0.7, 2)[0] |
| 837 | status_x = (width - status_size[0]) // 2 |
| 838 | status_y = no_signal_y + 50 |
| 839 | cv2.putText(frame, status_text, (status_x, status_y), |
| 840 | cv2.FONT_HERSHEY_SIMPLEX, 0.7, (100, 200, 255), 2) |
| 841 | |
| 842 | return frame |
| 843 | |
| 844 | |
| 845 | # ---------- 主显示和检测循环 ---------- |
| 846 | def main_detection_loop(): |
| 847 | global stop_threads, combined_frame, display_mode |
| 848 | |
| 849 | # 加载雷达图片 |
| 850 | if not load_radio_images(): |
| 851 | print("Warning: Radar images not loaded properly") |
| 852 | |
| 853 | # 启动雷达读取器 |
| 854 | radar_reader = RadarUARTReader('/dev/ttyS0', 115200) |
| 855 | if not radar_reader.start(): |
| 856 | print("Warning: Radar reader failed to start") |
| 857 | |
| 858 | # 加载 anchor |
| 859 | try: |
| 860 | with open(ANCHORS_FILE, 'r') as f: |
| 861 | values = [float(_v) for _v in f.readlines()] |
| 862 | anchors = np.array(values).reshape(3, -1, 2).tolist() |
| 863 | print(f"Using anchor file: {ANCHORS_FILE}") |
| 864 | except: |
| 865 | print("Warning: Cannot load anchor file, using default anchors") |
| 866 | anchors = [] |
| 867 | |
| 868 | # 初始化COCO帮助类 |
| 869 | co_helper = COCO_test_helper(enable_letter_box=True) |
| 870 | |
| 871 | # 加载模型 |
| 872 | model, platform = setup_model(MODEL_PATH, device_id=0) |
| 873 | print("Model loaded, starting detection...") |
| 874 | |
| 875 | # 设置无边框全屏窗口 |
| 876 | if IMG_SHOW: |
| 877 | window_name = setup_borderless_fullscreen_window() |
| 878 | hide_mouse() |
| 879 | |
| 880 | # 初始显示画面 |
| 881 | initial_frame = np.zeros((SCREEN_HEIGHT, SCREEN_WIDTH, 3), dtype=np.uint8) |
| 882 | cv2.putText(initial_frame, "Initializing...", (SCREEN_WIDTH // 2 - 100, SCREEN_HEIGHT // 2), |
| 883 | cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2) |
| 884 | cv2.imshow(window_name, initial_frame) |
| 885 | cv2.waitKey(100) |
| 886 | |
| 887 | frame_counter = 0 |
| 888 | detection_cache = { |
| 889 | 'boxes': None, |
| 890 | 'classes': None, |
| 891 | 'scores': None, |
| 892 | 'last_frame': -1 |
| 893 | } |
| 894 | |
| 895 | try: |
| 896 | print("Entering main display loop...") |
| 897 | |
| 898 | while not stop_threads: |
| 899 | # 根据雷达数据更新显示模式 |
| 900 | update_display_mode_by_radar() |
| 901 | |
| 902 | # 获取最新的合成帧 |
| 903 | with combined_lock: |
| 904 | current_combined = combined_frame.copy() if combined_frame is not None else None |
| 905 | |
| 906 | # 只在四分屏模式下进行人体检测 |
| 907 | if display_mode == 0 and frame_counter % DETECTION_INTERVAL == 0: |
| 908 | if current_combined is not None: |
| 909 | boxes, classes, scores = process_frame_for_detection( |
| 910 | current_combined, model, platform, co_helper, anchors) |
| 911 | detection_cache.update({ |
| 912 | 'boxes': boxes, |
| 913 | 'classes': classes, |
| 914 | 'scores': scores, |
| 915 | 'last_frame': frame_counter |
| 916 | }) |
| 917 | |
| 918 | # 创建并显示帧 |
| 919 | if IMG_SHOW: |
| 920 | try: |
| 921 | display_frame = create_display_frame(display_mode, current_combined, |
| 922 | detection_cache, frame_counter) |
| 923 | cv2.imshow(window_name, display_frame) |
| 924 | |
| 925 | # 只处理ESC键退出 |
| 926 | key = cv2.waitKey(1) & 0xFF |
| 927 | if key == 27: # ESC键退出 |
| 928 | stop_threads = True |
| 929 | break |
| 930 | |
| 931 | except Exception as e: |
| 932 | print(f"Display error: {e}") |
| 933 | |
| 934 | frame_counter += 1 |
| 935 | time.sleep(0.01) |
| 936 | |
| 937 | finally: |
| 938 | if IMG_SHOW: |
| 939 | cv2.destroyAllWindows() |
| 940 | if hasattr(model, 'release'): |
| 941 | model.release() |
| 942 | radar_reader.stop() |
| 943 | print("Detection loop ended") |
| 944 | |
| 945 | |
| 946 | # ---------- 主函数 ---------- |
| 947 | if __name__ == "__main__": |
| 948 | os.environ['DISPLAY'] = ':0' |
| 949 | |
| 950 | print("=" * 60) |
| 951 | print("RTSP监控系统 - 简化版本") |
| 952 | print("=" * 60) |
| 953 | print(f"RTSP URLs:") |
| 954 | for i, url in enumerate(RTSP_URLS): |
| 955 | print(f" Camera {i + 1}: {url}") |
| 956 | print("=" * 60) |
| 957 | |
| 958 | # 初始化时间戳 |
| 959 | current_time = time.time() |
| 960 | for i in range(4): |
| 961 | stream_last_frame_time[i] = current_time |
| 962 | |
| 963 | # 启动四个RTSP流读取线程 |
| 964 | rtsp_threads = [] |
| 965 | for i in range(4): |
| 966 | t = threading.Thread(target=read_rtsp_stream, args=(i, RTSP_URLS[i])) |
| 967 | t.daemon = True |
| 968 | t.start() |
| 969 | rtsp_threads.append(t) |
| 970 | print(f"✓ 启动RTSP线程 {i}") |
| 971 | time.sleep(1) # 错开连接时间 |
| 972 | |
| 973 | # 启动画面合成线程 |
| 974 | combine_thread = threading.Thread(target=combine_frames) |
| 975 | combine_thread.daemon = True |
| 976 | combine_thread.start() |
| 977 | print("✓ 启动画面合成线程") |
| 978 | |
| 979 | try: |
| 980 | # 在主线程中启动检测循环 |
| 981 | main_detection_loop() |
| 982 | except KeyboardInterrupt: |
| 983 | print("用户中断") |
| 984 | except Exception as e: |
| 985 | print(f"程序错误: {e}") |
| 986 | finally: |
| 987 | stop_threads = True |
| 988 | time.sleep(2) |
| 989 | print("所有线程已停止,程序退出") |