import cv2 import threading import sys, os, time import argparse import numpy as np import serial from collections import deque from PyQt5.QtWidgets import (QApplication, QLabel, QWidget, QVBoxLayout, QHBoxLayout, QMainWindow, QStackedWidget, QSlider, QPushButton, QGridLayout, QFrame) from PyQt5.QtGui import QImage, QPixmap, QFont from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QObject from PIL import Image, ImageDraw, ImageFont try: from surround_view import FisheyeCameraModel, BirdView import surround_view.param_settings as settings except ImportError as e: print("[ERROR] Cannot import surround_view modules: {}".format(e)) sys.exit(1) # ========================================================== # 1. 字体管理 # ========================================================== class FontManager: _fonts = {} _lock = threading.Lock() @classmethod def get(cls, size=20): with cls._lock: if size not in cls._fonts: candidates = [ "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc", "/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf", "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc", ] loaded = False for p in candidates: if os.path.exists(p): try: cls._fonts[size] = ImageFont.truetype(p, size) loaded = True break except Exception: continue if not loaded: cls._fonts[size] = ImageFont.load_default() return cls._fonts[size] # ========================================================== # 2. 中文叠加 # ========================================================== def add_texts_to_pil(pil_image, text_list): draw = ImageDraw.Draw(pil_image) for item in text_list: if len(item) == 5: x, y, text, color, size = item font = FontManager.get(size) else: x, y, text, color = item font = FontManager.get(20) draw.text((x + 1, y + 1), text, font=font, fill=(0, 0, 0)) draw.text((x, y), text, font=font, fill=color) # ========================================================== # 3. 串口1:收发线程 # 发送帧格式:55 [前] [左] [后] [右] [00] [雷达阈值dm] [校验] AA # 校验 = (前+左+后+右+00+雷达阈值) & 0xFF # ========================================================== SEND_INTERVAL_MS = 100 class SerialWorker(threading.Thread): def __init__(self, port="/dev/ttyS0", baudrate=115200): super().__init__(daemon=True) self.port = port self.baudrate = baudrate self.running = True # 接收状态 self._rx_lock = threading.Lock() self._radar_dist = 0 self._stopped = False self._active_dirs = [] # 接收防抖 self._rx_pending_dirs = [] self._rx_pending_radar = 0 self._rx_pending_stop = False self._rx_pending_count = 0 RX_DEBOUNCE = 3 # 发送内容 self._tx_lock = threading.Lock() self._tx_front = 0x00 self._tx_left = 0x00 self._tx_back = 0x00 self._tx_right = 0x00 self._tx_radar_thr = 0x00 # 发送防抖 self._stable_presence = [0x00, 0x00, 0x00, 0x00] self._pending_presence = [0x00, 0x00, 0x00, 0x00] self._pending_count = 0 self.DEBOUNCE_FRAMES = 9 # 打印计数(防刷屏) self._tx_print_cnt = 0 def update_send(self, front, left, back, right, radar_thr): """外部调用,更新待发送的有人/无人状态及雷达阈值""" presence = [front & 0xFF, left & 0xFF, back & 0xFF, right & 0xFF] # 发送防抖:连续 DEBOUNCE_FRAMES 次一致才更新稳定值 if presence == self._pending_presence: self._pending_count += 1 else: self._pending_presence = presence[:] self._pending_count = 1 if self._pending_count >= self.DEBOUNCE_FRAMES: self._stable_presence = presence[:] with self._tx_lock: self._tx_front = self._stable_presence[0] self._tx_left = self._stable_presence[1] self._tx_back = self._stable_presence[2] self._tx_right = self._stable_presence[3] self._tx_radar_thr = radar_thr & 0xFF def _build_tx_frame(self): """按协议构建9字节发送帧""" with self._tx_lock: b1 = self._tx_front # 前 b2 = self._tx_left # 左 b3 = self._tx_back # 后 b4 = self._tx_right # 右 b5 = 0x00 # 固定00 b6 = self._tx_radar_thr # 雷达阈值dm chk = (b1 + b2 + b3 + b4 + b5 + b6) & 0xFF return bytes([0x55, b1, b2, b3, b4, b5, b6, chk, 0xAA]) def get_state(self): with self._rx_lock: return list(self._active_dirs), self._radar_dist, self._stopped def _parse_rx_frame(self, frame: bytes): if len(frame) != 9 or frame[0] != 0x55 or frame[8] != 0xAA: return # 打印接收原始帧 print("[RX ttyS0] {}".format( " ".join("{:02X}".format(b) for b in frame))) front = frame[1]; left = frame[2] back = frame[3]; right = frame[4] radar = frame[5]; stop = frame[6] active = [] for name, val in [("front", front), ("left", left), ("back", back), ("right", right)]: if val != 0x00: active.append(name) # 接收防抖 if (active == self._rx_pending_dirs and radar == self._rx_pending_radar and (stop != 0x00) == self._rx_pending_stop): self._rx_pending_count += 1 else: self._rx_pending_dirs = active self._rx_pending_radar = radar self._rx_pending_stop = (stop != 0x00) self._rx_pending_count = 1 if self._rx_pending_count >= 3: with self._rx_lock: self._active_dirs = active self._radar_dist = radar self._stopped = (stop != 0x00) def run(self): try: ser = serial.Serial( port=self.port, baudrate=self.baudrate, bytesize=serial.EIGHTBITS, stopbits=serial.STOPBITS_ONE, parity=serial.PARITY_NONE, timeout=0.05) except Exception as e: print("[串口1 ERROR] 无法打开: {}".format(e)) return # ── 发送子线程 ── def _send_loop(): interval = SEND_INTERVAL_MS / 1000.0 while self.running: try: frame = self._build_tx_frame() ser.write(frame) # 每10次打印一次发送内容(约1秒) self._tx_print_cnt += 1 if self._tx_print_cnt % 10 == 0: print("[TX ttyS0] {} 含义: 前={:02X} 左={:02X} 后={:02X} 右={:02X} 雷达阈值={:02X}dm 校验={:02X}".format( " ".join("{:02X}".format(b) for b in frame), frame[1], frame[2], frame[3], frame[4], frame[6], frame[7])) except Exception as e: if self.running: print("[串口1 TX ERROR] {}".format(e)) time.sleep(interval) tx_thread = threading.Thread(target=_send_loop, daemon=True) tx_thread.start() # ── 接收主循环 ── buf = bytearray() while self.running: try: byte = ser.read(1) if not byte: continue b = byte[0] if len(buf) == 0: if b == 0x55: buf.append(b) continue buf.append(b) if len(buf) == 9: self._parse_rx_frame(bytes(buf)) buf.clear() elif len(buf) > 9: buf.clear() except Exception as e: if self.running: print("[串口1 RX ERROR] {}".format(e)) time.sleep(0.1) ser.close() def stop(self): self.running = False # ========================================================== # 4. 串口2:红外遥控解析线程 # ========================================================== REMOTE_HEADER = bytes([0x01, 0x52, 0xF0, 0xFF, 0x00, 0x00, 0xFF]) REMOTE_FRAME_LEN = 11 class RemoteSignals(QObject): enter_settings = pyqtSignal() exit_settings = pyqtSignal() class RemoteWorker(threading.Thread): def __init__(self, port="/dev/ttyS3", baudrate=115200): super().__init__(daemon=True) self.port = port self.baudrate = baudrate self.running = True self.signals = RemoteSignals() def _parse_frame(self, frame: bytes): if len(frame) != REMOTE_FRAME_LEN: return if frame[:7] != REMOTE_HEADER: return user_code = frame[7] print("[遥控] 用户码: 0x{:02X}".format(user_code)) if user_code == 0x05: self.signals.enter_settings.emit() elif user_code == 0x0A: self.signals.exit_settings.emit() def run(self): try: ser = serial.Serial(port=self.port, baudrate=self.baudrate, bytesize=serial.EIGHTBITS, stopbits=serial.STOPBITS_ONE, parity=serial.PARITY_NONE, timeout=1) except Exception as e: print("[串口2 ERROR] 无法打开 {}: {}".format(self.port, e)) return buf = bytearray() while self.running: try: byte = ser.read(1) if not byte: continue b = byte[0] if len(buf) == 0: if b == 0x01: buf.append(b) continue buf.append(b) if len(buf) <= 7: if buf[len(buf) - 1] != REMOTE_HEADER[len(buf) - 1]: buf.clear() continue if len(buf) == REMOTE_FRAME_LEN: self._parse_frame(bytes(buf)) buf.clear() elif len(buf) > REMOTE_FRAME_LEN: buf.clear() except Exception as e: if self.running: print("[串口2 ERROR] {}".format(e)) time.sleep(0.1) ser.close() def stop(self): self.running = False # ========================================================== # 5. 轮播调度器 # ========================================================== class CarouselScheduler: INTERVAL = 4.0 def __init__(self): self._queue = [] self._cur_dir = None self._cur_start = 0.0 def update(self, active_dirs: list): if not active_dirs: self._queue = [] self._cur_dir = None return None, 0 for d in active_dirs: if d not in self._queue: self._queue.append(d) self._queue = [d for d in self._queue if d in active_dirs or d == self._cur_dir] if self._cur_dir is None or self._cur_dir not in self._queue: self._cur_dir = self._queue[0] if self._queue else None self._cur_start = time.time() self._skip_gone(active_dirs) if time.time() - self._cur_start >= self.INTERVAL: self._advance(active_dirs) remaining = max(0.0, self.INTERVAL - (time.time() - self._cur_start)) return self._cur_dir, remaining def _advance(self, active_dirs): if not self._queue: self._cur_dir = None return try: idx = self._queue.index(self._cur_dir) except ValueError: idx = -1 self._cur_dir = self._queue[(idx + 1) % len(self._queue)] self._cur_start = time.time() self._skip_gone(active_dirs) def _skip_gone(self, active_dirs): for _ in range(len(self._queue)): if self._cur_dir in active_dirs: break self._advance(active_dirs) # ========================================================== # 6. 原始帧采集线程 # ========================================================== class RawCaptureWorker(threading.Thread): def __init__(self, caps, names): super().__init__(daemon=True) self.caps = caps self.names = names self.running = True self.queues = {name: deque(maxlen=2) for name in names} def get_frame(self, name): q = self.queues.get(name) if q: try: return q[-1] except IndexError: pass return None def run(self): while self.running: for cap, name in zip(self.caps, self.names): ret, frame = cap.read() if ret and frame is not None: self.queues[name].append(frame) def stop(self): self.running = False # ========================================================== # 7. 鸟瞰处理线程 # ========================================================== class BirdViewWorker(threading.Thread): def __init__(self, raw_worker, camera_models, birdview, framestep=2): super().__init__(daemon=True) self.raw_worker = raw_worker self.models = camera_models self.names = raw_worker.names self.birdview = birdview self.framestep = framestep self.running = True self.frame_idx = 0 self._result_queue = deque(maxlen=2) self._lock = threading.Lock() def get_bird(self): with self._lock: try: return self._result_queue[-1] except IndexError: return None def run(self): while self.running: self.frame_idx += 1 if self.frame_idx % self.framestep != 0: time.sleep(0.005) continue frames = [] for name, model in zip(self.names, self.models): frame = self.raw_worker.get_frame(name) if frame is None: break try: proc = model.flip(model.project(model.undistort(frame))) frames.append(proc) except Exception as e: print("[BIRD PROC ERROR] {}".format(e)) break if len(frames) != 4: time.sleep(0.01) continue try: self.birdview.update_frames(frames) self.birdview.stitch_all_parts() self.birdview.make_white_balance() self.birdview.copy_car_image() bird = self.birdview.image.copy() with self._lock: self._result_queue.append(bird) except Exception as e: print("[BIRDVIEW ERROR] {}".format(e)) time.sleep(0.01) def stop(self): self.running = False # ========================================================== # 8. 多摄像头初始化 # ========================================================== class MultiCameraBirdView: def __init__(self): self.running = True self.names = settings.camera_names self.yamls = [ os.path.join(os.getcwd(), "yaml", name + ".yaml") for name in self.names ] try: self.camera_models = [ FisheyeCameraModel(f, n) for f, n in zip(self.yamls, self.names) ] except Exception as e: print("[ERROR] {}".format(e)) self.running = False return self.caps = [] self.which_cameras = {"front": 0, "left": 1, "back": 2, "right": 3} for name in self.names: cap_id = self.which_cameras.get(name, 0) cap = cv2.VideoCapture(cap_id, cv2.CAP_V4L2) cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*"YUYV")) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) if not cap.isOpened(): print("[ERROR] Cannot open {} (ID:{})".format(name, cap_id)) self.running = False return self.caps.append(cap) self.birdview = BirdView() self._initialize_weights() def _initialize_weights(self): try: images = [ os.path.join(os.getcwd(), "images", name + ".png") for name in self.names ] static_frames = [] for img_path, model in zip(images, self.camera_models): img = cv2.imread(img_path) if img is None: continue static_frames.append( model.flip(model.project(model.undistort(img)))) if len(static_frames) == 4: self.birdview.get_weights_and_masks(static_frames) print("[INFO] Weights initialized") else: print("[WARNING] Static images incomplete") except Exception as e: print("[ERROR] Weight init failed: {}".format(e)) # ========================================================== # 9. 常量 # ========================================================== DIR_CN = {"front": "前方", "left": "左方", "back": "后方", "right": "右方"} WARN_COLOR = (255, 80, 80) SAFE_COLOR = (100, 255, 100) SCREEN_W = 1024 SCREEN_H = 600 BIRD_W = SCREEN_W // 3 FRONT_W = SCREEN_W - BIRD_W # ========================================================== # 10. 渲染线程(主视图) # ========================================================== class RenderWorker(threading.Thread): TARGET_FPS = 30 FRAME_DELAY = 1.0 / TARGET_FPS def __init__(self, bird_worker, raw_worker, serial_worker, carousel, radar_thr_ref): super().__init__(daemon=True) self.bird_worker = bird_worker self.raw_worker = raw_worker self.serial_worker = serial_worker self.carousel = carousel self.radar_thr_ref = radar_thr_ref # list[int],索引0为雷达阈值dm self.running = True self._out_queue = deque(maxlen=2) self._lock = threading.Lock() self._last_bird = None for sz in (18, 20, 24, 32): FontManager.get(sz) def get_qimage(self): with self._lock: try: return self._out_queue[-1] except IndexError: return None def run(self): while self.running: t0 = time.time() bird = self.bird_worker.get_bird() if bird is not None: self._last_bird = bird if self._last_bird is None: time.sleep(0.02) continue try: active_dirs, radar_dist, is_stopped = \ self.serial_worker.get_state() display_dir, remaining = self.carousel.update(active_dirs) right_frame = None if display_dir: right_frame = self.raw_worker.get_frame(display_dir) if right_frame is None: right_frame = self.raw_worker.get_frame("front") if right_frame is None: time.sleep(0.01) continue bird_part = cv2.resize(self._last_bird, (BIRD_W, SCREEN_H), interpolation=cv2.INTER_LINEAR) front_part = cv2.resize(right_frame, (FRONT_W, SCREEN_H), interpolation=cv2.INTER_LINEAR) display = np.hstack((bird_part, front_part)) rgb = cv2.cvtColor(display, cv2.COLOR_BGR2RGB) pil_img = Image.fromarray(rgb) texts = [(10, 10, "鸟瞰视图", (0, 255, 100), 20)] if display_dir: dir_cn = DIR_CN.get(display_dir, display_dir) texts.append((BIRD_W + 10, 10, "{} 检测到人员".format(dir_cn), WARN_COLOR, 32)) if len(active_dirs) > 1: queue_cn = " → ".join(DIR_CN.get(d, d) for d in self.carousel._queue) texts.append((BIRD_W + 10, 55, "轮播顺序: {}".format(queue_cn), (255, 200, 0), 18)) texts.append((BIRD_W + 10, 82, "当前: {} 切换倒计时: {:.1f}s".format( dir_cn, remaining), (200, 200, 200), 18)) else: texts.append((BIRD_W + 10, 55, "仅 {} 有人".format(dir_cn), (200, 200, 200), 18)) else: texts.append((BIRD_W + 10, 10, "前视监控 (周围无人)", SAFE_COLOR, 24)) texts.append((BIRD_W + 10, SCREEN_H - 75, "雷达距离: {} 分米".format(radar_dist), (180, 220, 255), 20)) stop_text = "停机状态: 已停机" if is_stopped else "停机状态: 运行中" stop_color = (255, 100, 100) if is_stopped else (100, 255, 100) texts.append((BIRD_W + 10, SCREEN_H - 48, stop_text, stop_color, 20)) texts.append((BIRD_W + 10, SCREEN_H - 22, time.strftime("%Y-%m-%d %H:%M:%S"), (160, 160, 160), 18)) add_texts_to_pil(pil_img, texts) arr = np.ascontiguousarray(np.array(pil_img)) h, w, ch = arr.shape qimg = QImage(arr.tobytes(), w, h, w * ch, QImage.Format_RGB888) with self._lock: self._out_queue.append(qimg) # ── 更新串口发送内容 ── dir_map = {"front": 0, "left": 1, "back": 2, "right": 3} presence = [0x00, 0x00, 0x00, 0x00] for d in active_dirs: idx = dir_map.get(d) if idx is not None: presence[idx] = 0x01 radar_thr = self.radar_thr_ref[0] # 从共享引用读取 self.serial_worker.update_send( presence[0], presence[1], presence[2], presence[3], radar_thr) except Exception as e: print("[RENDER WORKER ERROR] {}".format(e)) elapsed = time.time() - t0 sleep_t = self.FRAME_DELAY - elapsed if sleep_t > 0: time.sleep(sleep_t) def stop(self): self.running = False # ========================================================== # 11. 设置界面 # ========================================================== class SettingsPanel(QWidget): DEFAULT_SETTINGS = { "carousel_interval": 4, "brightness": 50, "radar_unit": 0, "show_radar": 1, "radar_alarm_dist": 50, # 雷达报警阈值,单位 dm } def __init__(self, parent=None): super().__init__(parent) self._cfg = dict(self.DEFAULT_SETTINGS) self._build_ui() def _build_ui(self): self.setStyleSheet(""" QWidget { background-color: #1a1a2e; color: #e0e0e0; } QLabel { font-size: 16px; } QPushButton { background-color: #16213e; color: #e0e0e0; border: 1px solid #0f3460; border-radius: 6px; padding: 8px 20px; font-size: 15px; } QPushButton:hover { background-color: #0f3460; } QPushButton:pressed{ background-color: #533483; } QSlider::groove:horizontal { height: 6px; background: #0f3460; border-radius: 3px; } QSlider::handle:horizontal { width: 18px; height: 18px; margin: -6px 0; background: #e94560; border-radius: 9px; } QFrame#sep { background-color: #0f3460; } """) root = QVBoxLayout(self) root.setContentsMargins(40, 30, 40, 30) root.setSpacing(20) title = QLabel("⚙ 系统设置") title.setAlignment(Qt.AlignCenter) title.setFont(QFont("", 26, QFont.Bold)) title.setStyleSheet("color: #e94560; font-size: 26px;") root.addWidget(title) root.addWidget(self._make_sep()) grid = QGridLayout() grid.setHorizontalSpacing(30) grid.setVerticalSpacing(22) # 轮播间隔 grid.addWidget(self._label("轮播间隔(秒)"), 0, 0) self._sld_interval = self._slider(1, 10, self._cfg["carousel_interval"]) self._lbl_interval = self._value_label(str(self._cfg["carousel_interval"])) self._sld_interval.valueChanged.connect( lambda v: self._lbl_interval.setText(str(v))) grid.addWidget(self._sld_interval, 0, 1) grid.addWidget(self._lbl_interval, 0, 2) # 亮度 grid.addWidget(self._label("画面亮度"), 1, 0) self._sld_bright = self._slider(0, 100, self._cfg["brightness"]) self._lbl_bright = self._value_label(str(self._cfg["brightness"])) self._sld_bright.valueChanged.connect( lambda v: self._lbl_bright.setText(str(v))) grid.addWidget(self._sld_bright, 1, 1) grid.addWidget(self._lbl_bright, 1, 2) # 雷达报警阈值(dm) grid.addWidget(self._label("雷达报警阈值(dm)"), 2, 0) self._sld_radar_thr = self._slider(1, 200, self._cfg["radar_alarm_dist"]) self._lbl_radar_thr = self._value_label( str(self._cfg["radar_alarm_dist"])) self._sld_radar_thr.valueChanged.connect( lambda v: self._lbl_radar_thr.setText(str(v))) grid.addWidget(self._sld_radar_thr, 2, 1) grid.addWidget(self._lbl_radar_thr, 2, 2) # 雷达单位 grid.addWidget(self._label("雷达距离单位"), 3, 0) self._btn_unit = QPushButton( "分米" if self._cfg["radar_unit"] == 0 else "厘米") self._btn_unit.clicked.connect(self._toggle_unit) grid.addWidget(self._btn_unit, 3, 1) # 显示雷达 grid.addWidget(self._label("显示雷达距离"), 4, 0) self._btn_radar = QPushButton( "开启" if self._cfg["show_radar"] else "关闭") self._btn_radar.clicked.connect(self._toggle_radar) grid.addWidget(self._btn_radar, 4, 1) root.addLayout(grid) root.addWidget(self._make_sep()) hint = QLabel("按遥控 0A 保存并退出设置") hint.setAlignment(Qt.AlignCenter) hint.setStyleSheet("color: #888; font-size: 14px;") root.addWidget(hint) root.addStretch() @staticmethod def _label(text): lbl = QLabel(text) lbl.setAlignment(Qt.AlignRight | Qt.AlignVCenter) return lbl @staticmethod def _value_label(text): lbl = QLabel(text) lbl.setFixedWidth(60) lbl.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) lbl.setStyleSheet("color: #e94560; font-size: 18px; font-weight:bold;") return lbl @staticmethod def _slider(mn, mx, val): s = QSlider(Qt.Horizontal) s.setRange(mn, mx) s.setValue(val) s.setFixedWidth(400) return s @staticmethod def _make_sep(): f = QFrame() f.setObjectName("sep") f.setFrameShape(QFrame.HLine) f.setFixedHeight(1) return f def _toggle_unit(self): self._cfg["radar_unit"] = 1 - self._cfg["radar_unit"] self._btn_unit.setText( "分米" if self._cfg["radar_unit"] == 0 else "厘米") def _toggle_radar(self): self._cfg["show_radar"] = 1 - self._cfg["show_radar"] self._btn_radar.setText( "开启" if self._cfg["show_radar"] else "关闭") def load(self): self._sld_interval.setValue(self._cfg["carousel_interval"]) self._sld_bright.setValue(self._cfg["brightness"]) self._sld_radar_thr.setValue(self._cfg["radar_alarm_dist"]) self._btn_unit.setText( "分米" if self._cfg["radar_unit"] == 0 else "厘米") self._btn_radar.setText( "开启" if self._cfg["show_radar"] else "关闭") print("[设置] 界面已加载") def save(self): self._cfg["carousel_interval"] = self._sld_interval.value() self._cfg["brightness"] = self._sld_bright.value() self._cfg["radar_alarm_dist"] = self._sld_radar_thr.value() print("[设置] 已保存: {}".format(self._cfg)) def get_settings(self) -> dict: return dict(self._cfg) # ========================================================== # 12. PyQt 主窗口 # ========================================================== class SurroundViewWindow(QMainWindow): PAGE_MAIN = 0 PAGE_SETTINGS = 1 def __init__(self, multi_cam, serial_worker, remote_worker): super().__init__() self.multi_cam = multi_cam self.serial_worker = serial_worker self.remote_worker = remote_worker self.carousel = CarouselScheduler() # 共享雷达阈值(list包装,方便跨线程引用) self._radar_thr_ref = [50] self.setWindowTitle("环视系统 - 串口联动") self.resize(SCREEN_W, SCREEN_H) self.stack = QStackedWidget() self.setCentralWidget(self.stack) # 主视图页 main_page = QWidget() main_lay = QVBoxLayout(main_page) main_lay.setContentsMargins(0, 0, 0, 0) self.image_label = QLabel() self.image_label.setAlignment(Qt.AlignCenter) self.image_label.setStyleSheet("background-color: black;") main_lay.addWidget(self.image_label) self.stack.addWidget(main_page) # 设置页 self.settings_panel = SettingsPanel() self.stack.addWidget(self.settings_panel) self.stack.setCurrentIndex(self.PAGE_MAIN) # 工作线程 self.raw_worker = RawCaptureWorker(multi_cam.caps, multi_cam.names) self.raw_worker.start() self.bird_worker = BirdViewWorker( self.raw_worker, multi_cam.camera_models, multi_cam.birdview, framestep=2) self.bird_worker.start() self.render_worker = RenderWorker( self.bird_worker, self.raw_worker, serial_worker, self.carousel, self._radar_thr_ref) # ← 传入共享阈值引用 self.render_worker.start() self.timer = QTimer() self.timer.timeout.connect(self._flush) self.timer.start(16) remote_worker.signals.enter_settings.connect(self._on_enter_settings) remote_worker.signals.exit_settings.connect(self._on_exit_settings) def _flush(self): if self.stack.currentIndex() != self.PAGE_MAIN: return qimg = self.render_worker.get_qimage() if qimg is None: return pixmap = QPixmap.fromImage(qimg) lw = self.image_label.width() lh = self.image_label.height() if qimg.width() != lw or qimg.height() != lh: pixmap = pixmap.scaled(lw, lh, Qt.KeepAspectRatio, Qt.FastTransformation) self.image_label.setPixmap(pixmap) def _on_enter_settings(self): if self.stack.currentIndex() == self.PAGE_SETTINGS: return self.settings_panel.load() self.stack.setCurrentIndex(self.PAGE_SETTINGS) print("[UI] 已切换到设置界面") def _on_exit_settings(self): if self.stack.currentIndex() != self.PAGE_SETTINGS: return self.settings_panel.save() cfg = self.settings_panel.get_settings() CarouselScheduler.INTERVAL = float(cfg["carousel_interval"]) self._radar_thr_ref[0] = int(cfg["radar_alarm_dist"]) self.stack.setCurrentIndex(self.PAGE_MAIN) print("[UI] 已退出设置,雷达阈值: {}dm".format(self._radar_thr_ref[0])) def closeEvent(self, event): self.timer.stop() self.render_worker.stop() self.bird_worker.stop() self.raw_worker.stop() self.serial_worker.stop() self.remote_worker.stop() self.multi_cam.running = False for cap in self.multi_cam.caps: cap.release() event.accept() # ========================================================== # 13. 主函数 # ========================================================== def main(): parser = argparse.ArgumentParser(description="环视系统 + 串口联动") parser.add_argument("--mode", type=str, required=True, choices=["realtime", "static"]) parser.add_argument("--port", type=str, default="/dev/ttyS0") parser.add_argument("--remote-port", type=str, default="/dev/ttyS3") parser.add_argument("--baudrate", type=int, default=115200) parser.add_argument("--framestep", type=int, default=2) parser.add_argument("--interval", type=float, default=4.0) args = parser.parse_args() if args.mode == "realtime": CarouselScheduler.INTERVAL = args.interval serial_worker = SerialWorker(port=args.port, baudrate=args.baudrate) serial_worker.start() print("[INFO] 雷达串口已启动: {} @ {} 发送周期: {}ms".format( args.port, args.baudrate, SEND_INTERVAL_MS)) remote_worker = RemoteWorker(port=args.remote_port, baudrate=args.baudrate) remote_worker.start() print("[INFO] 遥控串口已启动: {} @ {}".format( args.remote_port, args.baudrate)) multi_cam = MultiCameraBirdView() if not multi_cam.running: print("[ERROR] 摄像头初始化失败") serial_worker.stop() remote_worker.stop() return app = QApplication(sys.argv) win = SurroundViewWindow(multi_cam, serial_worker, remote_worker) win.bird_worker.framestep = args.framestep win.show() sys.exit(app.exec_()) elif args.mode == "static": print("[INFO] Static mode not implemented.") if __name__ == "__main__": main()