fix:精简工程
This commit is contained in:
199
1.py
199
1.py
@@ -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
474
2.py
@@ -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()
|
||||
BIN
LJ360.tar.gz
BIN
LJ360.tar.gz
Binary file not shown.
3
camera_manager.sh → bash/camera_manager.sh
Executable file → Normal file
3
camera_manager.sh → bash/camera_manager.sh
Executable file → Normal file
@@ -40,8 +40,7 @@ WorkingDirectory=$WORK_DIR
|
||||
ExecStart=/bin/bash $SCRIPT_PATH run
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StandardOutput=append:$LOG_FILE
|
||||
StandardError=append:$LOG_FILE
|
||||
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -13,13 +13,35 @@ PYTHON_SCRIPT="web.py"
|
||||
USER="ztl"
|
||||
|
||||
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"
|
||||
# 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() {
|
||||
# 确保 logrotate 配置存在
|
||||
setup_logrotate
|
||||
|
||||
cd "$WORK_DIR" || exit 1
|
||||
echo "$(date): 启动 $APP_NAME 应用..." >> "$LOG_FILE"
|
||||
while true; do
|
||||
@@ -28,11 +50,16 @@ start_app() {
|
||||
else
|
||||
echo "$(date): 程序异常退出(代码 $?),5秒后重启..." >> "$LOG_FILE"
|
||||
fi
|
||||
|
||||
# 强制运行 logrotate 检查大小
|
||||
# 注意:虽然 logrotate 通常由 cron 每天运行,但这里手动调用以确保实时性
|
||||
logrotate -f "$LOGROTATE_CONF" 2>/dev/null
|
||||
|
||||
sleep 5
|
||||
done
|
||||
}
|
||||
|
||||
# 创建 systemd 服务文件
|
||||
# 3. 创建 systemd 服务文件
|
||||
create_systemd_service() {
|
||||
cat > "$SYSTEMD_SERVICE_FILE" <<EOF
|
||||
[Unit]
|
||||
@@ -61,7 +88,7 @@ EOF
|
||||
echo "✅ systemd 服务已创建: $APP_NAME"
|
||||
}
|
||||
|
||||
# 启用开机自启
|
||||
# 4. 启用开机自启
|
||||
enable_autostart() {
|
||||
# 确保 ztl 用户有串口和摄像头权限
|
||||
usermod -aG dialout ztl 2>/dev/null
|
||||
@@ -77,16 +104,17 @@ enable_autostart() {
|
||||
echo "✅ 已启用开机自启,并以 ztl 用户启动服务"
|
||||
}
|
||||
|
||||
# 禁用开机自启
|
||||
# 5. 禁用开机自启
|
||||
disable_autostart() {
|
||||
systemctl stop "$APP_NAME".service 2>/dev/null
|
||||
systemctl disable "$APP_NAME".service 2>/dev/null
|
||||
rm -f "$SYSTEMD_SERVICE_FILE"
|
||||
rm -f "$LOGROTATE_CONF" # 同时删除 logrotate 配置
|
||||
systemctl daemon-reload
|
||||
echo "✅ 已禁用开机自启,并移除服务"
|
||||
}
|
||||
|
||||
# 显示状态
|
||||
# 6. 显示状态
|
||||
show_status() {
|
||||
if systemctl is-active --quiet "$APP_NAME".service; then
|
||||
echo "🟢 服务正在运行(开机自启已启用)"
|
||||
@@ -98,7 +126,7 @@ show_status() {
|
||||
fi
|
||||
}
|
||||
|
||||
# 主菜单
|
||||
# 7. 主菜单
|
||||
show_menu() {
|
||||
echo "========================================"
|
||||
echo " LJ360 摄像头系统管理工具"
|
||||
|
||||
995
demo1.py
995
demo1.py
@@ -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.
BIN
lib/librknnrt.so
BIN
lib/librknnrt.so
Binary file not shown.
Binary file not shown.
583861
lj360_camera_keepalive.log
583861
lj360_camera_keepalive.log
File diff suppressed because it is too large
Load Diff
731
lj360_manager.sh
731
lj360_manager.sh
@@ -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): 已启动 LJ360,PID: \$!" >> "\$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
|
||||
@@ -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
|
||||
BIN
model/bus.jpg
BIN
model/bus.jpg
Binary file not shown.
|
Before Width: | Height: | Size: 177 KiB |
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
wget -O ./yolov5s_relu.onnx https://ftrg.zbox.filez.com/v2/delivery/data/95f00b0fc900458ba134f8b180b3f7a1/examples/yolov5/yolov5s_relu.onnx
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
403
saveimg.py
403
saveimg.py
@@ -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
|
||||
|
||||
# 启用 OpenCL(Mali-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()
|
||||
@@ -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, [], []
|
||||
@@ -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()
|
||||
409
tools/cpu_id.py
409
tools/cpu_id.py
@@ -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()
|
||||
925
web_back.py
925
web_back.py
@@ -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.
Reference in New Issue
Block a user