add:增加倒车功能

This commit is contained in:
2026-04-01 17:27:55 +08:00
parent 6551f227be
commit fc4b56776b
4 changed files with 106 additions and 4136 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
{ {
"carousel_interval": 4, "carousel_interval": 4,
"brightness": 50, "brightness": 50,
"radar_unit": 0, "radar_unit": 1,
"show_radar": 0, "show_radar": 1,
"radar_alarm_dist": 70, "radar_alarm_dist": 5,
"display_mode": 0 "display_mode": 0
} }

View File

@@ -8,7 +8,7 @@ from tkinter import ttk, scrolledtext
# ========================================================== # ==========================================================
# 串口配置 # 串口配置
# ========================================================== # ==========================================================
SERIAL_PORT = "/dev/ttyS3" SERIAL_PORT = "/dev/ttyS0"
BAUD_RATE = 115200 BAUD_RATE = 115200
TIMEOUT = 1 TIMEOUT = 1

135
web.py
View File

@@ -224,26 +224,30 @@ class SerialWorker(threading.Thread):
def get_state(self): def get_state(self):
with self._rx_lock: with self._rx_lock:
return list(self._active_dirs), self._radar_dist, self._stopped return list(self._active_dirs), self._radar_dist, self._stopped, self._reverse #add 返回倒车状态
def _parse_rx_frame(self, frame): def _parse_rx_frame(self, frame):
if len(frame) != 9 or frame[0] != 0x55 or frame[8] != 0xAA: # print("[串口1 RX] " + " ".join("{:02X}".format(b) for b in frame))
if len(frame) != 10 or frame[0] != 0x55 or frame[9] != 0xAA:
return return
front = frame[1]; left = frame[2] front = frame[1]; left = frame[2]
back = frame[3]; right = frame[4] back = frame[3]; right = frame[4]
radar = frame[5]; stop = frame[6] radar = frame[5]; stop = frame[6]
reverse = frame[7] reverse = frame[7] # 倒车信号 00=正常 01=倒车
active = [] active = []
for name, val in [("front", front), ("left", left), for name, val in [("front", front), ("left", left),
("back", back), ("right", right)]: ("back", back), ("right", right)]:
if val != 0x00: if val != 0x00:
active.append(name) active.append(name)
with self._rx_lock: with self._rx_lock:
self._active_dirs = active self._active_dirs = active
self._radar_dist = radar self._radar_dist = radar
self._stopped = (stop != 0x00) self._stopped = (stop != 0x00)
# self._reverse = (reverse != 0x00) self._reverse = (reverse == 0x01) # add 倒车信号
def run(self): def run(self):
try: try:
@@ -288,10 +292,10 @@ class SerialWorker(threading.Thread):
buf.append(b) buf.append(b)
continue continue
buf.append(b) buf.append(b)
if len(buf) == 9: if len(buf) == 10:
self._parse_rx_frame(bytes(buf)) self._parse_rx_frame(bytes(buf))
buf.clear() buf.clear()
elif len(buf) > 9: elif len(buf) > 10:
buf.clear() buf.clear()
except Exception as e: except Exception as e:
if self.running: if self.running:
@@ -777,19 +781,81 @@ class RenderWorker(threading.Thread):
sw = self.sw sw = self.sw
sh = self.sh sh = self.sh
# 获取状态 # 获取状态 add 修改解包状态
active_dirs, radar_dist, is_stopped = \ active_dirs, radar_dist, is_stopped, is_reverse = \
self.serial_worker.get_state() self.serial_worker.get_state()
display_dir, remaining = self.carousel.update(active_dirs) display_dir, remaining = self.carousel.update(active_dirs)
# 决定显示模式 # 决定显示模式
# 当有人时active_dirs非空进入全屏轮播模式 # 当有人时active_dirs非空进入全屏轮播模式
self._fullscreen_mode = len(active_dirs) > 0 # 决定显示模式(倒车优先级最高)
if is_reverse:
display_mode = "reverse"
elif len(active_dirs) > 0:
display_mode = "fullscreen"
else:
display_mode = "normal"
# self._fullscreen_mode = (display_mode == "fullscreen")
cfg = self.cfg_ref cfg = self.cfg_ref
brightness = cfg.get("brightness", 50) brightness = cfg.get("brightness", 50)
if display_mode == "reverse":
# ==========================================
# 倒车模式:左侧鸟瞰 + 右侧后视摄像头
# ==========================================
bw = sw // 3
fw = sw - bw
back_frame = self.raw_worker.get_frame("back")
crop_ratio = self.CROP_RATIO.get("back", 0)
if back_frame is not None and crop_ratio > 0:
back_frame = self.crop_image(back_frame, crop_ratio)
bird_part = cv2.resize(self._last_bird, (bw, sh),
interpolation=cv2.INTER_LINEAR)
if back_frame is not None:
back_part = cv2.resize(back_frame, (fw, sh),
interpolation=cv2.INTER_LINEAR)
else:
back_part = np.zeros((sh, fw, 3), dtype=np.uint8)
if brightness != 50:
alpha_v = brightness / 50.0
back_part = cv2.convertScaleAbs(back_part, alpha=alpha_v, beta=0)
display = np.hstack((bird_part, back_part))
rgb = cv2.cvtColor(display, cv2.COLOR_BGR2RGB)
pil_img = Image.fromarray(rgb)
texts = []
# 倒车标题
texts.append((bw + 10, 10, "倒车模式 ◀ 后视", (255, 180, 0), 32))
# 雷达信息
if cfg.get("show_radar", 1):
unit_str = "厘米" if cfg.get("radar_unit", 0) else "分米"
dist_val = (radar_dist * 10 if cfg.get("radar_unit", 0)
else radar_dist)
alarm_d = cfg.get("radar_alarm_dist", 50)
rc = ((255, 80, 80) if dist_val <= alarm_d else (180, 220, 255))
texts.append((bw + 10, sh - 80,
f"雷达: {dist_val} {unit_str} 报警值: {alarm_d}{unit_str}",
rc, 22))
# 停机状态
stop_text = "已停机" if is_stopped else "运行中"
stop_color = ((255, 100, 100) if is_stopped else (100, 255, 100))
texts.append((bw + 10, sh - 52, f"状态: {stop_text}", stop_color, 22))
# 时间戳
texts.append((bw + 10, sh - 26,
time.strftime("%Y-%m-%d %H:%M:%S"), (160, 160, 160), 20))
if self._fullscreen_mode: elif display_mode == "fullscreen":
# ========================================== # ==========================================
# 全屏轮播模式:显示有人方向的画面 # 全屏轮播模式:显示有人方向的画面
# ========================================== # ==========================================
@@ -806,7 +872,7 @@ class RenderWorker(threading.Thread):
frame = self.raw_worker.get_frame("front") frame = self.raw_worker.get_frame("front")
if frame is not None: if frame is not None:
# 应用左右裁切 # 应用左右裁切
crop_ratio = self.CROP_RATIO.get(display_dir or active_dirs[0] if active_dirs else "front", 0) crop_ratio = self.CROP_RATIO.get(display_dir or active_dirs[0] if active_dirs else "front", 0)
if crop_ratio > 0: if crop_ratio > 0:
frame = self.crop_image(frame, crop_ratio) frame = self.crop_image(frame, crop_ratio)
@@ -887,7 +953,7 @@ class RenderWorker(threading.Thread):
time.sleep(0.01) time.sleep(0.01)
continue continue
# 对右侧画面应用裁切 # 对右侧画面应用裁切
crop_ratio = self.CROP_RATIO.get(display_dir or "front", 0) crop_ratio = self.CROP_RATIO.get(display_dir or "front", 0)
if crop_ratio > 0: if crop_ratio > 0:
right_frame = self.crop_image(right_frame, crop_ratio) right_frame = self.crop_image(right_frame, crop_ratio)
@@ -960,7 +1026,10 @@ class RenderWorker(threading.Thread):
# 记录状态用于调试 # 记录状态用于调试
if self._last_active_dirs != active_dirs: if self._last_active_dirs != active_dirs:
self._last_active_dirs = active_dirs.copy() self._last_active_dirs = active_dirs.copy()
print(f"[RENDER] 模式切换: {'全屏轮播' if self._fullscreen_mode else '正常模式'}, 检测到: {active_dirs}") mode_str = {"reverse": "倒车模式", "fullscreen": "全屏轮播",
"normal": "正常模式"}.get(display_mode, display_mode)
print(f"[RENDER] 模式切换: {mode_str}, 检测到: {active_dirs}, 倒车: {is_reverse}")
except Exception as e: except Exception as e:
print("[RENDER ERROR] {}".format(e)) print("[RENDER ERROR] {}".format(e))
@@ -1049,7 +1118,7 @@ class QuadRenderWorker(threading.Thread):
for (name, label_cn) in self.ORDER: for (name, label_cn) in self.ORDER:
f = self.raw_worker.get_frame(name) f = self.raw_worker.get_frame(name)
if f is not None: if f is not None:
# 先裁切,再拉伸 # 先裁切,再拉伸
crop_ratio = self.CROP_RATIO.get(name, 0) crop_ratio = self.CROP_RATIO.get(name, 0)
if crop_ratio > 0: if crop_ratio > 0:
f = self.crop_image(f, crop_ratio) f = self.crop_image(f, crop_ratio)
@@ -1116,8 +1185,8 @@ class QuadRenderWorker(threading.Thread):
# ========================================================== # ==========================================================
# 12. 回放面板 # 12. 回放面板
# 视频充满右侧区域:强制拉伸到 label 尺寸(不保持宽高比) # 视频充满右侧区域:强制拉伸到 label 尺寸(不保持宽高比)
# 字体放大 # 字体放大
# ========================================================== # ==========================================================
class PlaybackPanel(QWidget): class PlaybackPanel(QWidget):
def __init__(self, parent=None): def __init__(self, parent=None):
@@ -1151,7 +1220,7 @@ class PlaybackPanel(QWidget):
self._list_widget = QListWidget() self._list_widget = QListWidget()
self._list_widget.setFocusPolicy(Qt.NoFocus) self._list_widget.setFocusPolicy(Qt.NoFocus)
# 行高增大,字体加大 # 行高增大,字体加大
self._list_widget.setStyleSheet(""" self._list_widget.setStyleSheet("""
QListWidget { QListWidget {
background:#0d1117; border:1px solid #0f3460; background:#0d1117; border:1px solid #0f3460;
@@ -1199,7 +1268,7 @@ class PlaybackPanel(QWidget):
"font-size:18px; color:#e94560; font-weight:bold;") "font-size:18px; color:#e94560; font-weight:bold;")
rl.addWidget(play_title) rl.addWidget(play_title)
# label 充满右侧Expanding + setScaledContents(True) # label 充满右侧Expanding + setScaledContents(True)
# setScaledContents(True) 让 pixmap 自动随 label 尺寸拉伸 # setScaledContents(True) 让 pixmap 自动随 label 尺寸拉伸
self._play_label = QLabel("选择左侧录像后按 OK 播放") self._play_label = QLabel("选择左侧录像后按 OK 播放")
self._play_label.setAlignment(Qt.AlignCenter) self._play_label.setAlignment(Qt.AlignCenter)
@@ -1208,7 +1277,7 @@ class PlaybackPanel(QWidget):
" border:1px solid #0f3460;") " border:1px solid #0f3460;")
self._play_label.setSizePolicy( self._play_label.setSizePolicy(
QSizePolicy.Expanding, QSizePolicy.Expanding) QSizePolicy.Expanding, QSizePolicy.Expanding)
self._play_label.setScaledContents(True) # 自动拉伸填满 self._play_label.setScaledContents(True) # 自动拉伸填满
rl.addWidget(self._play_label, stretch=1) rl.addWidget(self._play_label, stretch=1)
self._status_label = QLabel("") self._status_label = QLabel("")
@@ -1273,7 +1342,7 @@ class PlaybackPanel(QWidget):
self._play_fps = self._play_cap.get(cv2.CAP_PROP_FPS) or RECORD_FPS self._play_fps = self._play_cap.get(cv2.CAP_PROP_FPS) or RECORD_FPS
self._play_paused = False self._play_paused = False
# 开始播放时启用 ScaledContents 填满 label # 开始播放时启用 ScaledContents 填满 label
self._play_label.setScaledContents(True) self._play_label.setScaledContents(True)
self._play_label.setText("") self._play_label.setText("")
@@ -1293,7 +1362,7 @@ class PlaybackPanel(QWidget):
self._status_label.setText("✅ 播放完毕") self._status_label.setText("✅ 播放完毕")
return return
# 直接设置 pixmaplabel.setScaledContents(True) 自动填满 # 直接设置 pixmaplabel.setScaledContents(True) 自动填满
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
h, w = rgb.shape[:2] h, w = rgb.shape[:2]
qimg = QImage(rgb.tobytes(), w, h, w * 3, QImage.Format_RGB888) qimg = QImage(rgb.tobytes(), w, h, w * 3, QImage.Format_RGB888)
@@ -1339,7 +1408,7 @@ class PlaybackPanel(QWidget):
# ========================================================== # ==========================================================
# 13. 设置面板 # 13. 设置面板
# 字体全面放大(列表 20px、标题 26px、值标签 36px # 字体全面放大(列表 20px、标题 26px、值标签 36px
# ========================================================== # ==========================================================
class SettingsPanel(QWidget): class SettingsPanel(QWidget):
SUB_MAIN = 0 SUB_MAIN = 0
@@ -1363,7 +1432,7 @@ class SettingsPanel(QWidget):
{"name":"画面亮度", "type":"range", {"name":"画面亮度", "type":"range",
"key":"brightness", "min":0, "max":100, "step":5}, "key":"brightness", "min":0, "max":100, "step":5},
{"name":"雷达报警距离", "type":"range", {"name":"雷达报警距离", "type":"range",
"key":"radar_alarm_dist", "min":10, "max":200, "step":10}, "key":"radar_alarm_dist", "min":5, "max":50, "step":5},
{"name":"雷达距离单位", "type":"toggle", {"name":"雷达距离单位", "type":"toggle",
"key":"radar_unit", "key":"radar_unit",
"values":[0, 1], "labels":["分米", "厘米"]}, "values":[0, 1], "labels":["分米", "厘米"]},
@@ -1375,7 +1444,7 @@ class SettingsPanel(QWidget):
] ]
def _build_ui(self): def _build_ui(self):
# 全面放大字号 # 全面放大字号
self.setStyleSheet(""" self.setStyleSheet("""
QWidget { QWidget {
background:#1a1a2e; background:#1a1a2e;
@@ -1427,7 +1496,7 @@ class SettingsPanel(QWidget):
self._list_widget.setFocusPolicy(Qt.NoFocus) self._list_widget.setFocusPolicy(Qt.NoFocus)
ml.addWidget(self._list_widget, stretch=1) ml.addWidget(self._list_widget, stretch=1)
# 当前值标签放大 # 当前值标签放大
self._val_label = QLabel("") self._val_label = QLabel("")
self._val_label.setAlignment(Qt.AlignCenter) self._val_label.setAlignment(Qt.AlignCenter)
self._val_label.setFixedHeight(52) self._val_label.setFixedHeight(52)
@@ -1546,7 +1615,7 @@ class SettingsPanel(QWidget):
# ========================================================== # ==========================================================
# 14. 主窗口 # 14. 主窗口
# 真全屏showFullScreen() + image_label 用 setScaledContents(True) # 真全屏showFullScreen() + image_label 用 setScaledContents(True)
# 渲染线程按真实屏幕尺寸生成图像label 自动填满无黑边 # 渲染线程按真实屏幕尺寸生成图像label 自动填满无黑边
# ========================================================== # ==========================================================
class SurroundViewWindow(QMainWindow): class SurroundViewWindow(QMainWindow):
@@ -1562,7 +1631,7 @@ class SurroundViewWindow(QMainWindow):
self.carousel = CarouselScheduler() self.carousel = CarouselScheduler()
self.setWindowTitle("环视系统") self.setWindowTitle("环视系统")
# 先全屏,再取实际尺寸传给渲染线程 # 先全屏,再取实际尺寸传给渲染线程
self.showFullScreen() self.showFullScreen()
self.sw = 1024 self.sw = 1024
self.sh = 600 self.sh = 600
@@ -1578,13 +1647,13 @@ class SurroundViewWindow(QMainWindow):
ml.setContentsMargins(0, 0, 0, 0) ml.setContentsMargins(0, 0, 0, 0)
ml.setSpacing(0) ml.setSpacing(0)
# setScaledContents(True) 让 pixmap 自动充满 label无黑边 # setScaledContents(True) 让 pixmap 自动充满 label无黑边
self.image_label = QLabel() self.image_label = QLabel()
self.image_label.setAlignment(Qt.AlignCenter) self.image_label.setAlignment(Qt.AlignCenter)
self.image_label.setStyleSheet("background:black;") self.image_label.setStyleSheet("background:black;")
self.image_label.setSizePolicy( self.image_label.setSizePolicy(
QSizePolicy.Expanding, QSizePolicy.Expanding) QSizePolicy.Expanding, QSizePolicy.Expanding)
self.image_label.setScaledContents(True) # 关键 self.image_label.setScaledContents(True) # 关键
ml.addWidget(self.image_label) ml.addWidget(self.image_label)
self.stack.addWidget(main_page) self.stack.addWidget(main_page)
@@ -1602,7 +1671,7 @@ class SurroundViewWindow(QMainWindow):
multi_cam.birdview, framestep=2) multi_cam.birdview, framestep=2)
self.bird_worker.start() self.bird_worker.start()
# 渲染线程用实际屏幕尺寸 # 渲染线程用实际屏幕尺寸
self.render_360 = RenderWorker( self.render_360 = RenderWorker(
self.bird_worker, self.raw_worker, self.bird_worker, self.raw_worker,
serial_worker, self.carousel, cfg, serial_worker, self.carousel, cfg,
@@ -1642,7 +1711,7 @@ class SurroundViewWindow(QMainWindow):
else self.render_360.get_qimage()) else self.render_360.get_qimage())
if qimg is None: if qimg is None:
return return
# 直接 setPixmapsetScaledContents(True) 自动填满,无黑边 # 直接 setPixmapsetScaledContents(True) 自动填满,无黑边
self.image_label.setPixmap(QPixmap.fromImage(qimg)) self.image_label.setPixmap(QPixmap.fromImage(qimg))
def _on_key(self, code): def _on_key(self, code):
@@ -1719,7 +1788,7 @@ def main():
app = QApplication(sys.argv) app = QApplication(sys.argv)
# 禁用系统缩放干扰,确保全屏像素对齐 # 禁用系统缩放干扰,确保全屏像素对齐
app.setAttribute(Qt.AA_DisableHighDpiScaling, True) app.setAttribute(Qt.AA_DisableHighDpiScaling, True)
win = SurroundViewWindow( win = SurroundViewWindow(