Última actividad 1782445070

PowLu revisó este gist 1782445070. Ir a la revisión

1 file changed, 989 insertions

第一版主程序.py(archivo creado)

@@ -0,0 +1,989 @@
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("所有线程已停止,程序退出")
Siguiente Anterior