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:雷达/人体感应解析线程 # ========================================================== 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.lock = threading.Lock() self._radar_dist = 0 self._stopped = False self._active_dirs = [] def get_state(self): with self.lock: return list(self._active_dirs), self._radar_dist, self._stopped def _parse_frame(self, frame: bytes): if len(frame) != 9 or frame[0] != 0x55 or frame[8] != 0xAA: return 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) with self.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=1) except Exception as e: print("[串口1 ERROR] 无法打开: {}".format(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 == 0x55: buf.append(b) continue buf.append(b) if len(buf) == 9: self._parse_frame(bytes(buf)) buf.clear() elif len(buf) > 9: buf.clear() except Exception as e: if self.running: print("[串口1 ERROR] {}".format(e)) time.sleep(0.1) ser.close() def stop(self): self.running = False # ========================================================== # 4. 串口2:红外遥控解析线程 # 帧格式: 01 52 F0 FF 00 00 FF [用户码] [?] [?] [?] # 帧头固定7字节: 01 52 F0 FF 00 00 FF # 用户码在第8字节(index=7) # ========================================================== REMOTE_HEADER = bytes([0x01, 0x52, 0xF0, 0xFF, 0x00, 0x00, 0xFF]) REMOTE_FRAME_LEN = 11 # 根据示例 01 52 F0 FF 00 00 FF 02 FD 97 54 共11字节 class RemoteSignals(QObject): """用 Qt 信号跨线程通知主线程""" enter_settings = pyqtSignal() # 按下 0x05 → 进入设置 exit_settings = pyqtSignal() # 按下 0x0A → 退出并保存 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] # 帧头同步:等待第一个字节 0x01 if len(buf) == 0: if b == 0x01: buf.append(b) continue buf.append(b) # 帧头前7字节逐字节校验,失配则重置 if len(buf) <= 7: if buf[len(buf) - 1] != REMOTE_HEADER[len(buf) - 1]: print("[遥控同步] 帧头失配,重置") 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): super().__init__(daemon=True) self.bird_worker = bird_worker self.raw_worker = raw_worker self.serial_worker = serial_worker self.carousel = carousel self.running = True self._out_queue = deque(maxlen=2) self._lock = threading.Lock() self._last_bird = None self._last_time = "" 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) self._last_time = time.strftime("%Y-%m-%d %H:%M:%S") 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, self._last_time, (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) 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): """ 全屏设置界面。 外部只需调用: panel.load() - 进入时刷新显示 panel.save() - 退出时保存(内部也会自动调用) panel.get_settings() -> dict """ # 默认配置,可按需扩展 DEFAULT_SETTINGS = { "carousel_interval": 4, # 轮播间隔(秒) "brightness": 50, # 亮度 0~100 "radar_unit": 0, # 0=分米 1=厘米 "show_radar": 1, # 是否显示雷达距离 } def __init__(self, parent=None): super().__init__(parent) self._cfg = dict(self.DEFAULT_SETTINGS) self._build_ui() # ---------------------------------------------------------- # 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) sep = self._make_sep() root.addWidget(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) # --- 雷达单位切换 --- grid.addWidget(self._label("雷达距离单位"), 2, 0) self._btn_unit = QPushButton( "分米" if self._cfg["radar_unit"] == 0 else "厘米") self._btn_unit.setCheckable(False) self._btn_unit.clicked.connect(self._toggle_unit) grid.addWidget(self._btn_unit, 2, 1) # --- 是否显示雷达 --- grid.addWidget(self._label("显示雷达距离"), 3, 0) self._btn_radar = QPushButton( "开启" if self._cfg["show_radar"] else "关闭") self._btn_radar.clicked.connect(self._toggle_radar) grid.addWidget(self._btn_radar, 3, 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(50) 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._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() # radar_unit / show_radar 已在点击时即时更新 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() self.setWindowTitle("环视系统 - 串口联动") self.resize(SCREEN_W, SCREEN_H) # QStackedWidget:页面0=主视图 页面1=设置 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) # index 0 # --- 设置页 --- self.settings_panel = SettingsPanel() self.stack.addWidget(self.settings_panel) # index 1 # 默认显示主视图 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.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, lh = self.image_label.width(), self.image_label.height() w, h = qimg.width(), qimg.height() if w != lw or h != 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.stack.setCurrentIndex(self.PAGE_MAIN) print("[UI] 已退出设置,返回主视图") # ---------------------------------------------------------- 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", help="雷达串口") parser.add_argument("--remote-port",type=str, default="/dev/ttyS3", help="遥控串口") 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, help="轮播间隔秒数") 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] 雷达串口已启动: {} @ {}".format(args.port, args.baudrate)) # 遥控串口 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()