add:增加倒车功能
This commit is contained in:
135
web.py
135
web.py
@@ -224,26 +224,30 @@ class SerialWorker(threading.Thread):
|
||||
|
||||
def get_state(self):
|
||||
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):
|
||||
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
|
||||
front = frame[1]; left = frame[2]
|
||||
back = frame[3]; right = frame[4]
|
||||
radar = frame[5]; stop = frame[6]
|
||||
reverse = frame[7]
|
||||
reverse = frame[7] # 倒车信号 00=正常 01=倒车
|
||||
|
||||
active = []
|
||||
for name, val in [("front", front), ("left", left),
|
||||
("back", back), ("right", right)]:
|
||||
("back", back), ("right", right)]:
|
||||
if val != 0x00:
|
||||
active.append(name)
|
||||
|
||||
with self._rx_lock:
|
||||
self._active_dirs = active
|
||||
self._radar_dist = radar
|
||||
self._stopped = (stop != 0x00)
|
||||
# self._reverse = (reverse != 0x00)
|
||||
self._reverse = (reverse == 0x01) # add 倒车信号
|
||||
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
@@ -288,10 +292,10 @@ class SerialWorker(threading.Thread):
|
||||
buf.append(b)
|
||||
continue
|
||||
buf.append(b)
|
||||
if len(buf) == 9:
|
||||
if len(buf) == 10:
|
||||
self._parse_rx_frame(bytes(buf))
|
||||
buf.clear()
|
||||
elif len(buf) > 9:
|
||||
elif len(buf) > 10:
|
||||
buf.clear()
|
||||
except Exception as e:
|
||||
if self.running:
|
||||
@@ -777,19 +781,81 @@ class RenderWorker(threading.Thread):
|
||||
sw = self.sw
|
||||
sh = self.sh
|
||||
|
||||
# 获取状态
|
||||
active_dirs, radar_dist, is_stopped = \
|
||||
# 获取状态 add 修改解包状态
|
||||
active_dirs, radar_dist, is_stopped, is_reverse = \
|
||||
self.serial_worker.get_state()
|
||||
display_dir, remaining = self.carousel.update(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
|
||||
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")
|
||||
|
||||
if frame is not None:
|
||||
# ★ 应用左右裁切
|
||||
# 应用左右裁切
|
||||
crop_ratio = self.CROP_RATIO.get(display_dir or active_dirs[0] if active_dirs else "front", 0)
|
||||
if crop_ratio > 0:
|
||||
frame = self.crop_image(frame, crop_ratio)
|
||||
@@ -887,7 +953,7 @@ class RenderWorker(threading.Thread):
|
||||
time.sleep(0.01)
|
||||
continue
|
||||
|
||||
# ★ 对右侧画面应用裁切
|
||||
# 对右侧画面应用裁切
|
||||
crop_ratio = self.CROP_RATIO.get(display_dir or "front", 0)
|
||||
if crop_ratio > 0:
|
||||
right_frame = self.crop_image(right_frame, crop_ratio)
|
||||
@@ -960,7 +1026,10 @@ class RenderWorker(threading.Thread):
|
||||
# 记录状态用于调试
|
||||
if self._last_active_dirs != active_dirs:
|
||||
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:
|
||||
print("[RENDER ERROR] {}".format(e))
|
||||
@@ -1049,7 +1118,7 @@ class QuadRenderWorker(threading.Thread):
|
||||
for (name, label_cn) in self.ORDER:
|
||||
f = self.raw_worker.get_frame(name)
|
||||
if f is not None:
|
||||
# ★ 先裁切,再拉伸
|
||||
# 先裁切,再拉伸
|
||||
crop_ratio = self.CROP_RATIO.get(name, 0)
|
||||
if crop_ratio > 0:
|
||||
f = self.crop_image(f, crop_ratio)
|
||||
@@ -1116,8 +1185,8 @@ class QuadRenderWorker(threading.Thread):
|
||||
|
||||
# ==========================================================
|
||||
# 12. 回放面板
|
||||
# ★ 视频充满右侧区域:强制拉伸到 label 尺寸(不保持宽高比)
|
||||
# ★ 字体放大
|
||||
# 视频充满右侧区域:强制拉伸到 label 尺寸(不保持宽高比)
|
||||
# 字体放大
|
||||
# ==========================================================
|
||||
class PlaybackPanel(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
@@ -1151,7 +1220,7 @@ class PlaybackPanel(QWidget):
|
||||
|
||||
self._list_widget = QListWidget()
|
||||
self._list_widget.setFocusPolicy(Qt.NoFocus)
|
||||
# ★ 行高增大,字体加大
|
||||
# 行高增大,字体加大
|
||||
self._list_widget.setStyleSheet("""
|
||||
QListWidget {
|
||||
background:#0d1117; border:1px solid #0f3460;
|
||||
@@ -1199,7 +1268,7 @@ class PlaybackPanel(QWidget):
|
||||
"font-size:18px; color:#e94560; font-weight:bold;")
|
||||
rl.addWidget(play_title)
|
||||
|
||||
# ★ label 充满右侧:Expanding + setScaledContents(True)
|
||||
# label 充满右侧:Expanding + setScaledContents(True)
|
||||
# setScaledContents(True) 让 pixmap 自动随 label 尺寸拉伸
|
||||
self._play_label = QLabel("选择左侧录像后按 OK 播放")
|
||||
self._play_label.setAlignment(Qt.AlignCenter)
|
||||
@@ -1208,7 +1277,7 @@ class PlaybackPanel(QWidget):
|
||||
" border:1px solid #0f3460;")
|
||||
self._play_label.setSizePolicy(
|
||||
QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self._play_label.setScaledContents(True) # ★ 自动拉伸填满
|
||||
self._play_label.setScaledContents(True) # 自动拉伸填满
|
||||
rl.addWidget(self._play_label, stretch=1)
|
||||
|
||||
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_paused = False
|
||||
|
||||
# ★ 开始播放时启用 ScaledContents 填满 label
|
||||
# 开始播放时启用 ScaledContents 填满 label
|
||||
self._play_label.setScaledContents(True)
|
||||
self._play_label.setText("")
|
||||
|
||||
@@ -1293,7 +1362,7 @@ class PlaybackPanel(QWidget):
|
||||
self._status_label.setText("✅ 播放完毕")
|
||||
return
|
||||
|
||||
# ★ 直接设置 pixmap,label.setScaledContents(True) 自动填满
|
||||
# 直接设置 pixmap,label.setScaledContents(True) 自动填满
|
||||
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
h, w = rgb.shape[:2]
|
||||
qimg = QImage(rgb.tobytes(), w, h, w * 3, QImage.Format_RGB888)
|
||||
@@ -1339,7 +1408,7 @@ class PlaybackPanel(QWidget):
|
||||
|
||||
# ==========================================================
|
||||
# 13. 设置面板
|
||||
# ★ 字体全面放大(列表 20px、标题 26px、值标签 36px)
|
||||
# 字体全面放大(列表 20px、标题 26px、值标签 36px)
|
||||
# ==========================================================
|
||||
class SettingsPanel(QWidget):
|
||||
SUB_MAIN = 0
|
||||
@@ -1363,7 +1432,7 @@ class SettingsPanel(QWidget):
|
||||
{"name":"画面亮度", "type":"range",
|
||||
"key":"brightness", "min":0, "max":100, "step":5},
|
||||
{"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",
|
||||
"key":"radar_unit",
|
||||
"values":[0, 1], "labels":["分米", "厘米"]},
|
||||
@@ -1375,7 +1444,7 @@ class SettingsPanel(QWidget):
|
||||
]
|
||||
|
||||
def _build_ui(self):
|
||||
# ★ 全面放大字号
|
||||
# 全面放大字号
|
||||
self.setStyleSheet("""
|
||||
QWidget {
|
||||
background:#1a1a2e;
|
||||
@@ -1427,7 +1496,7 @@ class SettingsPanel(QWidget):
|
||||
self._list_widget.setFocusPolicy(Qt.NoFocus)
|
||||
ml.addWidget(self._list_widget, stretch=1)
|
||||
|
||||
# ★ 当前值标签放大
|
||||
# 当前值标签放大
|
||||
self._val_label = QLabel("")
|
||||
self._val_label.setAlignment(Qt.AlignCenter)
|
||||
self._val_label.setFixedHeight(52)
|
||||
@@ -1546,7 +1615,7 @@ class SettingsPanel(QWidget):
|
||||
|
||||
# ==========================================================
|
||||
# 14. 主窗口
|
||||
# ★ 真全屏:showFullScreen() + image_label 用 setScaledContents(True)
|
||||
# 真全屏:showFullScreen() + image_label 用 setScaledContents(True)
|
||||
# 渲染线程按真实屏幕尺寸生成图像,label 自动填满无黑边
|
||||
# ==========================================================
|
||||
class SurroundViewWindow(QMainWindow):
|
||||
@@ -1562,7 +1631,7 @@ class SurroundViewWindow(QMainWindow):
|
||||
self.carousel = CarouselScheduler()
|
||||
|
||||
self.setWindowTitle("环视系统")
|
||||
# ★ 先全屏,再取实际尺寸传给渲染线程
|
||||
# 先全屏,再取实际尺寸传给渲染线程
|
||||
self.showFullScreen()
|
||||
self.sw = 1024
|
||||
self.sh = 600
|
||||
@@ -1578,13 +1647,13 @@ class SurroundViewWindow(QMainWindow):
|
||||
ml.setContentsMargins(0, 0, 0, 0)
|
||||
ml.setSpacing(0)
|
||||
|
||||
# ★ setScaledContents(True) 让 pixmap 自动充满 label,无黑边
|
||||
# setScaledContents(True) 让 pixmap 自动充满 label,无黑边
|
||||
self.image_label = QLabel()
|
||||
self.image_label.setAlignment(Qt.AlignCenter)
|
||||
self.image_label.setStyleSheet("background:black;")
|
||||
self.image_label.setSizePolicy(
|
||||
QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self.image_label.setScaledContents(True) # ★ 关键
|
||||
self.image_label.setScaledContents(True) # 关键
|
||||
ml.addWidget(self.image_label)
|
||||
self.stack.addWidget(main_page)
|
||||
|
||||
@@ -1602,7 +1671,7 @@ class SurroundViewWindow(QMainWindow):
|
||||
multi_cam.birdview, framestep=2)
|
||||
self.bird_worker.start()
|
||||
|
||||
# ★ 渲染线程用实际屏幕尺寸
|
||||
# 渲染线程用实际屏幕尺寸
|
||||
self.render_360 = RenderWorker(
|
||||
self.bird_worker, self.raw_worker,
|
||||
serial_worker, self.carousel, cfg,
|
||||
@@ -1642,7 +1711,7 @@ class SurroundViewWindow(QMainWindow):
|
||||
else self.render_360.get_qimage())
|
||||
if qimg is None:
|
||||
return
|
||||
# ★ 直接 setPixmap,setScaledContents(True) 自动填满,无黑边
|
||||
# 直接 setPixmap,setScaledContents(True) 自动填满,无黑边
|
||||
self.image_label.setPixmap(QPixmap.fromImage(qimg))
|
||||
|
||||
def _on_key(self, code):
|
||||
@@ -1719,7 +1788,7 @@ def main():
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
# ★ 禁用系统缩放干扰,确保全屏像素对齐
|
||||
# 禁用系统缩放干扰,确保全屏像素对齐
|
||||
app.setAttribute(Qt.AA_DisableHighDpiScaling, True)
|
||||
|
||||
win = SurroundViewWindow(
|
||||
|
||||
Reference in New Issue
Block a user