fix:精简工程

This commit is contained in:
2026-04-01 15:03:20 +08:00
parent a7a67296e3
commit 6551f227be
37 changed files with 4063 additions and 586172 deletions

199
1.py
View File

@@ -1,199 +0,0 @@
import cv2
import threading
import sys
import os
import argparse
import numpy as np
from surround_view import FisheyeCameraModel, BirdView
import surround_view.param_settings as settings
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]
self.camera_models = [FisheyeCameraModel(camera_file, camera_name) for camera_file, camera_name in zip(self.yamls, self.names)]
# 初始化摄像头
self.caps = []
self.which_cameras = {
"front": 0,
"left": 1,
"back": 2,
"right":3
}
# 初始化摄像头捕获
for name in self.names:
cap = cv2.VideoCapture(self.which_cameras[name], 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)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 3)
if not cap.isOpened():
print(f"[ERROR] Cannot open {name} camera", file=sys.stderr)
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 image_file, camera_model in zip(images, self.camera_models):
img = cv2.imread(image_file)
if img is None:
print(f"[ERROR] Cannot load static image: {image_file}")
continue
img = camera_model.undistort(img)
img = camera_model.project(img)
img = camera_model.flip(img)
static_frames.append(img)
if len(static_frames) == 4:
# 初始化权重和掩码矩阵
Gmat, Mmat = self.birdview.get_weights_and_masks(static_frames)
print("[INFO] Weights and masks initialized successfully")
else:
print("[WARNING] Could not load all static images for weight initialization")
except Exception as e:
print(f"[ERROR] Failed to initialize weights: {e}", file=sys.stderr)
def process_frame(self, img, camera_model):
"""处理单帧图像:去畸变 -> 投影 -> 翻转"""
img = camera_model.undistort(img)
img = camera_model.project(img)
img = camera_model.flip(img)
return img
def run(self):
# 获取屏幕分辨率(假设为 1920x1080如果不同可以手动指定
# 在 Linux 下全屏显示通常需要先创建一个窗口
# cv2.namedWindow("SurroundView_System", cv2.WND_PROP_FULLSCREEN)
# cv2.setWindowProperty("SurroundView_System", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
cv2.namedWindow("SurroundView_System", cv2.WINDOW_NORMAL)
# 2. 指定窗口大小 (宽度, 高度)
cv2.resizeWindow("SurroundView_System", 1024, 600)
SCREEN_W = 1024
SCREEN_H = 600
# 计算布局比例
# 鸟瞰图占 1/3 宽度: 1920 / 3 = 640
# 原始画面占 2/3 宽度: 1920 * 2 / 3 = 1280
BIRD_W = SCREEN_W // 3
FRONT_W = SCREEN_W - BIRD_W
while self.running:
frames = []
raw_front_frame = None
for i, (cap, camera_model) in enumerate(zip(self.caps, self.camera_models)):
ret, frame = cap.read()
if not ret or frame is None:
continue
# 保存一份原始的前向画面用于监视 (假设 front 是第一个摄像头)
if self.names[i] == "front":
raw_front_frame = frame.copy()
processed_frame = self.process_frame(frame, camera_model)
frames.append(processed_frame)
if len(frames) == 4 and raw_front_frame is not None:
try:
# 1. 处理鸟瞰图
self.birdview.update_frames(frames)
self.birdview.stitch_all_parts()
self.birdview.make_white_balance()
self.birdview.copy_car_image()
# 2. 缩放鸟瞰图 (保持比例,填满左侧 1/3)
# 假设原始 birdview.image 比较长,我们将其 resize 到 BIRD_W x SCREEN_H
bird_part = cv2.resize(self.birdview.image, (BIRD_W, SCREEN_H))
# 3. 处理前向原始画面 (缩放至右侧 2/3)
# raw_front_frame 是 1280x720, 缩放到 FRONT_W x SCREEN_H
front_part = cv2.resize(raw_front_frame, (FRONT_W, SCREEN_H))
# 4. 水平拼接 (左鸟瞰 + 右前视)
display_all = np.hstack((bird_part, front_part))
# 5. 全屏显示
cv2.imshow("SurroundView_System", display_all)
if cv2.waitKey(1) & 0xFF == ord('q'):
self.running = False
break
except Exception as e:
print(f"[ERROR] Processing error: {e}")
continue
for cap in self.caps:
cap.release()
cv2.destroyAllWindows()
def main():
parser = argparse.ArgumentParser(description="Real-time 4-Camera Bird's Eye View")
parser.add_argument("--mode", type=str, required=True,
choices=["realtime", "static"],
help="Mode: 'realtime' for live video, 'static' for static images")
args = parser.parse_args()
if args.mode == "static":
# 原有的静态图像处理代码
names = settings.camera_names
images = [os.path.join(os.getcwd(), "images", name + ".png") for name in names]
yamls = [os.path.join(os.getcwd(), "yaml", name + ".yaml") for name in names]
camera_models = [FisheyeCameraModel(camera_file, camera_name) for camera_file, camera_name in zip(yamls, names)]
projected = []
for image_file, camera in zip(images, camera_models):
img = cv2.imread(image_file)
img = camera.undistort(img)
img = camera.project(img)
img = camera.flip(img)
projected.append(img)
birdview = BirdView()
Gmat, Mmat = birdview.get_weights_and_masks(projected) # 初始化权重矩阵
birdview.update_frames(projected)
# birdview.get_weights_and_masks.stitch_all_parts()
# birdview.make_white_balance()
birdview.copy_car_image()
img_small = cv2.resize(birdview.image, (600, 1024))
cv2.imshow("BirdView Result", img_small)
while True:
key = cv2.waitKey(1) & 0xFF
if key == ord("q"):
cv2.destroyAllWindows()
return -1
elif args.mode == "realtime":
# 实时视频处理
print("Starting real-time 4-camera bird's eye view...")
print("Press 'q' to quit")
multi_cam = MultiCameraBirdView()
if multi_cam.running:
multi_cam.run()
else:
print("[ERROR] Failed to initialize cameras")
if __name__ == "__main__":
main()

474
2.py
View File

@@ -1,474 +0,0 @@
import cv2
import threading
import sys, os, time
import argparse
import numpy as np
import serial
import json
import glob
import shutil
from collections import deque
from datetime import datetime
from PyQt5.QtWidgets import (QApplication, QLabel, QWidget, QVBoxLayout,
QHBoxLayout, QMainWindow, QStackedWidget,
QFrame, QListWidget, QListWidgetItem,
QSizePolicy, QProgressBar, QGridLayout)
from PyQt5.QtGui import QImage, QPixmap, QFont
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QObject, QSize
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)
# ==========================================================
# 遥控键码
# ==========================================================
KEY_UP = 0x01
KEY_DOWN = 0x09
KEY_LEFT = 0x04
KEY_RIGHT = 0x06
KEY_BACK = 0x02
KEY_OK = 0x05
# ==========================================================
# 参数
# ==========================================================
VIDEO_DIR = "/videos"
SEGMENT_SECS = 180
RECORD_FPS = 10
CELL_W, CELL_H = 640, 360
GRID_W, GRID_H = 1280, 720
DISK_WARN_PCT = 90
SEND_INTERVAL_MS = 100
MODE_360 = 0
MODE_QUAD = 1
DIR_CN = {"front":"前方","left":"左方","back":"后方","right":"右方"}
WARN_COLOR = (255, 80, 80)
SAFE_COLOR = (100, 255, 100)
# ==========================================================
# 配置
# ==========================================================
CONFIG_PATH = os.path.join(os.getcwd(), "surround_config.json")
DEFAULT_CONFIG = {
"carousel_interval": 4,
"brightness": 50,
"radar_unit": 0,
"show_radar": 1,
"radar_alarm_dist": 50,
"display_mode": MODE_360,
}
def load_config():
if os.path.exists(CONFIG_PATH):
try:
with open(CONFIG_PATH, "r") as f:
cfg = json.load(f)
for k, v in DEFAULT_CONFIG.items():
cfg.setdefault(k, v)
return cfg
except: pass
return dict(DEFAULT_CONFIG)
def save_config(cfg):
try:
with open(CONFIG_PATH, "w") as f:
json.dump(cfg, f, indent=2, ensure_ascii=False)
except: pass
# ==========================================================
# 工具函数
# ==========================================================
def get_disk_info_str(path):
try:
usage = shutil.disk_usage(path)
pct = usage.used * 100 // usage.total
return "存储: {:.1f}G/{:.1f}G ({}%)".format(usage.used/1024**3, usage.total/1024**3, pct), pct
except: return "磁盘未知", 0
class FontManager:
_fonts = {}
@classmethod
def get(cls, size=20):
if size not in cls._fonts:
p = "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc"
if not os.path.exists(p): p = None
cls._fonts[size] = ImageFont.truetype(p, size) if p else ImageFont.load_default()
return cls._fonts[size]
def add_texts_to_pil(pil_image, text_list):
draw = ImageDraw.Draw(pil_image)
for item in text_list:
x, y, text, color, size = item
font = FontManager.get(size)
draw.text((x+1, y+1), text, font=font, fill=(0,0,0))
draw.text((x, y), text, font=font, fill=color)
# ==========================================================
# 串口通信 (TTYS0)
# ==========================================================
class SerialWorker(threading.Thread):
def __init__(self, port="/dev/ttyS0", baudrate=115200):
super().__init__(daemon=True)
self.port, self.baudrate = port, baudrate
self.running = True
self._rx_lock = threading.Lock()
self._radar_dist, self._stopped, self._active_dirs = 0, False, []
self._tx_lock = threading.Lock()
self._tx_data = [0,0,0,0, 50] # F,L,B,R, Thr
self._rx_enabled = True
def set_rx_enabled(self, e): self._rx_enabled = e
def update_send(self, f, l, b, r, thr):
with self._tx_lock: self._tx_data = [f,l,b,r, thr]
def run(self):
try:
ser = serial.Serial(self.port, self.baudrate, timeout=0.05)
except: return
def _send_loop():
while self.running:
with self._tx_lock:
d = self._tx_data
chk = sum(d[:4] + [0, d[4]]) & 0xFF
frame = bytes([0x55, d[0], d[1], d[2], d[3], 0x00, d[4], chk, 0xAA])
ser.write(frame)
time.sleep(SEND_INTERVAL_MS/1000.0)
threading.Thread(target=_send_loop, daemon=True).start()
buf = bytearray()
while self.running:
if not self._rx_enabled:
time.sleep(0.1); continue
b = ser.read(1)
if not b: continue
buf.append(b[0])
if len(buf) == 9:
if buf[0] == 0x55 and buf[8] == 0xAA:
with self._rx_lock:
self._active_dirs = [n for i,n in enumerate(["front","left","back","right"]) if buf[i+1]]
self._radar_dist, self._stopped = buf[5], buf[6] > 0
buf.clear()
elif len(buf) > 9: buf.clear()
# ==========================================================
# 渲染线程 (消除黑边关键)
# ==========================================================
class RenderWorker(threading.Thread):
def __init__(self, bird_worker, raw_worker, serial_worker, carousel, cfg, sw, sh):
super().__init__(daemon=True)
self.bird_w, self.raw_w, self.ser_w = bird_worker, raw_worker, serial_worker
self.carousel, self.cfg = carousel, cfg
self.sw, self.sh = sw, sh
self.running = True
self._out_q = deque(maxlen=2)
def get_qimage(self):
try: return self._out_q[-1]
except: return None
def run(self):
while self.running:
bird = self.bird_w.get_bird()
if bird is None: time.sleep(0.02); continue
active, dist, stopped = self.ser_w.get_state()
cur_dir, rem = self.carousel.update(active)
# 分配宽度
bw = self.sw // 3
fw = self.sw - bw
# 强制拉伸(消除黑边)
bird_part = cv2.resize(bird, (bw, self.sh))
raw_f = self.raw_w.get_frame(cur_dir if cur_dir else "front")
if raw_f is None: raw_f = np.zeros((self.sh, fw, 3), np.uint8)
front_part = cv2.resize(raw_f, (fw, self.sh))
# 亮度
alpha = self.cfg.get("brightness", 50) / 50.0
if alpha != 1.0: front_part = cv2.convertScaleAbs(front_part, alpha=alpha)
canvas = np.hstack((bird_part, front_part))
pil_img = Image.fromarray(cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB))
# 文字叠加 (字号加大)
texts = [(10, 10, "鸟瞰视图", (0,255,100), 25)]
if cur_dir:
texts.append((bw+20, 20, "{} 有人".format(DIR_CN[cur_dir]), WARN_COLOR, 40))
# 雷达
if self.cfg.get("show_radar"):
txt = "雷达: {}cm".format(dist)
texts.append((bw+20, self.sh-80, txt, (255,255,0), 30))
add_texts_to_pil(pil_img, texts)
arr = np.array(pil_img)
self._out_q.append(QImage(arr.data, arr.shape[1], arr.shape[0], QImage.Format_RGB888))
# 反向串口发送
presence = [1 if d in active else 0 for d in ["front","left","back","right"]]
self.ser_w.update_send(*presence, self.cfg.get("radar_alarm_dist", 50))
time.sleep(0.03)
# ==========================================================
# 四分屏渲染
# ==========================================================
class QuadWorker(threading.Thread):
def __init__(self, raw_worker, serial_worker, cfg, sw, sh):
super().__init__(daemon=True)
self.raw_w, self.ser_w, self.cfg = raw_worker, serial_worker, cfg
self.sw, self.sh = sw, sh
self.running = True
self._out_q = deque(maxlen=2)
def run(self):
cw, ch = self.sw // 2, self.sh // 2
while self.running:
frames = []
for n in ["front","left","back","right"]:
f = self.raw_w.get_frame(n)
frames.append(cv2.resize(f if f is not None else np.zeros((ch,cw,3), np.uint8), (cw,ch)))
top = np.hstack((frames[0], frames[1]))
bot = np.hstack((frames[2], frames[3]))
canvas = np.vstack((top, bot))
rgb = cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB)
self._out_q.append(QImage(rgb.data, self.sw, self.sh, QImage.Format_RGB888))
self.ser_w.update_send(0,0,0,0, self.cfg.get("radar_alarm_dist", 50))
time.sleep(0.03)
# ==========================================================
# 回放面板 (充满右侧)
# ==========================================================
class PlaybackPanel(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self._video_list = []
self._play_cap = None
self._timer = QTimer()
self._timer.timeout.connect(self._step)
layout = QHBoxLayout(self)
# 左侧列表放大
self.list_w = QListWidget()
self.list_w.setFixedWidth(300)
self.list_w.setStyleSheet("font-size: 22px; background:#0d1117; color:white;")
layout.addWidget(self.list_w)
# 右侧画面
self.view = QLabel("选择视频按 OK")
self.view.setStyleSheet("background:black; border:2px solid #333;")
self.view.setAlignment(Qt.AlignCenter)
self.view.setScaledContents(True) # ★ 关键:拉伸充满
layout.addWidget(self.view, 1)
def load(self):
self.list_w.clear()
self._video_list = sorted(glob.glob(VIDEO_DIR + "/rec_*.mp4"), reverse=True)
for f in self._video_list:
item = QListWidgetItem(os.path.basename(f))
item.setSizeHint(QSize(0, 60))
self.list_w.addItem(item)
self.list_w.setCurrentRow(0)
def on_key(self, code):
if code == KEY_UP: self.list_w.setCurrentRow(max(0, self.list_w.currentRow()-1))
elif code == KEY_DOWN: self.list_w.setCurrentRow(min(len(self._video_list)-1, self.list_w.currentRow()+1))
elif code == KEY_OK: self._start_play()
def _start_play(self):
if self._play_cap: self._play_cap.release()
self._play_cap = cv2.VideoCapture(self._video_list[self.list_w.currentRow()])
self._timer.start(33)
def _step(self):
ret, frame = self._play_cap.read()
if ret:
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
qimg = QImage(rgb.data, rgb.shape[1], rgb.shape[0], QImage.Format_RGB888)
self.view.setPixmap(QPixmap.fromImage(qimg))
else: self._timer.stop()
def cleanup(self):
self._timer.stop()
if self._play_cap: self._play_cap.release()
# ==========================================================
# 设置面板 (字体放大)
# ==========================================================
class SettingsPanel(QWidget):
def __init__(self, cfg, parent=None):
super().__init__(parent)
self.cfg = cfg
self.cur = 0
self.sub_mode = False # False=Menu, True=Playback
self.menu = [
{"n":"显示模式", "k":"display_mode", "opts":["360环视","四分屏"]},
{"n":"轮播间隔", "k":"carousel_interval", "min":1, "max":10},
{"n":"画面亮度", "k":"brightness", "min":0, "max":100},
{"n":"雷达阈值", "k":"radar_alarm_dist", "min":10, "max":200},
{"n":"视频回放", "k":"page"}
]
self.stack = QStackedWidget()
# Menu Page
self.menu_page = QWidget()
mv = QVBoxLayout(self.menu_page)
self.list_v = QListWidget()
# ★ 列表样式:大字体,大行高
self.list_v.setStyleSheet("""
QListWidget { font-size: 26px; background:#1a1a2e; color:white; border:none; }
QListWidget::item { height: 70px; border-bottom:1px solid #333; padding-left:20px; }
QListWidget::item:selected { background:#e94560; }
""")
mv.addWidget(self.list_v)
self.val_l = QLabel()
self.val_l.setAlignment(Qt.AlignCenter)
self.val_l.setStyleSheet("font-size: 50px; color:#e94560; padding:20px;")
mv.addWidget(self.val_l)
self.stack.addWidget(self.menu_page)
self.play_panel = PlaybackPanel()
self.stack.addWidget(self.play_panel)
layout = QVBoxLayout(self)
layout.setContentsMargins(0,0,0,0)
layout.addWidget(self.stack)
def load(self):
self.sub_mode = False
self.stack.setCurrentIndex(0)
self._refresh()
def _refresh(self):
self.list_v.clear()
for i, m in enumerate(self.menu):
self.list_v.addItem(m["n"])
self.list_v.setCurrentRow(self.cur)
# 显示当前值
item = self.menu[self.cur]
if "opts" in item:
self.val_l.setText(item["opts"][self.cfg[item["k"]]])
elif "min" in item:
self.val_l.setText(str(self.cfg[item["k"]]))
else:
self.val_l.setText("按 OK 进入")
def on_key(self, code):
if self.sub_mode:
if code == KEY_BACK:
self.sub_mode = False
self.play_panel.cleanup()
self.stack.setCurrentIndex(0)
else: self.play_panel.on_key(code)
return
if code == KEY_UP: self.cur = (self.cur-1)%len(self.menu); self._refresh()
elif code == KEY_DOWN: self.cur = (self.cur+1)%len(self.menu); self._refresh()
elif code == KEY_LEFT or code == KEY_RIGHT:
item = self.menu[self.cur]
step = 1 if code == KEY_RIGHT else -1
if "opts" in item:
self.cfg[item["k"]] = (self.cfg[item["k"]] + step) % len(item["opts"])
elif "min" in item:
self.cfg[item["k"]] = max(item["min"], min(item["max"], self.cfg[item["k"]] + step * 5))
self._refresh()
elif code == KEY_OK:
if self.menu[self.cur]["k"] == "page":
self.sub_mode = True
self.play_panel.load()
self.stack.setCurrentIndex(1)
elif "opts" in self.menu[self.cur]:
self.cfg[self.menu[self.cur]["k"]] = (self.cfg[self.menu[self.cur]["k"]] + 1) % len(self.menu[self.cur]["opts"])
self._refresh()
# ==========================================================
# 主窗口
# ==========================================================
class MainWindow(QMainWindow):
def __init__(self, multi_cam, ser_w, remote_w, cfg):
super().__init__()
self.cfg = cfg
self.ser_w = ser_w
self.showFullScreen()
sw, sh = self.width(), self.height()
self.stack = QStackedWidget()
self.setCentralWidget(self.stack)
# View Page
self.view_l = QLabel()
self.view_l.setScaledContents(True) # ★ 强制铺满
self.stack.addWidget(self.view_l)
self.set_panel = SettingsPanel(cfg)
self.stack.addWidget(self.set_panel)
# Workers
self.raw_w = RawCaptureWorker(multi_cam.caps, multi_cam.names)
self.raw_w.start()
self.bird_w = BirdViewWorker(self.raw_w, multi_cam.camera_models, multi_cam.birdview)
self.bird_w.start()
self.render_360 = RenderWorker(self.bird_w, self.raw_w, ser_w, CarouselScheduler(), cfg, sw, sh)
self.render_360.start()
self.render_quad = QuadWorker(self.raw_w, ser_w, cfg, sw, sh)
self.render_quad.start()
RecordWorker(self.raw_w).start()
self.timer = QTimer()
self.timer.timeout.connect(self._flush)
self.timer.start(20)
remote_w.signals.key_pressed.connect(self._on_key)
def _flush(self):
if self.stack.currentIndex() != 0: return
qimg = self.render_quad._out_q[-1] if self.cfg["display_mode"] == 1 else self.render_360.get_qimage()
if qimg: self.view_l.setPixmap(QPixmap.fromImage(qimg))
def _on_key(self, code):
if self.stack.currentIndex() == 0:
if code == KEY_OK:
self.set_panel.load()
self.stack.setCurrentIndex(1)
else:
if code == KEY_BACK and not self.set_panel.sub_mode:
save_config(self.cfg)
self.ser_w.set_rx_enabled(self.cfg["display_mode"] == 0)
self.stack.setCurrentIndex(0)
else:
self.set_panel.on_key(code)
# ==========================================================
# 入口
# ==========================================================
def main():
cfg = load_config()
app = QApplication(sys.argv)
ser_w = SerialWorker()
ser_w.start()
rem_w = RemoteWorker()
rem_w.start()
cam = MultiCameraBirdView()
if not cam.running: return
win = MainWindow(cam, ser_w, rem_w, cfg)
win.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()

1571
3.py

File diff suppressed because it is too large Load Diff

Binary file not shown.

3
camera_manager.sh → bash/camera_manager.sh Executable file → Normal file
View File

@@ -40,8 +40,7 @@ WorkingDirectory=$WORK_DIR
ExecStart=/bin/bash $SCRIPT_PATH run ExecStart=/bin/bash $SCRIPT_PATH run
Restart=always Restart=always
RestartSec=5 RestartSec=5
StandardOutput=append:$LOG_FILE
StandardError=append:$LOG_FILE
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@@ -13,13 +13,35 @@ PYTHON_SCRIPT="web.py"
USER="ztl" USER="ztl"
SYSTEMD_SERVICE_FILE="/etc/systemd/system/${APP_NAME}.service" SYSTEMD_SERVICE_FILE="/etc/systemd/system/${APP_NAME}.service"
SCRIPT_PATH="$WORK_DIR/camera_manager.sh" SCRIPT_PATH="$WORK_DIR/bash/camera_manager.sh"
# 日志文件(保活日志) # 日志文件
LOG_FILE="$WORK_DIR/${APP_NAME}_keepalive.log" LOG_FILE="$WORK_DIR/${APP_NAME}_keepalive.log"
# logrotate 配置文件路径
LOGROTATE_CONF="/etc/logrotate.d/${APP_NAME}"
# 启动主程序(带保活循环) # 1. 配置 logrotate (限制日志大小为 10K)
setup_logrotate() {
cat > "$LOGROTATE_CONF" <<EOF
$LOG_FILE {
size 10k
rotate 3
compress
delaycompress
missingok
notifempty
copytruncate
create 644 $USER $USER
}
EOF
echo "✅ 日志轮替规则已配置 (限制 10k)"
}
# 2. 启动主程序(带保活循环)
start_app() { start_app() {
# 确保 logrotate 配置存在
setup_logrotate
cd "$WORK_DIR" || exit 1 cd "$WORK_DIR" || exit 1
echo "$(date): 启动 $APP_NAME 应用..." >> "$LOG_FILE" echo "$(date): 启动 $APP_NAME 应用..." >> "$LOG_FILE"
while true; do while true; do
@@ -28,11 +50,16 @@ start_app() {
else else
echo "$(date): 程序异常退出(代码 $?5秒后重启..." >> "$LOG_FILE" echo "$(date): 程序异常退出(代码 $?5秒后重启..." >> "$LOG_FILE"
fi fi
# 强制运行 logrotate 检查大小
# 注意:虽然 logrotate 通常由 cron 每天运行,但这里手动调用以确保实时性
logrotate -f "$LOGROTATE_CONF" 2>/dev/null
sleep 5 sleep 5
done done
} }
# 创建 systemd 服务文件 # 3. 创建 systemd 服务文件
create_systemd_service() { create_systemd_service() {
cat > "$SYSTEMD_SERVICE_FILE" <<EOF cat > "$SYSTEMD_SERVICE_FILE" <<EOF
[Unit] [Unit]
@@ -61,7 +88,7 @@ EOF
echo "✅ systemd 服务已创建: $APP_NAME" echo "✅ systemd 服务已创建: $APP_NAME"
} }
# 启用开机自启 # 4. 启用开机自启
enable_autostart() { enable_autostart() {
# 确保 ztl 用户有串口和摄像头权限 # 确保 ztl 用户有串口和摄像头权限
usermod -aG dialout ztl 2>/dev/null usermod -aG dialout ztl 2>/dev/null
@@ -77,16 +104,17 @@ enable_autostart() {
echo "✅ 已启用开机自启,并以 ztl 用户启动服务" echo "✅ 已启用开机自启,并以 ztl 用户启动服务"
} }
# 禁用开机自启 # 5. 禁用开机自启
disable_autostart() { disable_autostart() {
systemctl stop "$APP_NAME".service 2>/dev/null systemctl stop "$APP_NAME".service 2>/dev/null
systemctl disable "$APP_NAME".service 2>/dev/null systemctl disable "$APP_NAME".service 2>/dev/null
rm -f "$SYSTEMD_SERVICE_FILE" rm -f "$SYSTEMD_SERVICE_FILE"
rm -f "$LOGROTATE_CONF" # 同时删除 logrotate 配置
systemctl daemon-reload systemctl daemon-reload
echo "✅ 已禁用开机自启,并移除服务" echo "✅ 已禁用开机自启,并移除服务"
} }
# 显示状态 # 6. 显示状态
show_status() { show_status() {
if systemctl is-active --quiet "$APP_NAME".service; then if systemctl is-active --quiet "$APP_NAME".service; then
echo "🟢 服务正在运行(开机自启已启用)" echo "🟢 服务正在运行(开机自启已启用)"
@@ -98,7 +126,7 @@ show_status() {
fi fi
} }
# 主菜单 # 7. 主菜单
show_menu() { show_menu() {
echo "========================================" echo "========================================"
echo " LJ360 摄像头系统管理工具" echo " LJ360 摄像头系统管理工具"
@@ -152,4 +180,4 @@ while true; do
;; ;;
esac esac
echo "" echo ""
done done

995
demo1.py
View File

@@ -1,995 +0,0 @@
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()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1,731 +0,0 @@
#!/bin/bash
# =============================================
# LJ360 全景监控服务管理脚本 - 增强版
# 基于步骤化提示、彩色输出、友好交互
# 适用于 RK3588 / Linux 本地或远程管理
# =============================================
# ---------------------------------------------
# 初始化设置
# ---------------------------------------------
# 遇到错误立即退出
set -e
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
PURPLE='\033[0;35m'
NC='\033[0m' # No Color
# 进度与状态打印函数
print_header() {
clear
echo -e "${PURPLE}"
echo "╔══════════════════════════════════════════════════════╗"
echo "║ LJ360 全景监控服务管理工具 (增强版) ║"
echo "║ RK3588 / Linux ║"
echo "╚══════════════════════════════════════════════════════╝"
echo -e "${NC}"
}
print_step() {
echo -e "${BLUE}[STEP $1/$2]${NC} $3"
}
print_success() {
echo -e "${GREEN}${NC} $1"
}
print_warning() {
echo -e "${YELLOW}${NC} $1"
}
print_error() {
echo -e "${RED}${NC} $1"
}
print_progress() {
echo -e "${CYAN}[进度]${NC} $1"
}
print_divider() {
echo -e "${BLUE}──────────────────────────────────────────────${NC}"
}
# ---------------------------------------------
# 全局配置
# ---------------------------------------------
TOTAL_STEPS=9
STEP=1
SERVICE_NAME="lj360.service"
WATCHDOG_SERVICE="lj360_watchdog.service"
# 获取当前用户
CURRENT_USER=$(whoami)
# 检查是否以 root 运行
if [ "$EUID" -eq 0 ]; then
print_error "❌ 请勿使用 root 用户运行此脚本。"
print_error "✅ 请使用以下命令运行(普通用户):"
print_error " bash $0"
exit 1
fi
# 项目目录
PROJECT_DIR="/home/$CURRENT_USER/LJ360"
print_header
print_success "🔧 当前用户: $CURRENT_USER"
print_success "📂 项目目录: $PROJECT_DIR"
echo ""
# ---------------------------------------------
# 主菜单逻辑与步骤化功能
# ---------------------------------------------
main_menu() {
while true; do
show_status
echo -e "${CYAN}🔧 请选择要执行的操作:${NC}"
print_divider
echo "1) 🛠️ 安装 LJ360 全景监控服务"
echo "2) 🛡️ 安装并启动看门狗服务"
echo "3) ▶️ 启动全景监控服务"
echo "4) ⏹️ 停止全景监控服务"
echo "5) 🔄 重启全景监控服务"
echo "6) 🔔 启用开机自启"
echo "7) 🚫 禁用开机自启"
echo "8) 📜 查看实时日志"
echo "9) 📊 查看系统状态"
echo "10) 🧹 卸载 LJ360 服务"
echo "0) 🚪 退出脚本"
print_divider
read -p "👉 请输入选项 [0-10]: " choice
case $choice in
1)
STEP=1
install_service
;;
2)
STEP=1
install_watchdog_service
;;
3)
STEP=1
start_service
;;
4)
STEP=1
stop_service
;;
5)
STEP=1
restart_service
;;
6)
STEP=1
enable_service
;;
7)
STEP=1
disable_service
;;
8)
STEP=1
view_logs
;;
9)
STEP=1
view_status
;;
10)
STEP=1
uninstall_service
;;
0)
print_success "👋 再见!脚本已退出。"
exit 0
;;
*)
print_error "❌ 无效选项,请重新输入 [0-10]"
sleep 2
;;
esac
done
}
# ---------------------------------------------
# 功能函数
# ---------------------------------------------
show_status() {
print_header
echo -e "${BLUE}【📊 当前服务状态】${NC}"
print_divider
# 检查主服务状态
if systemctl is-active --quiet $SERVICE_NAME 2>/dev/null; then
echo -e "${GREEN}● LJ360 服务正在运行 ✅${NC}"
else
echo -e "${RED}● LJ360 服务未运行 ❌${NC}"
fi
# 检查看门狗服务状态
if systemctl is-active --quiet $WATCHDOG_SERVICE 2>/dev/null; then
echo -e "${GREEN}● 看门狗服务正在运行 🛡️${NC}"
else
echo -e "${YELLOW}● 看门狗服务未运行 ⚠${NC}"
fi
# 检查开机自启状态
if systemctl is-enabled $SERVICE_NAME 2>/dev/null; then
echo -e "${GREEN}● 开机自启已启用 ✅${NC}"
else
echo -e "${YELLOW}● 开机自启未启用 ⚠${NC}"
fi
# 检查Web端口
echo ""
echo -e "${BLUE}【🌐 网络端口状态】${NC}"
print_divider
if netstat -tlnp 2>/dev/null | grep -q ":5000"; then
echo -e "${GREEN}● Web 服务端口 5000 已监听 ✅${NC}"
else
echo -e "${RED}● Web 服务端口 5000 未监听 ❌${NC}"
fi
echo ""
}
# --------------------------
# Step 1: 安装服务
# --------------------------
install_service() {
print_step $STEP $TOTAL_STEPS "安装 LJ360 全景监控服务"
# 检查项目目录是否存在
if [ ! -d "$PROJECT_DIR" ]; then
print_error "❌ 项目目录不存在: $PROJECT_DIR"
print_error "请确认项目路径是否正确"
read -p "🔘 按 Enter 继续..." -r
return
fi
# 检查web.py文件是否存在
if [ ! -f "$PROJECT_DIR/web.py" ]; then
print_error "❌ web.py 文件不存在: $PROJECT_DIR/web.py"
print_error "请确认项目文件是否正确"
read -p "🔘 按 Enter 继续..." -r
return
fi
# 创建启动脚本
local START_SCRIPT="$PROJECT_DIR/start_lj360.sh"
print_progress "🔧 创建启动脚本: $START_SCRIPT"
cat > "$START_SCRIPT" << EOF
#!/bin/bash
# LJ360 全景监控启动脚本
# 项目目录: $PROJECT_DIR
# 切换到项目目录
cd "$PROJECT_DIR"
# 设置环境变量
export DISPLAY=:0
export XAUTHORITY=/home/$CURRENT_USER/.Xauthority
export PYTHONPATH=$PROJECT_DIR:\$PYTHONPATH
# 日志文件
LOG_FILE="$PROJECT_DIR/lj360.log"
ERROR_LOG="$PROJECT_DIR/lj360_error.log"
echo "=========================================" >> "\$LOG_FILE"
echo "LJ360 启动时间: \$(date)" >> "\$LOG_FILE"
echo "=========================================" >> "\$LOG_FILE"
# 检查是否已有进程在运行
if pgrep -f "python3 web.py" > /dev/null; then
echo "\$(date): LJ360 应用已在运行,跳过启动" >> "\$LOG_FILE"
exit 0
fi
# 启动应用并记录输出
echo "\$(date): 启动 LJ360 web.py..." >> "\$LOG_FILE"
exec python3 web.py >> "\$LOG_FILE" 2>> "\$ERROR_LOG"
EOF
chmod +x "$START_SCRIPT"
chown "$CURRENT_USER:$CURRENT_USER" "$START_SCRIPT"
print_success "✅ 启动脚本已创建并授权"
# 创建 systemd 服务
print_progress "🔧 创建 systemd 服务文件: /etc/systemd/system/$SERVICE_NAME"
sudo tee /etc/systemd/system/$SERVICE_NAME > /dev/null << EOF
[Unit]
Description=LJ360 Surround View Service
After=network.target graphical.target
Wants=network.target graphical.target
[Service]
Type=simple
User=$CURRENT_USER
WorkingDirectory=$PROJECT_DIR
Environment="DISPLAY=:0"
Environment="XAUTHORITY=/home/$CURRENT_USER/.Xauthority"
Environment="PYTHONPATH=$PROJECT_DIR"
Environment="XDG_RUNTIME_DIR=/run/user/1000"
ExecStart=$PROJECT_DIR/start_lj360.sh
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
# 限制资源使用(可选)
# MemoryMax=1G
# CPUQuota=80%
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
print_success "✅ systemd 服务文件已部署 & 缓存已刷新"
STEP=$((STEP + 1))
read -p "🔘 按 Enter 继续..." -r
}
# --------------------------
# Step 2: 安装看门狗服务
# --------------------------
install_watchdog_service() {
print_step $STEP $TOTAL_STEPS "安装并启动看门狗服务"
# 创建看门狗脚本
local WATCHDOG_SCRIPT="$PROJECT_DIR/watchdog_lj360.sh"
print_progress "🔧 创建看门狗脚本: $WATCHDOG_SCRIPT"
cat > "$WATCHDOG_SCRIPT" << EOF
#!/bin/bash
# LJ360 看门狗服务脚本
# 项目目录: $PROJECT_DIR
APP_NAME="web.py"
CHECK_INTERVAL=10
LOG_FILE="$PROJECT_DIR/watchdog.log"
echo "=========================================" >> "\$LOG_FILE"
echo "看门狗服务启动时间: \$(date)" >> "\$LOG_FILE"
echo "=========================================" >> "\$LOG_FILE"
while true; do
# 检查主服务进程是否存在
if ! pgrep -f "python3 \$APP_NAME" > /dev/null; then
echo "\$(date): 检测到 LJ360 未运行,正在启动..." >> "\$LOG_FILE"
cd "$PROJECT_DIR"
# 设置环境变量
export DISPLAY=:0
export XAUTHORITY=/home/$CURRENT_USER/.Xauthority
export PYTHONPATH=$PROJECT_DIR:\$PYTHONPATH
# 启动应用
nohup python3 web.py >> "$PROJECT_DIR/lj360.log" 2>> "$PROJECT_DIR/lj360_error.log" &
echo "\$(date): 已启动 LJ360PID: \$!" >> "\$LOG_FILE"
# 等待应用启动
sleep 5
fi
sleep \$CHECK_INTERVAL
done
EOF
chmod +x "$WATCHDOG_SCRIPT"
chown "$CURRENT_USER:$CURRENT_USER" "$WATCHDOG_SCRIPT"
print_success "✅ 看门狗脚本已创建并授权"
# 创建看门狗 systemd 服务
print_progress "🔧 创建看门狗服务文件: /etc/systemd/system/$WATCHDOG_SERVICE"
sudo tee /etc/systemd/system/$WATCHDOG_SERVICE > /dev/null << EOF
[Unit]
Description=LJ360 Watchdog Service
After=network.target
Wants=network.target
[Service]
Type=simple
User=$CURRENT_USER
WorkingDirectory=$PROJECT_DIR
ExecStart=$PROJECT_DIR/watchdog_lj360.sh
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
print_success "✅ 看门狗服务文件已部署"
# 启用并启动看门狗服务
print_progress "🚀 启动看门狗服务"
sudo systemctl enable $WATCHDOG_SERVICE
sudo systemctl start $WATCHDOG_SERVICE
sleep 2
if systemctl is-active --quiet $WATCHDOG_SERVICE; then
print_success "✅ 看门狗服务启动成功"
else
print_warning "⚠️ 看门狗服务启动可能有问题,请查看日志"
fi
STEP=$((STEP + 1))
read -p "🔘 按 Enter 继续..." -r
}
# --------------------------
# Step 3: 启动服务
# --------------------------
start_service() {
print_step $STEP $TOTAL_STEPS "启动 LJ360 全景监控服务"
# 检查服务文件是否存在
if [ ! -f "/etc/systemd/system/$SERVICE_NAME" ]; then
print_error "❌ 服务未安装,请先安装服务"
read -p "🔘 按 Enter 继续..." -r
return
fi
print_progress "🚀 正在启动服务: $SERVICE_NAME"
sudo systemctl start "$SERVICE_NAME"
sleep 3
# 检查启动状态
if systemctl is-active --quiet "$SERVICE_NAME"; then
print_success "✅ 服务启动成功"
# 检查Web端口
sleep 2
if netstat -tlnp 2>/dev/null | grep -q ":5000"; then
local IP_ADDR=$(hostname -I | awk '{print $1}')
print_success "🌐 Web 服务已启动: http://$IP_ADDR:5000"
print_success "🌐 本地访问: http://localhost:5000"
fi
else
print_warning "⚠️ 服务启动可能有问题,请查看日志"
fi
STEP=$((STEP + 1))
read -p "🔘 按 Enter 继续..." -r
}
# --------------------------
# Step 4: 停止服务
# --------------------------
stop_service() {
print_step $STEP $TOTAL_STEPS "停止 LJ360 全景监控服务"
# 检查服务文件是否存在
if [ ! -f "/etc/systemd/system/$SERVICE_NAME" ]; then
print_error "❌ 服务未安装,请先安装服务"
read -p "🔘 按 Enter 继续..." -r
return
fi
print_progress "⏹️ 正在停止服务: $SERVICE_NAME"
sudo systemctl stop "$SERVICE_NAME"
sleep 2
# 清理可能的残留进程
print_progress "🧹 清理残留进程..."
pkill -f "python3 web.py" 2>/dev/null || true
# 检查停止状态
if ! systemctl is-active --quiet "$SERVICE_NAME"; then
print_success "✅ 服务停止成功"
else
print_warning "⚠️ 服务停止可能有问题"
fi
STEP=$((STEP + 1))
read -p "🔘 按 Enter 继续..." -r
}
# --------------------------
# Step 5: 重启服务
# --------------------------
restart_service() {
print_step $STEP $TOTAL_STEPS "重启 LJ360 全景监控服务"
# 检查服务文件是否存在
if [ ! -f "/etc/systemd/system/$SERVICE_NAME" ]; then
print_error "❌ 服务未安装,请先安装服务"
read -p "🔘 按 Enter 继续..." -r
return
fi
print_progress "🔄 正在重启服务: $SERVICE_NAME"
sudo systemctl restart "$SERVICE_NAME"
sleep 3
# 检查重启状态
if systemctl is-active --quiet "$SERVICE_NAME"; then
print_success "✅ 服务重启成功"
# 检查Web端口
sleep 2
if netstat -tlnp 2>/dev/null | grep -q ":5000"; then
local IP_ADDR=$(hostname -I | awk '{print $1}')
print_success "🌐 Web 服务已重启: http://$IP_ADDR:5000"
fi
else
print_warning "⚠️ 服务重启可能有问题,请查看日志"
fi
STEP=$((STEP + 1))
read -p "🔘 按 Enter 继续..." -r
}
# --------------------------
# Step 6: 启用开机自启
# --------------------------
enable_service() {
print_step $STEP $TOTAL_STEPS "启用开机自启"
# 检查服务文件是否存在
if [ ! -f "/etc/systemd/system/$SERVICE_NAME" ]; then
print_error "❌ 服务未安装,请先安装服务"
read -p "🔘 按 Enter 继续..." -r
return
fi
print_progress "🔔 正在启用开机自启: $SERVICE_NAME"
sudo systemctl enable "$SERVICE_NAME"
sleep 1
if systemctl is-enabled "$SERVICE_NAME"; then
print_success "✅ 开机自启已启用"
else
print_error "❌ 开机自启启用失败"
fi
STEP=$((STEP + 1))
read -p "🔘 按 Enter 继续..." -r
}
# --------------------------
# Step 7: 禁用开机自启
# --------------------------
disable_service() {
print_step $STEP $TOTAL_STEPS "禁用开机自启"
# 检查服务文件是否存在
if [ ! -f "/etc/systemd/system/$SERVICE_NAME" ]; then
print_error "❌ 服务未安装,请先安装服务"
read -p "🔘 按 Enter 继续..." -r
return
fi
print_progress "🚫 正在禁用开机自启: $SERVICE_NAME"
sudo systemctl disable "$SERVICE_NAME"
sleep 1
if ! systemctl is-enabled "$SERVICE_NAME"; then
print_success "✅ 开机自启已禁用"
else
print_error "❌ 开机自启禁用失败"
fi
STEP=$((STEP + 1))
read -p "🔘 按 Enter 继续..." -r
}
# --------------------------
# Step 8: 查看日志
# --------------------------
view_logs() {
print_step $STEP $TOTAL_STEPS "查看实时日志"
# 检查服务文件是否存在
if [ ! -f "/etc/systemd/system/$SERVICE_NAME" ]; then
print_error "❌ 服务未安装,请先安装服务"
read -p "🔘 按 Enter 继续..." -r
return
fi
print_progress "📜 请选择要查看的日志类型:"
echo "1) 📋 查看 systemd 服务日志"
echo "2) 📄 查看应用日志文件"
echo "3) ⚠️ 查看错误日志文件"
echo "4) 🛡️ 查看看门狗日志"
print_divider
read -p "👉 请选择 [1-4]: " log_choice
case $log_choice in
1)
print_progress "📜 正在打开 systemd 服务日志(按 Ctrl+C 退出)"
echo ""
sudo journalctl -u "$SERVICE_NAME" -f
;;
2)
print_progress "📄 正在查看应用日志文件: $PROJECT_DIR/lj360.log"
echo ""
if [ -f "$PROJECT_DIR/lj360.log" ]; then
tail -50 "$PROJECT_DIR/lj360.log"
echo ""
echo "按 Enter 查看更多,或按 Ctrl+C 退出..."
tail -f "$PROJECT_DIR/lj360.log"
else
print_error "❌ 日志文件不存在"
fi
;;
3)
print_progress "⚠️ 正在查看错误日志文件: $PROJECT_DIR/lj360_error.log"
echo ""
if [ -f "$PROJECT_DIR/lj360_error.log" ]; then
tail -50 "$PROJECT_DIR/lj360_error.log"
else
print_warning "⚠️ 错误日志文件不存在"
fi
;;
4)
print_progress "🛡️ 正在查看看门狗日志文件: $PROJECT_DIR/watchdog.log"
echo ""
if [ -f "$PROJECT_DIR/watchdog.log" ]; then
tail -50 "$PROJECT_DIR/watchdog.log"
else
print_warning "⚠️ 看门狗日志文件不存在"
fi
;;
*)
print_error "❌ 无效选项"
;;
esac
STEP=$((STEP + 1))
read -p "🔘 日志查看结束,按 Enter 继续..." -r
}
# --------------------------
# Step 9: 查看系统状态
# --------------------------
view_status() {
print_step $STEP $TOTAL_STEPS "查看详细系统状态"
echo -e "${CYAN}【📊 系统资源状态】${NC}"
print_divider
# CPU使用率
echo -e "${BLUE}● CPU 使用率:${NC}"
top -bn1 | grep "Cpu(s)" | awk '{print " " $2 "% user, " $4 "% system, " $8 "% idle"}'
# 内存使用
echo -e "${BLUE}● 内存使用:${NC}"
free -h | awk '/^Mem:/ {print " " $3 " / " $2 " (" $3/$2*100 "%) used"}'
# 磁盘空间
echo -e "${BLUE}● 磁盘空间:${NC}"
df -h /home | awk 'NR==2 {print " " $3 " / " $2 " (" $5 ") used"}'
echo ""
echo -e "${CYAN}【🔍 进程状态】${NC}"
print_divider
# LJ360相关进程
if pgrep -f "python3 web.py" > /dev/null; then
echo -e "${GREEN}● LJ360 进程正在运行 ✅${NC}"
ps aux | grep "python3 web.py" | grep -v grep | awk '{print " PID: " $2 ", CPU: " $3 "%, MEM: " $4 "%, CMD: " $11}'
else
echo -e "${RED}● LJ360 进程未运行 ❌${NC}"
fi
echo ""
echo -e "${CYAN}【📈 服务运行时间】${NC}"
print_divider
if systemctl is-active --quiet $SERVICE_NAME; then
UPTIME=$(systemctl show $SERVICE_NAME --property=ActiveEnterTimestamp | awk -F= '{print $2}')
echo -e "${GREEN}● 服务启动时间: $UPTIME${NC}"
fi
if systemctl is-active --quiet $WATCHDOG_SERVICE; then
WATCHDOG_UPTIME=$(systemctl show $WATCHDOG_SERVICE --property=ActiveEnterTimestamp | awk -F= '{print $2}')
echo -e "${GREEN}● 看门狗启动时间: $WATCHDOG_UPTIME${NC}"
fi
STEP=$((STEP + 1))
read -p "🔘 按 Enter 继续..." -r
}
# --------------------------
# Step 10: 卸载服务
# --------------------------
uninstall_service() {
print_step $STEP $TOTAL_STEPS "卸载 LJ360 全景监控服务"
echo -e "${RED}⚠ 警告:此操作将卸载 LJ360 全景监控服务!${NC}"
echo -n "✅ 确定要卸载吗?(y/N): "
read -r confirm
if [[ $confirm =~ ^[Yy]$ ]]; then
print_progress "⏹️ 停止服务..."
sudo systemctl stop "$SERVICE_NAME" 2>/dev/null || true
sudo systemctl stop "$WATCHDOG_SERVICE" 2>/dev/null || true
print_progress "🚫 禁用开机自启..."
sudo systemctl disable "$SERVICE_NAME" 2>/dev/null || true
sudo systemctl disable "$WATCHDOG_SERVICE" 2>/dev/null || true
print_progress "🗑️ 删除 systemd 服务文件..."
sudo rm -f /etc/systemd/system/"$SERVICE_NAME"
sudo rm -f /etc/systemd/system/"$WATCHDOG_SERVICE"
sudo systemctl daemon-reload
sudo systemctl reset-failed
print_progress "🗑️ 删除启动脚本..."
rm -f "$PROJECT_DIR/start_lj360.sh"
rm -f "$PROJECT_DIR/watchdog_lj360.sh"
print_progress "🔪 清理进程..."
pkill -f "python3 web.py" 2>/dev/null || true
pkill -f "watchdog_lj360.sh" 2>/dev/null || true
print_success "✅ LJ360 全景监控服务卸载完成"
print_warning "📝 日志文件保留在: $PROJECT_DIR/"
print_warning " - lj360.log"
print_warning " - lj360_error.log"
print_warning " - watchdog.log"
else
print_warning "❌ 取消卸载操作"
fi
STEP=$((STEP + 1))
read -p "🔘 按 Enter 继续..." -r
}
# ---------------------------------------------
# 启动主菜单
# ---------------------------------------------
# 检查是否在正确的目录运行
if [ ! -d "$PROJECT_DIR" ]; then
print_error "❌ 项目目录不存在: $PROJECT_DIR"
print_error "请确认项目路径是否正确,或者修改脚本中的 PROJECT_DIR 变量"
exit 1
fi
main_menu

View File

@@ -1,18 +0,0 @@
10.0
13.0
16.0
30.0
33.0
23.0
30.0
61.0
62.0
45.0
59.0
119.0
116.0
90.0
156.0
198.0
373.0
326.0

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

View File

@@ -1,80 +0,0 @@
person
bicycle
car
motorcycle
airplane
bus
train
truck
boat
traffic light
fire hydrant
stop sign
parking meter
bench
bird
cat
dog
horse
sheep
cow
elephant
bear
zebra
giraffe
backpack
umbrella
handbag
tie
suitcase
frisbee
skis
snowboard
sports ball
kite
baseball bat
baseball glove
skateboard
surfboard
tennis racket
bottle
wine glass
cup
fork
knife
spoon
bowl
banana
apple
sandwich
orange
broccoli
carrot
hot dog
pizza
donut
cake
chair
couch
potted plant
bed
dining table
toilet
tv
laptop
mouse
remote
keyboard
cell phone
microwave
oven
toaster
sink
refrigerator
book
clock
vase
scissors
teddy bear
hair drier
toothbrush

View File

@@ -1 +0,0 @@
wget -O ./yolov5s_relu.onnx https://ftrg.zbox.filez.com/v2/delivery/data/95f00b0fc900458ba134f8b180b3f7a1/examples/yolov5/yolov5s_relu.onnx

View File

View File

@@ -1,176 +0,0 @@
from copy import copy
import os
import cv2
import numpy as np
import json
class Letter_Box_Info():
def __init__(self, shape, new_shape, w_ratio, h_ratio, dw, dh, pad_color) -> None:
self.origin_shape = shape
self.new_shape = new_shape
self.w_ratio = w_ratio
self.h_ratio = h_ratio
self.dw = dw
self.dh = dh
self.pad_color = pad_color
def coco_eval_with_json(anno_json, pred_json):
from pycocotools.coco import COCO
from pycocotools.cocoeval import COCOeval
anno = COCO(anno_json)
pred = anno.loadRes(pred_json)
eval = COCOeval(anno, pred, 'bbox')
# eval.params.useCats = 0
# eval.params.maxDets = list((100, 300, 1000))
# a = np.array(list(range(50, 96, 1)))/100
# eval.params.iouThrs = a
eval.evaluate()
eval.accumulate()
eval.summarize()
map, map50 = eval.stats[:2] # update results (mAP@0.5:0.95, mAP@0.5)
print('map --> ', map)
print('map50--> ', map50)
print('map75--> ', eval.stats[2])
print('map85--> ', eval.stats[-2])
print('map95--> ', eval.stats[-1])
class COCO_test_helper():
def __init__(self, enable_letter_box = False) -> None:
self.record_list = []
self.enable_ltter_box = enable_letter_box
if self.enable_ltter_box is True:
self.letter_box_info_list = []
else:
self.letter_box_info_list = None
def letter_box(self, im, new_shape, pad_color=(0,0,0), info_need=False):
# Resize and pad image while meeting stride-multiple constraints
shape = im.shape[:2] # current shape [height, width]
if isinstance(new_shape, int):
new_shape = (new_shape, new_shape)
# Scale ratio
r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])
# Compute padding
ratio = r # width, height ratios
new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r))
dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1] # wh padding
dw /= 2 # divide padding into 2 sides
dh /= 2
if shape[::-1] != new_unpad: # resize
im = cv2.resize(im, new_unpad, interpolation=cv2.INTER_LINEAR)
top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))
left, right = int(round(dw - 0.1)), int(round(dw + 0.1))
im = cv2.copyMakeBorder(im, top, bottom, left, right, cv2.BORDER_CONSTANT, value=pad_color) # add border
if self.enable_ltter_box is True:
self.letter_box_info_list.append(Letter_Box_Info(shape, new_shape, ratio, ratio, dw, dh, pad_color))
if info_need is True:
return im, ratio, (dw, dh)
else:
return im
def direct_resize(self, im, new_shape, info_need=False):
shape = im.shape[:2]
h_ratio = new_shape[0]/ shape[0]
w_ratio = new_shape[1]/ shape[1]
if self.enable_ltter_box is True:
self.letter_box_info_list.append(Letter_Box_Info(shape, new_shape, w_ratio, h_ratio, 0, 0, (0,0,0)))
im = cv2.resize(im, (new_shape[1], new_shape[0]))
return im
def get_real_box(self, box, in_format='xyxy'):
bbox = copy(box)
if self.enable_ltter_box == True:
# unletter_box result
if in_format=='xyxy':
bbox[:,0] -= self.letter_box_info_list[-1].dw
bbox[:,0] /= self.letter_box_info_list[-1].w_ratio
bbox[:,0] = np.clip(bbox[:,0], 0, self.letter_box_info_list[-1].origin_shape[1])
bbox[:,1] -= self.letter_box_info_list[-1].dh
bbox[:,1] /= self.letter_box_info_list[-1].h_ratio
bbox[:,1] = np.clip(bbox[:,1], 0, self.letter_box_info_list[-1].origin_shape[0])
bbox[:,2] -= self.letter_box_info_list[-1].dw
bbox[:,2] /= self.letter_box_info_list[-1].w_ratio
bbox[:,2] = np.clip(bbox[:,2], 0, self.letter_box_info_list[-1].origin_shape[1])
bbox[:,3] -= self.letter_box_info_list[-1].dh
bbox[:,3] /= self.letter_box_info_list[-1].h_ratio
bbox[:,3] = np.clip(bbox[:,3], 0, self.letter_box_info_list[-1].origin_shape[0])
return bbox
def get_real_seg(self, seg):
#! fix side effect
dh = int(self.letter_box_info_list[-1].dh)
dw = int(self.letter_box_info_list[-1].dw)
origin_shape = self.letter_box_info_list[-1].origin_shape
new_shape = self.letter_box_info_list[-1].new_shape
if (dh == 0) and (dw == 0) and origin_shape == new_shape:
return seg
elif dh == 0 and dw != 0:
seg = seg[:, :, dw:-dw] # a[0:-0] = []
elif dw == 0 and dh != 0 :
seg = seg[:, dh:-dh, :]
seg = np.where(seg, 1, 0).astype(np.uint8).transpose(1,2,0)
seg = cv2.resize(seg, (origin_shape[1], origin_shape[0]), interpolation=cv2.INTER_LINEAR)
if len(seg.shape) < 3:
return seg[None,:,:]
else:
return seg.transpose(2,0,1)
def add_single_record(self, image_id, category_id, bbox, score, in_format='xyxy', pred_masks = None):
if self.enable_ltter_box == True:
# unletter_box result
if in_format=='xyxy':
bbox[0] -= self.letter_box_info_list[-1].dw
bbox[0] /= self.letter_box_info_list[-1].w_ratio
bbox[1] -= self.letter_box_info_list[-1].dh
bbox[1] /= self.letter_box_info_list[-1].h_ratio
bbox[2] -= self.letter_box_info_list[-1].dw
bbox[2] /= self.letter_box_info_list[-1].w_ratio
bbox[3] -= self.letter_box_info_list[-1].dh
bbox[3] /= self.letter_box_info_list[-1].h_ratio
# bbox = [value/self.letter_box_info_list[-1].ratio for value in bbox]
if in_format=='xyxy':
# change xyxy to xywh
bbox[2] = bbox[2] - bbox[0]
bbox[3] = bbox[3] - bbox[1]
else:
assert False, "now only support xyxy format, please add code to support others format"
def single_encode(x):
from pycocotools.mask import encode
rle = encode(np.asarray(x[:, :, None], order="F", dtype="uint8"))[0]
rle["counts"] = rle["counts"].decode("utf-8")
return rle
if pred_masks is None:
self.record_list.append({"image_id": image_id,
"category_id": category_id,
"bbox":[round(x, 3) for x in bbox],
'score': round(score, 5),
})
else:
rles = single_encode(pred_masks)
self.record_list.append({"image_id": image_id,
"category_id": category_id,
"bbox":[round(x, 3) for x in bbox],
'score': round(score, 5),
'segmentation': rles,
})
def export_to_json(self, path):
with open(path, 'w') as f:
json.dump(self.record_list, f)

View File

@@ -1,103 +0,0 @@
import os
import numpy as np
import onnxruntime as rt
type_map = {
'tensor(int32)' : np.int32,
'tensor(int64)' : np.int64,
'tensor(float32)' : np.float32,
'tensor(float64)' : np.float64,
'tensor(float)' : np.float32,
}
if getattr(np, 'bool', False):
type_map['tensor(bool)'] = np.bool
else:
type_map['tensor(bool)'] = bool
def ignore_dim_with_zero(_shape, _shape_target):
_shape = list(_shape)
_shape_target = list(_shape_target)
for i in range(_shape.count(1)):
_shape.remove(1)
for j in range(_shape_target.count(1)):
_shape_target.remove(1)
if _shape == _shape_target:
return True
else:
return False
class ONNX_model_container_py:
def __init__(self, model_path) -> None:
# sess_options=
sp_options = rt.SessionOptions()
sp_options.log_severity_level = 3
# [1 for info, 2 for warning, 3 for error, 4 for fatal]
self.sess = rt.InferenceSession(model_path, sess_options=sp_options, providers=['CPUExecutionProvider'])
self.model_path = model_path
def run(self, input_datas):
if len(input_datas) < len(self.sess.get_inputs()):
assert False,'inputs_datas number not match onnx model{} input'.format(self.model_path)
elif len(input_datas) > len(self.sess.get_inputs()):
print('WARNING: input datas number large than onnx input node')
input_dict = {}
for i, _input in enumerate(self.sess.get_inputs()):
# convert type
if _input.type in type_map and \
type_map[_input.type] != input_datas[i].dtype:
print('WARNING: force data-{} from {} to {}'.format(i, input_datas[i].dtype, type_map[_input.type]))
input_datas[i] = input_datas[i].astype(type_map[_input.type])
# reshape if need
if _input.shape != list(input_datas[i].shape):
if ignore_dim_with_zero(input_datas[i].shape,_input.shape):
input_datas[i] = input_datas[i].reshape(_input.shape)
print("WARNING: reshape inputdata-{}: from {} to {}".format(i, input_datas[i].shape, _input.shape))
else:
assert False, 'input shape{} not match real data shape{}'.format(_input.shape, input_datas[i].shape)
input_dict[_input.name] = input_datas[i]
output_list = []
for i in range(len(self.sess.get_outputs())):
output_list.append(self.sess.get_outputs()[i].name)
#forward model
res = self.sess.run(output_list, input_dict)
return res
class ONNX_model_container_cpp:
def __init__(self, model_path) -> None:
pass
def run(self, input_datas):
pass
def ONNX_model_container(model_path, backend='py'):
if backend == 'py':
return ONNX_model_container_py(model_path)
elif backend == 'cpp':
return ONNX_model_container_cpp(model_path)
def reset_onnx_shape(onnx_model_path, output_path, input_shapes):
if isinstance(input_shapes[0], int):
command = "python -m onnxsim {} {} --input-shape {}".format(onnx_model_path, output_path, ','.join([str(v) for v in input_shapes]))
else:
if len(input_shapes)!= 1:
print("RESET ONNX SHAPE with more than one input, try to match input name")
sess = rt.InferenceSession(onnx_model_path)
input_names = [input.name for input in sess.get_inputs()]
command = "python -m onnxsim {} {} --input-shape ".format(onnx_model_path, output_path)
for i, input_name in enumerate(input_names):
command += "{}:{} ".format(input_name, ','.join([str(v) for v in input_shapes[i]]))
else:
command = "python -m onnxsim {} {} --input-shape {}".format(onnx_model_path, output_path, ','.join([str(v) for v in input_shapes[0]]))
print(command)
os.system(command)
return output_path

View File

@@ -1,52 +0,0 @@
import torch
torch.backends.quantized.engine = 'qnnpack'
def multi_list_unfold(tl):
def unfold(_inl, target):
if not isinstance(_inl, list) and not isinstance(_inl, tuple):
target.append(_inl)
else:
unfold(_inl)
def flatten_list(in_list):
flatten = lambda x: [subitem for item in x for subitem in flatten(item)] if type(x) is list else [x]
return flatten(in_list)
class Torch_model_container:
def __init__(self, model_path, qnnpack=False) -> None:
if qnnpack is True:
torch.backends.quantized.engine = 'qnnpack'
#! Backends must be set before load model.
self.pt_model = torch.jit.load(model_path)
self.pt_model.eval()
holdon = 1
def run(self, input_datas):
assert isinstance(input_datas, list), "input_datas should be a list, like [np.ndarray, np.ndarray]"
input_datas_torch_type = []
for _data in input_datas:
input_datas_torch_type.append(torch.tensor(_data))
for i,val in enumerate(input_datas_torch_type):
if val.dtype == torch.float64:
input_datas_torch_type[i] = input_datas_torch_type[i].float()
result = self.pt_model(*input_datas_torch_type)
if isinstance(result, tuple):
result = list(result)
if not isinstance(result, list):
result = [result]
result = flatten_list(result)
for i in range(len(result)):
result[i] = torch.dequantize(result[i])
for i in range(len(result)):
# TODO support quantized_output
result[i] = result[i].cpu().detach().numpy()
return result

View File

@@ -1,31 +0,0 @@
from rknn.api import RKNN
class RKNN_model_container():
def __init__(self, model_path, target=None, device_id=None) -> None:
rknn = RKNN()
# Direct Load RKNN Model
rknn.load_rknn(model_path)
print('--> Init runtime environment')
if target==None:
ret = rknn.init_runtime()
else:
ret = rknn.init_runtime(target=target, device_id=device_id)
if ret != 0:
print('Init runtime environment failed')
exit(ret)
print('done')
self.rknn = rknn
def run(self, inputs):
if isinstance(inputs, list) or isinstance(inputs, tuple):
pass
else:
inputs = [inputs]
result = self.rknn.inference(inputs=inputs)
return result

View File

@@ -1,26 +0,0 @@
from rknnlite.api import RKNNLite as RKNN
class RKNN_model_container():
def __init__(self, model_path, target=None, device_id=None) -> None:
rknn = RKNN()
rknn.load_rknn(model_path)
ret = rknn.init_runtime()
self.rknn = rknn
def run(self, inputs):
if self.rknn is None:
print("ERROR: rknn has been released")
return []
if isinstance(inputs, list) or isinstance(inputs, tuple):
pass
else:
inputs = [inputs]
result = self.rknn.inference(inputs=inputs)
return result
def release(self):
self.rknn.release()
self.rknn = None

View File

@@ -1,403 +0,0 @@
# main.py
import cv2
import threading
import sys
import os
import argparse
import numpy as np
import time
from surround_view import FisheyeCameraModel, BirdView
import surround_view.param_settings as settings
sys.path.append(os.path.dirname(__file__))
from py_utils.coco_utils import COCO_test_helper
from py_utils.rknn_executor import RKNN_model_container
from mjpeg_streamer import MJPEGServer
# --- 新增 ---
from shared_buffer import CameraFrameBuffer, DetectionResultBuffer
# 启用 OpenCLMali-G610
if cv2.ocl.haveOpenCL():
cv2.ocl.setUseOpenCL(True)
print("✅ OpenCL is ON — using Mali-G610 GPU for acceleration")
else:
print("⚠️ OpenCL not available")
# ------ YOLO 配置 -----------
YOLO_MODEL_PATH = './yolov5s-640-640.rknn'
OBJ_THRESH = 0.6
NMS_THRESH = 0.6
IMG_SIZE = (640, 640)
CLASSES = ("person",)
ANCHORS_FILE = './model/anchors_yolov5.txt'
with open(ANCHORS_FILE, 'r') as f:
values = [float(_v) for _v in f.readlines()]
ANCHORS = np.array(values).reshape(3, -1, 2).tolist()
# ========== YOLO 后处理函数(保持不变)==========
def filter_boxes(boxes, box_confidences, box_class_probs):
box_confidences = box_confidences.reshape(-1)
class_max_score = np.max(box_class_probs, axis=-1)
classes = np.argmax(box_class_probs, axis=-1)
_class_pos = np.where(class_max_score * box_confidences >= OBJ_THRESH)
scores = (class_max_score * box_confidences)[_class_pos]
boxes = boxes[_class_pos]
classes = classes[_class_pos]
return boxes, classes, scores
def nms_boxes(boxes, scores):
x = boxes[:, 0]; y = boxes[:, 1]; w = boxes[:, 2] - boxes[:, 0]; h = boxes[:, 3] - boxes[:, 1]
areas = w * h; order = scores.argsort()[::-1]
keep = []
while order.size > 0:
i = order[0]; keep.append(i)
xx1 = np.maximum(x[i], x[order[1:]]); yy1 = np.maximum(y[i], y[order[1:]])
xx2 = np.minimum(x[i] + w[i], x[order[1:]] + w[order[1:]]); yy2 = np.minimum(y[i] + h[i], y[order[1:]] + h[order[1:]])
w1 = np.maximum(0.0, xx2 - xx1 + 0.00001); h1 = np.maximum(0.0, yy2 - yy1 + 0.00001)
inter = w1 * h1
ovr = inter / (areas[i] + areas[order[1:]] - inter)
inds = np.where(ovr <= NMS_THRESH)[0]
order = order[inds + 1]
return np.array(keep)
def box_process(position, anchors):
grid_h, grid_w = position.shape[2:4]
col, row = np.meshgrid(np.arange(0, grid_w), np.arange(0, grid_h))
col = col.reshape(1, 1, grid_h, grid_w); row = row.reshape(1, 1, grid_h, grid_w)
grid = np.concatenate((col, row), axis=1)
stride = np.array([IMG_SIZE[1] // grid_h, IMG_SIZE[0] // grid_w]).reshape(1, 2, 1, 1)
col = col.repeat(len(anchors), axis=0); row = row.repeat(len(anchors), axis=0)
anchors = np.array(anchors).reshape(*anchors.shape, 1, 1)
box_xy = position[:, :2, :, :] * 2 - 0.5
box_wh = pow(position[:, 2:4, :, :] * 2, 2) * anchors
box_xy += grid; box_xy *= stride
box = np.concatenate((box_xy, box_wh), axis=1)
xyxy = np.copy(box)
xyxy[:, 0, :, :] = box[:, 0, :, :] - box[:, 2, :, :] / 2
xyxy[:, 1, :, :] = box[:, 1, :, :] - box[:, 3, :, :] / 2
xyxy[:, 2, :, :] = box[:, 0, :, :] + box[:, 2, :, :] / 2
xyxy[:, 3, :, :] = box[:, 1, :, :] + box[:, 3, :, :] / 2
return xyxy
def post_process(input_data, anchors):
boxes, scores, classes_conf = [], [], []
input_data = [_in.reshape([len(anchors[0]), -1] + list(_in.shape[-2:])) for _in in input_data]
for i in range(len(input_data)):
boxes.append(box_process(input_data[i][:, :4, :, :], anchors[i]))
scores.append(input_data[i][:, 4:5, :, :])
classes_conf.append(input_data[i][:, 5:, :, :])
def sp_flatten(_in):
ch = _in.shape[1]; _in = _in.transpose(0, 2, 3, 1); return _in.reshape(-1, ch)
boxes = [sp_flatten(_v) for _v in boxes]
classes_conf = [sp_flatten(_v) for _v in classes_conf]
scores = [sp_flatten(_v) for _v in scores]
boxes = np.concatenate(boxes); classes_conf = np.concatenate(classes_conf); scores = np.concatenate(scores)
boxes, classes, scores = filter_boxes(boxes, scores, classes_conf)
nboxes, nclasses, nscores = [], [], []
for c in set(classes):
inds = np.where(classes == c)
b = boxes[inds]; c = classes[inds]; s = scores[inds]
keep = nms_boxes(b, s)
if len(keep) != 0:
nboxes.append(b[keep]); nclasses.append(c[keep]); nscores.append(s[keep])
if not nclasses: return None, None, None
boxes = np.concatenate(nboxes); classes = np.concatenate(nclasses); scores = np.concatenate(nscores)
return boxes, classes, scores
def draw_detections(image, boxes, scores, classes):
if boxes is None: return image
for box, score, cl in zip(boxes, scores, classes):
if CLASSES[cl] != "person": continue
top, left, right, bottom = [int(_b) for _b in box]
cv2.rectangle(image, (top, left), (right, bottom), (0, 255, 0), 2)
label = f'person: {score:.2f}'
(w, h), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
cv2.rectangle(image, (top, left - h - 5), (top + w, left), (0, 255, 0), -1)
cv2.putText(image, label, (top, left - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
return image
# ========== 主类 ==========
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]
self.camera_models = [
FisheyeCameraModel(camera_file, camera_name)
for camera_file, camera_name in zip(self.yamls, self.names)
]
self.which_cameras = {"front": 0, "back": 2, "left": 1, "right": 3}
self.caps = []
print("[INFO] 初始化摄像头...")
for name in self.names:
cap = cv2.VideoCapture(self.which_cameras[name], cv2.CAP_V4L2)
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*"YUYV"))
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
if not cap.isOpened():
print(f"[ERROR] 无法打开 {name} 摄像头", file=sys.stderr)
self.running = False
return
self.caps.append(cap)
self.birdview = BirdView()
self._initialize_weights()
# 预警状态
self.alerts = {name: False for name in self.names}
self.alert_lock = threading.Lock()
# YOLO 模型
try:
self.yolo_model = RKNN_model_container(YOLO_MODEL_PATH, target='rk3588')
self.co_helper = COCO_test_helper(enable_letter_box=True)
print("[INFO] YOLO 模型加载成功")
except Exception as e:
print(f"[ERROR] YOLO 模型加载失败: {e}")
self.yolo_model = None
# 共享缓冲区
self.undistorted_buffer = CameraFrameBuffer(self.names)
self.detection_buffer = DetectionResultBuffer(self.names)
self.shared_display_buffer = SharedFrameBuffer() # 用于 MJPEG
# 当前视图控制
self.current_view = "front"
self.current_view_lock = threading.Lock()
# 启动摄像头线程
self.camera_threads = []
for cap, model, name in zip(self.caps, self.camera_models, self.names):
thread = threading.Thread(target=self.camera_reader_thread, args=(cap, model, name), daemon=True)
thread.start()
self.camera_threads.append(thread)
# 启动 AI 检测线程
if self.yolo_model is not None:
self.ai_thread = threading.Thread(target=self.ai_detection_thread, daemon=True)
self.ai_thread.start()
else:
self.ai_thread = None
def _initialize_weights(self):
try:
images = [os.path.join(os.getcwd(), "images", name + ".png") for name in self.names]
static_frames = []
for img_path, cam_model in zip(images, self.camera_models):
img = cv2.imread(img_path)
if img is None:
img = np.zeros((1080, 1920, 3), dtype=np.uint8)
img = cam_model.undistort(img)
img = cam_model.project(img)
img = cam_model.flip(img)
static_frames.append(img)
if len(static_frames) == 4:
self.birdview.get_weights_and_masks(static_frames)
print("[INFO] 权重矩阵初始化成功")
except Exception as e:
print(f"[ERROR] 权重初始化失败: {e}")
def camera_reader_thread(self, cap, model, name):
"""摄像头读取 + 去畸变线程"""
while self.running:
ret, frame = cap.read()
if ret:
undistorted = model.undistort(frame)
self.undistorted_buffer.update(name, undistorted)
else:
print(f"[WARN] {name} 摄像头读取失败")
break
def detect_persons(self, image):
if self.yolo_model is None:
return image, [], []
try:
orig_h, orig_w = image.shape[:2]
pad_color = (0, 0, 0)
img_preprocessed = self.co_helper.letter_box(
im=image.copy(),
new_shape=(IMG_SIZE[1], IMG_SIZE[0]),
pad_color=pad_color
)
outputs = self.yolo_model.run([np.expand_dims(img_preprocessed, 0)])
boxes, classes, scores = post_process(outputs, ANCHORS)
if boxes is not None:
real_boxes = self.co_helper.get_real_box(boxes)
person_boxes, person_scores = [], []
for i in range(len(real_boxes)):
if classes[i] < len(CLASSES) and CLASSES[classes[i]] == "person":
box = real_boxes[i].copy()
box = np.clip(box, [0, 0, 0, 0], [orig_w, orig_h, orig_w, orig_h])
person_boxes.append(box)
person_scores.append(scores[i])
if person_boxes:
image = draw_detections(image, np.array(person_boxes), np.array(person_scores), np.zeros(len(person_boxes), dtype=int))
return image, person_boxes, person_scores
else:
return image, [], []
except Exception as e:
print(f"[ERROR] YOLO检测失败: {e}")
return image, [], []
def ai_detection_thread(self):
detection_interval = 3
frame_count = 0
while self.running:
with self.current_view_lock:
view = self.current_view
success, frame = self.undistorted_buffer.get(view)
if success:
frame_count += 1
if frame_count % detection_interval == 0:
img_with_det, boxes, scores = self.detect_persons(frame)
self.detection_buffer.update(view, img_with_det, boxes, scores)
with self.alert_lock:
if boxes:
self.alerts[view] = True
for v in self.alerts:
if v != view:
self.alerts[v] = False
else:
for v in self.alerts:
self.alerts[v] = False
else:
self.detection_buffer.update(view, frame, [], [])
time.sleep(0.001)
def overlay_alert(self, birdview_img):
h, w = birdview_img.shape[:2]
overlay = birdview_img.copy()
alpha = 0.2
red = (0, 0, 200)
margin_f_b = int(min(h, w) * 0.07)
margin_l_r = int(min(h, w) * 0.15)
with self.alert_lock:
alerts = self.alerts.copy()
if alerts["front"]: cv2.rectangle(overlay, (0, 0), (w, margin_f_b), red, -1)
if alerts["back"]: cv2.rectangle(overlay, (0, h - margin_f_b), (w, h), red, -1)
if alerts["left"]: cv2.rectangle(overlay, (0, 0), (margin_l_r, h), red, -1)
if alerts["right"]: cv2.rectangle(overlay, (w - margin_l_r, 0), (w, h), red, -1)
return cv2.addWeighted(birdview_img, 1 - alpha, overlay, alpha, 0)
def run(self):
h_display, w_display = 720, 1280
w_bird = w_display // 3
w_single = w_display - w_bird
while self.running:
# 1. 获取四路去畸变帧 → project → 拼接鸟瞰图
processed_frames = []
for name in self.names:
success, undist_frame = self.undistorted_buffer.get(name)
if success:
# 使用 UMat 加速 warpPerspective
uimg = cv2.UMat(undist_frame)
uresult = cv2.warpPerspective(
uimg,
self.camera_models[self.names.index(name)].project_matrix,
self.camera_models[self.names.index(name)].project_shape,
flags=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT
)
p_frame = uresult.get()
p_frame = self.camera_models[self.names.index(name)].flip(p_frame)
processed_frames.append(p_frame)
else:
processed_frames.append(np.zeros((600, 800, 3), dtype=np.uint8))
self.birdview.update_frames(processed_frames)
self.birdview.stitch_all_parts()
self.birdview.make_white_balance()
self.birdview.copy_car_image()
# 2. 获取当前视图的 AI 检测结果
with self.current_view_lock:
view = self.current_view
det_img, _, _ = self.detection_buffer.get(view)
if det_img is None:
det_img = np.zeros((h_display, w_single, 3), dtype=np.uint8)
# 3. 拼接显示
bird_resized = cv2.resize(self.overlay_alert(self.birdview.image), (w_bird, h_display))
single_resized = cv2.resize(det_img, (w_single, h_display))
display = np.hstack((bird_resized, single_resized))
self.shared_display_buffer.update(display)
cv2.namedWindow('Video', cv2.WND_PROP_FULLSCREEN)
cv2.setWindowProperty('Video', cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
cv2.imshow("Video", display)
key = cv2.waitKey(1) & 0xFF
if key == ord('q'):
self.running = False
elif key == ord('1'):
with self.current_view_lock:
self.current_view = "front"
elif key == ord('2'):
with self.current_view_lock:
self.current_view = "back"
elif key == ord('3'):
with self.current_view_lock:
self.current_view = "left"
elif key == ord('4'):
with self.current_view_lock:
self.current_view = "right"
elif key == ord('0'):
with self.alert_lock:
for k in self.alerts:
self.alerts[k] = False
for cap in self.caps:
cap.release()
cv2.destroyAllWindows()
# ========== 辅助类 ==========
class SharedFrameBuffer:
def __init__(self):
self._frame = None
self._lock = threading.Lock()
self._has_frame = False
def update(self, frame: np.ndarray):
with self._lock:
self._frame = frame.copy()
self._has_frame = True
def get_frame(self):
with self._lock:
if self._has_frame and self._frame is not None:
return True, self._frame.copy()
else:
return False, None
# ========== 主函数 ==========
def main():
print("🚀 启动实时四路环视系统...")
multi_cam = MultiCameraBirdView()
if not multi_cam.running:
print("[ERROR] 摄像头初始化失败")
return
# 启动 MJPEG 流
try:
mjpeg_server = MJPEGServer(multi_cam.shared_display_buffer, host="0.0.0.0", port=8080)
mjpeg_server.start()
print("[INFO] MJPEG 流已启动: http://<IP>:8080")
except Exception as e:
print(f"[WARN] MJPEG 流启动失败: {e}")
multi_cam.run()
if __name__ == "__main__":
main()

View File

@@ -1,40 +0,0 @@
# shared_buffer.py
import threading
import numpy as np
class CameraFrameBuffer:
"""线程安全的摄像头帧缓冲区(去畸变后)"""
def __init__(self, cam_names):
self.frames = {name: None for name in cam_names}
self.locks = {name: threading.Lock() for name in cam_names}
self.updated = {name: False for name in cam_names}
def update(self, name, frame):
with self.locks[name]:
self.frames[name] = frame.copy()
self.updated[name] = True
def get(self, name):
with self.locks[name]:
if self.updated[name] and self.frames[name] is not None:
return True, self.frames[name].copy()
else:
return False, None
class DetectionResultBuffer:
"""线程安全的检测结果缓冲区"""
def __init__(self, cam_names):
self.results = {name: (None, [], []) for name in cam_names} # (image, boxes, scores)
self.lock = threading.Lock()
def update(self, name, image, boxes, scores):
with self.lock:
self.results[name] = (image.copy() if image is not None else None, list(boxes), list(scores))
def get(self, name):
with self.lock:
img, boxes, scores = self.results[name]
if img is not None:
return img.copy(), list(boxes), list(scores)
return None, [], []

View File

@@ -1,91 +0,0 @@
#!/usr/bin/env python3
import cv2
import time
# get the installed camera list for initialization.
def get_cam_lst(cam_lst=range(0, 24)):
arr = []
for iCam in cam_lst:
cap = cv2.VideoCapture(iCam)
if not cap.read()[0]:
continue
else:
arr.append(iCam)
cap.release()
return arr
def show_cam_img(caps, cam_list):
print("INFO: Press 'q' to quit! Press 's' to save a picture, 'n' to change to next camera device!")
idx = 0
while True:
cap_device = caps[idx]
ret, frame = cap_device.read()
if ret:
cv2.imshow('video', frame)
else:
print("ERROR: failed read frame!")
# quit the test
c = cv2.waitKey(1)
if c == ord('q'):
break
# change to next camera device
if c == ord('n'):
idx += 1
if idx >= len(caps):
idx = 0
continue
# save the picture
if c == ord('s'):
if ret:
name = 'video{0}_{1}.png'.format(cam_list[idx],
time.strftime("%Y-%m-%d_%H:%M:%S", time.localtime()))
cv2.imwrite(name, frame)
print("saved file: %s!" %name)
cv2.destroyAllWindows()
def init_caps(cam_list, resolution=(1280,720)):
caps = []
for iCam in cam_list:
cap = cv2.VideoCapture(iCam)
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*"MJPG"))
cap.set(3, resolution[0])
cap.set(4, resolution[1])
caps.append(cap)
return caps
def deinit_caps(cap_list):
for cap in cap_list:
cap.release()
def show_cameras(video_list=None):
if video_list == None:
print("Start to search all available camera devices, please wait... ")
cam_list = get_cam_lst()
err_msg = "cannot find any video device!"
else:
cam_list = get_cam_lst(video_list)
err_msg = "cannot find available video device in list: {0}!".format(video_list) +\
"\nPlease check the video devices in /dev/v4l/by-path/ folder!"
if len(cam_list) < 1:
print("ERROR: " + err_msg)
return
print("Available video device list is {}".format(cam_list))
caps = init_caps(cam_list)
show_cam_img(caps, cam_list)
deinit_caps(caps)
if __name__ == "__main__":
# User can specify the video list here.
#show_cameras([2, 6, 10, 14])
# Or search all available video devices automatically.
show_cameras()

View File

@@ -1,409 +0,0 @@
# client.py - RK3588板端激活程序永久有效
import os
import sys
import json
import hashlib
import hmac
import requests
import subprocess
import time
import logging
import argparse
from datetime import datetime
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# 配置
CONFIG = {
'SERVER_URL': 'http://129.211.170.245:5000/', # 修改为实际服务器地址
'KEY_FILE': '/etc/device.key', # 密钥文件存储路径
'KEY_BACKUP_FILE': '/etc/device.key.bak', # 密钥备份文件
'CHIP_ID_SOURCES': [
'/sys/devices/soc0/serial_number',
'/sys/devices/platform/ff650000.serial/serial0',
'/proc/cpuinfo'
],
'ACTIVATE_URL': '/activate',
'VERIFY_URL': '/verify',
'ACTIVATE_RETRY_COUNT': 3,
'SECRET_KEY': '69588cc5e1b701942cda09b33de5a95c02471f3aaaf610d80aaf5681f4b166e4' # 必须和服务端一致
}
def get_chip_id():
"""
获取RK3588芯片ID增强版
"""
# 方法1: 从多个系统文件读取
for source in CONFIG['CHIP_ID_SOURCES']:
try:
if os.path.exists(source):
with open(source, 'r') as f:
content = f.read().strip()
# 处理cpuinfo的特殊格式
if source == '/proc/cpuinfo':
for line in content.split('\n'):
if 'Serial' in line:
chip_id = line.split(':')[1].strip()
if chip_id:
logger.info(f"{source}获取芯片ID: {chip_id}")
return chip_id
else:
if content:
logger.info(f"{source}获取芯片ID: {content}")
return content
except Exception as e:
logger.warning(f"读取{source}失败: {e}")
# 方法2: 使用dmidecode命令
try:
result = subprocess.run(
['dmidecode', '-s', 'system-serial-number'],
capture_output=True,
text=True,
timeout=5
)
if result.returncode == 0 and result.stdout.strip():
chip_id = result.stdout.strip()
logger.info(f"从dmidecode获取芯片ID: {chip_id}")
return chip_id
except Exception as e:
logger.warning(f"执行dmidecode失败: {e}")
# 方法3: 读取MAC地址
try:
import uuid
mac = uuid.UUID(int=uuid.getnode()).hex[-12:]
chip_id = f"MAC-{mac}"
logger.warning(f"使用MAC地址生成ID: {chip_id}")
return chip_id
except Exception as e:
logger.error(f"无法获取任何标识: {e}")
return None
def verify_key_signature(chip_id, key_data):
"""
验证密钥签名(永久有效)
"""
try:
# 构建待签名数据
data_to_sign = f"{chip_id}|{key_data['activation_id']}|{key_data['activate_time']}"
# 计算期望的签名
expected_signature = hmac.new(
CONFIG['SECRET_KEY'].encode('utf-8'),
data_to_sign.encode('utf-8'),
hashlib.sha256
).hexdigest()
# 比对签名
if key_data['signature'] != expected_signature:
logger.error("密钥签名验证失败")
return False, "密钥签名无效"
logger.info("密钥签名验证通过")
return True, "签名验证通过"
except Exception as e:
logger.error(f"签名验证异常: {e}")
return False, f"验证异常: {str(e)}"
def save_key_file(key_data):
"""
保存密钥文件到本地(永久有效)
"""
try:
# 确保目录存在
key_dir = os.path.dirname(CONFIG['KEY_FILE'])
if key_dir and not os.path.exists(key_dir):
os.makedirs(key_dir, exist_ok=True)
# 如果已有密钥文件,先备份
if os.path.exists(CONFIG['KEY_FILE']):
import shutil
shutil.copy2(CONFIG['KEY_FILE'], CONFIG['KEY_BACKUP_FILE'])
logger.info(f"已备份旧密钥文件到: {CONFIG['KEY_BACKUP_FILE']}")
# 保存新密钥文件
with open(CONFIG['KEY_FILE'], 'w') as f:
json.dump(key_data, f, indent=2)
# 设置文件权限为只读
os.chmod(CONFIG['KEY_FILE'], 0o600)
logger.info(f"密钥文件已保存: {CONFIG['KEY_FILE']}")
return True
except Exception as e:
logger.error(f"保存密钥文件失败: {e}")
return False
def load_key_file():
"""
加载本地密钥文件
"""
try:
# 尝试加载主密钥文件
if os.path.exists(CONFIG['KEY_FILE']):
with open(CONFIG['KEY_FILE'], 'r') as f:
key_data = json.load(f)
logger.info("密钥文件加载成功")
return key_data
# 尝试加载备份密钥文件
if os.path.exists(CONFIG['KEY_BACKUP_FILE']):
logger.warning("主密钥文件不存在,尝试加载备份")
with open(CONFIG['KEY_BACKUP_FILE'], 'r') as f:
key_data = json.load(f)
logger.info("备份密钥文件加载成功")
return key_data
logger.warning("密钥文件不存在")
return None
except Exception as e:
logger.error(f"加载密钥文件失败: {e}")
return None
def verify_local_key(chip_id):
"""
本地验证密钥(永久有效,无过期检查)
"""
# 加载密钥文件
key_data = load_key_file()
if not key_data:
return False, "密钥文件不存在"
# 验证芯片ID是否匹配
if key_data.get('chip_id') != chip_id:
logger.error(f"芯片ID不匹配: 期望 {key_data.get('chip_id')}, 实际 {chip_id}")
return False, "芯片ID不匹配"
# 验证签名
valid, message = verify_key_signature(chip_id, key_data)
if not valid:
return False, message
# 验证通过
logger.info(f"永久激活验证通过!")
logger.info(f"激活时间: {key_data['activate_time']}")
logger.info(f"激活ID: {key_data['activation_id']}")
return True, "验证通过(永久有效)"
def activate_device(chip_id):
"""
联网激活设备(永久激活)
"""
url = f"{CONFIG['SERVER_URL']}{CONFIG['ACTIVATE_URL']}"
payload = {'chip_id': chip_id}
for attempt in range(CONFIG['ACTIVATE_RETRY_COUNT']):
try:
logger.info(f"正在激活设备 (尝试 {attempt + 1}/{CONFIG['ACTIVATE_RETRY_COUNT']})...")
response = requests.post(
url,
json=payload,
timeout=10,
headers={'Content-Type': 'application/json'}
)
if response.status_code == 200:
data = response.json()
if data.get('code') == 200:
# 保存密钥文件
key_data = data['data']
if save_key_file(key_data):
logger.info("设备永久激活成功!")
return True
else:
logger.error("保存密钥文件失败")
else:
logger.error(f"激活失败: {data.get('message')}")
else:
logger.error(f"HTTP错误: {response.status_code}")
except requests.exceptions.RequestException as e:
logger.error(f"网络错误: {e}")
except Exception as e:
logger.error(f"激活过程中发生错误: {e}")
if attempt < CONFIG['ACTIVATE_RETRY_COUNT'] - 1:
wait_time = 2 ** attempt
logger.info(f"等待 {wait_time} 秒后重试...")
time.sleep(wait_time)
return False
def create_emergency_key(chip_id):
"""
创建应急密钥(当无法联网时使用,需人工授权)
警告:此功能仅用于紧急情况
"""
import secrets
logger.warning("正在创建应急密钥...")
# 生成应急密钥数据
emergency_key = {
'chip_id': chip_id,
'activation_id': f"EMERGENCY-{secrets.token_hex(8)}",
'activate_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'signature': 'EMERGENCY-MODE', # 应急模式签名
'permanent': True,
'emergency': True,
'version': '1.0'
}
# 保存应急密钥
if save_key_file(emergency_key):
logger.warning("应急密钥已创建!请确保这是授权操作!")
return True
return False
def main():
"""
主函数:程序启动验证(永久有效)
"""
parser = argparse.ArgumentParser(description='设备永久激活和验证程序')
parser.add_argument('--force-activate', action='store_true', help='强制重新激活')
parser.add_argument('--check-only', action='store_true', help='仅检查验证状态')
parser.add_argument('--emergency', action='store_true', help='创建应急密钥(仅限授权使用)')
parser.add_argument('--show-info', action='store_true', help='显示激活信息')
args = parser.parse_args()
# 获取芯片ID
chip_id = get_chip_id()
if not chip_id:
logger.error("无法获取芯片ID")
sys.exit(1)
logger.info(f"芯片ID: {chip_id}")
# 应急模式
if args.emergency:
if create_emergency_key(chip_id):
logger.info("应急密钥创建成功")
sys.exit(0)
else:
logger.error("应急密钥创建失败")
sys.exit(1)
# 显示激活信息
if args.show_info:
key_data = load_key_file()
if key_data:
print("\n" + "="*50)
print("设备激活信息")
print("="*50)
print(f"芯片ID: {key_data.get('chip_id')}")
print(f"激活时间: {key_data.get('activate_time')}")
print(f"激活ID: {key_data.get('activation_id')}")
print(f"激活类型: {'永久有效' if key_data.get('permanent') else '临时'}")
if key_data.get('emergency'):
print("警告: 应急模式激活")
print("="*50)
else:
print("设备未激活")
sys.exit(0)
# 检查是否需要激活
need_activate = args.force_activate
if not need_activate:
# 尝试本地验证
valid, message = verify_local_key(chip_id)
if valid:
logger.info(f"✅ 验证成功: {message}")
if not args.check_only:
logger.info("验证通过,启动主程序...")
# 在这里调用你的主程序
# subprocess.run(['python3', 'your_main_program.py'])
sys.exit(0)
else:
logger.warning(f"❌ 本地验证失败: {message}")
need_activate = True
# 需要联网激活
if need_activate:
logger.info("开始联网永久激活...")
if activate_device(chip_id):
# 激活后再次验证
valid, message = verify_local_key(chip_id)
if valid:
logger.info(f"✅ 激活验证成功: {message}")
logger.info("设备已永久激活,无需再次联网!")
if not args.check_only:
logger.info("激活完成,启动主程序...")
# subprocess.run(['python3', 'your_main_program.py'])
sys.exit(0)
else:
logger.error(f"❌ 激活后验证失败: {message}")
sys.exit(1)
else:
logger.error("设备激活失败,请检查网络连接")
sys.exit(1)
def install_service():
"""
安装为系统服务
"""
service_content = """[Unit]
Description=Device Permanent Activation Check Service
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/bin/python3 /opt/device_activation/client.py --check-only
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
"""
service_file = '/etc/systemd/system/device-activation.service'
try:
# 复制程序到系统目录
script_dir = '/opt/device_activation'
os.makedirs(script_dir, exist_ok=True)
import shutil
shutil.copy2(__file__, os.path.join(script_dir, 'client.py'))
os.chmod(os.path.join(script_dir, 'client.py'), 0o755)
# 创建服务文件
with open(service_file, 'w') as f:
f.write(service_content)
os.chmod(service_file, 0o644)
logger.info(f"服务文件已创建: {service_file}")
logger.info("请执行以下命令启用服务:")
logger.info("sudo systemctl daemon-reload")
logger.info("sudo systemctl enable device-activation.service")
logger.info("sudo systemctl start device-activation.service")
except Exception as e:
logger.error(f"安装服务失败: {e}")
if __name__ == '__main__':
if len(sys.argv) > 1 and sys.argv[1] == 'install-service':
install_service()
else:
main()

View File

@@ -1,925 +0,0 @@
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()

Binary file not shown.