拍视频版本

This commit is contained in:
2026-04-01 14:11:47 +08:00
commit a7a67296e3
82 changed files with 591269 additions and 0 deletions

199
1.py Normal file
View File

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

12
1.sh Normal file
View File

@@ -0,0 +1,12 @@
#!/bin/bash
sudo gst-launch-1.0 \
glvideomixer name=mix \
sink_0::xpos=0 sink_0::ypos=0 sink_0::width=960 sink_0::height=540 \
sink_1::xpos=960 sink_1::ypos=0 sink_1::width=960 sink_1::height=540 \
sink_2::xpos=0 sink_2::ypos=540 sink_2::width=960 sink_2::height=540 \
sink_3::xpos=960 sink_3::ypos=540 sink_3::width=960 sink_3::height=540 ! \
fpsdisplaysink video-sink=glimagesink sync=false \
v4l2src device=/dev/video0 ! video/x-raw,format=NV12,width=1280,height=720 ! glupload ! mix.sink_0 \
v4l2src device=/dev/video1 ! video/x-raw,format=NV12,width=1280,height=720 ! glupload ! mix.sink_1 \
v4l2src device=/dev/video2 ! video/x-raw,format=NV12,width=1280,height=720 ! glupload ! mix.sink_2 \
v4l2src device=/dev/video3 ! video/x-raw,format=NV12,width=1280,height=720 ! glupload ! mix.sink_3

474
2.py Normal file
View File

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

1571
3.py Normal file

File diff suppressed because it is too large Load Diff

BIN
LJ360.tar.gz Normal file

Binary file not shown.

206
README.md Normal file
View File

@@ -0,0 +1,206 @@
# LJ360 - 360全景人员接近预警系统
LJ360是一个基于OpenCV开发的360度全景人员接近预警系统支持多摄像头拼接、雷达距离检测和人体检测、web配置等功能。
## 更新
- 2025-12-25: CPP版本360环视成功运行
- 2025-12-23: 增加Flask框架支持WEB预览
- 2025-12-20: 增加YOLOv5模型支持人员检测
- 2025-12-19: Python版本360环视成功运行
- 2025-12-16: 单路摄像头投影生成
- 2025-12-15: 加载V4L2后端 移除gstreamer
- 2025-12-12模拟摄像头内参矩阵和畸变系数计算
---
- 2025-12-02: 本地视频360合成
- 2025-11-03YOLO融合 雷达预警
- 2025-11-01网络摄像头四分屏
- 2025-10-30初始版本发布
## 功能
- **多摄像头拼接**支持4个鱼眼摄像头实时拼接成360度全景视图
- **图像校正**:自动对鱼眼镜头的畸变进行校正
- **雷达检测**:支持串口雷达距离检测,实时显示障碍物距离
- **人体检测**集成YOLO目标检测算法实时检测行人
- **Web界面**提供Web界面查看全景视频流
- **硬件加速**支持RKNN神经网络加速和OpenCL GPU加速
## 系统要求
- Python 3.8
- OpenCV 4.0+
- NumPy
- RKNN Toolkit Lite (用于神经网络加速)
- Serial (用于雷达通信)
- Flask (用于Web界面)
## 硬件要求
- 4个鱼眼摄像头 (1920*1080@25fps)
- 4路AHD转MIPI TP2815接口板
- 摄像头映射关系video0(前)、video1(左)、video2(后)、video3(右)
- 雷达模块 (支持串口通信)
- RK3588平台 (用于RKNN加速和系统主控)
## 安装步骤
1. 克隆项目代码:
```bash
git clone <repository_url>
cd LJ360
```
2. 安装依赖:
```bash
NULL
```
3. 配置摄像头参数:
-`yaml/`目录下为每个摄像头创建配置文件
- 配置文件包含摄像头的内参、外参和畸变参数
4. 配置雷达参数:
-`web.py`中修改雷达串口配置
- 调整距离阈值参数
## 使用方法
### 1. 主要运行文件 (推荐)
```bash
python3 web.py
```
该脚本是系统的主要运行文件,同时实现:
- 4个摄像头的实时图像捕获
- 鱼眼图像校正和拼接
- 360度全景视图生成
- Web服务器启动提供全景视频流访问
启动后,在浏览器中访问`http://localhost:5000`查看全景视频流。
显示器同步显示360度全景视图和YOLO检测结果。
### 2. 运行摄像头校准
```bash
python run_calibrate_camera.py
```
用于校准摄像头参数生成yaml配置文件。
### 3. 生成投影映射
```bash
python run_get_projection_maps.py
```
生成图像投影映射,用于提高拼接效率。
### 4. 生成权重矩阵
```bash
python run_get_weight_matrices.py
```
生成图像拼接的权重矩阵,用于平滑过渡。
## 项目结构
```
LJ360/
├── cpp/ # C++实现的相关功能 开发中
│ └── AdasSourrondView/ # 360度全景视图C++实现
├── py_utils/ # Python工具模块
│ ├── rknn_executor.py # RKNN模型执行器
│ └── coco_utils.py # COCO数据集工具
├── surround_view/ # 全景视图核心模块
│ ├── birdview.py # 鸟瞰图生成
│ ├── fisheye_camera.py # 鱼眼相机模型
│ ├── capture_thread.py # 图像捕获线程
│ └── process_thread.py # 图像处理线程
├── yaml/ # 摄像头配置文件
├── model/ # 深度学习模型
├── images/ # 静态图像资源
├── web.py # 主程序
├── run_live_demo.py # 实时演示脚本
├── run_calibrate_camera.py # 摄像头校准脚本
├── run_get_projection_maps.py # 生成投影映射脚本
├── run_get_weight_matrices.py # 生成权重矩阵脚本
└── README.md # 项目说明文档
```
## 配置文件说明
### 摄像头配置文件 (yaml/*.yaml)
每个摄像头需要一个yaml配置文件包含以下参数
- `camera_matrix`: 相机内参矩阵
- `dist_coeffs`: 畸变系数
- `rotation_vector`: 旋转向量
- `translation_vector`: 平移向量
- `projection_matrix`: 投影矩阵
### 雷达配置
`web.py`中配置雷达参数:
- `RADAR_SERIAL_PORT`: 雷达串口
- `RADAR_BAUDRATE`: 波特率
- `DISTANCE_THRESHOLD`: 距离阈值
### YOLO配置
`web.py`中配置YOLO目标检测参数
- `YOLO_MODEL_PATH`: YOLO模型路径
- `OBJ_THRESH`: 目标检测阈值
- `NMS_THRESH`: 非极大值抑制阈值
- `IMG_SIZE`: 输入图像大小
## 注意事项
1. 确保所有摄像头的分辨率设置一致
2. 摄像头校准需要使用棋盘格进行
3. 雷达模块需要正确连接到串口
4. RKNN模型需要使用RKNN Toolkit转换生成
5. 运行前请确保所有依赖已正确安装
## 故障排除
### 摄像头无法连接
- 检查摄像头ID是否正确
- 确保摄驱动框架V4L2已安装
- 检查MIPI接口连接
### 图像拼接异常
- 检查摄像头校准参数
- 确保所有摄像头的视角重叠区域足够
- 调整权重矩阵参数
### 雷达无数据
- 检查串口连接
- 检查雷达波特率设置
- 确保雷达模块正常工作
## 更新日志
### v1.0.0
- 初始版本发布
- 支持4摄像头拼接
- 集成雷达距离检测
- 支持人体检测
- 提供Web界面
## 许可证
本项目采用MIT许可证详见LICENSE文件。
## 联系方式
如有问题或建议,请联系项目维护者。

Binary file not shown.

162
bash/disk_tool.sh Normal file
View File

@@ -0,0 +1,162 @@
#!/bin/bash
# ================= 配置区 =================
# 注意:这里不再硬编码 /dev/sda1脚本会自动检测第一个非系统盘的 SATA/USB 设备
TARGET_LABEL=""
MOUNT_POINT="/videos"
FSTAB_FILE="/etc/fstab"
# ==========================================
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 1. 检查 Root 权限
if [ "$EUID" -ne 0 ]; then
echo -e "${RED}❌ 权限不足!请使用 sudo 运行此脚本${NC}"
exit 1
fi
# 2. 自动检测硬盘设备 (智能查找)
# 逻辑:查找名为 sda1, sdb1, sdc1 等且不是根分区的设备
detect_device() {
# 尝试找 sda1
if [ -b "/dev/sda1" ] && [ "$(df /dev/sda1 2>/dev/null | tail -1 | awk '{print $NF}')" != "/" ]; then
echo "/dev/sda1"
return
fi
# 尝试找 sdb1
if [ -b "/dev/sdb1" ]; then
echo "/dev/sdb1"
return
fi
# 尝试找 mmcblk 以外的第一个大容量分区 (针对更复杂情况)
lsblk -ndpo NAME,TYPE,SIZE,MOUNTPOINT | grep -E "part|disk" | grep -v "mmcblk" | head -n 1 | awk '{print $1}'
}
DEVICE=$(detect_device)
if [ -z "$DEVICE" ]; then
echo -e "${RED}❌ 未检测到可用的外接硬盘设备 (sda1/sdb1 等)。请检查硬件连接。${NC}"
exit 1
fi
# 3. 获取 UUID (核心步骤)
UUID=$(blkid -s UUID -o value "$DEVICE" 2>/dev/null)
if [ -z "$UUID" ]; then
echo -e "${RED}❌ 无法获取设备 $DEVICE 的 UUID。可能文件系统损坏或未格式化。${NC}"
echo "提示:尝试执行 'sudo mkfs.ext4 $DEVICE' 进行格式化 (数据会丢失!)"
exit 1
fi
# 获取文件系统类型
FS_TYPE=$(blkid -s TYPE -o value "$DEVICE" 2>/dev/null)
echo -e "${GREEN}✅ 检测到设备:${NC} $DEVICE"
echo -e "${GREEN}✅ 文件系统:${NC} $FS_TYPE"
echo -e "${GREEN}✅ UUID:${NC} $UUID"
echo ""
# 4. 确保挂载点目录存在
if [ ! -d "$MOUNT_POINT" ]; then
echo "正在创建挂载点: $MOUNT_POINT"
mkdir -p "$MOUNT_POINT"
chmod 777 "$MOUNT_POINT"
fi
# 5. 菜单界面
echo "=========================================="
echo " RK3588 硬盘挂载工具"
echo "=========================================="
echo "目标设备: $DEVICE ($FS_TYPE)"
echo "设备 UUID: $UUID"
echo "挂载目录: $MOUNT_POINT"
echo "------------------------------------------"
echo " 1) 立即挂载硬盘 (Mount Now)"
echo " 2) 立即取消挂载 (Unmount Now)"
echo " 3) 开启开机自动挂载 (Enable Auto-Mount via UUID)"
echo " 4) 取消开机自动挂载 (Disable Auto-Mount)"
echo " 5) 查看当前磁盘状态 (Status)"
echo " 6) 退出 (Exit)"
echo "=========================================="
read -p "请输入选项 [1-6]: " choice
case $choice in
1)
echo "正在挂载 UUID=$UUID$MOUNT_POINT ..."
# 使用 UUID 挂载命令
mount -U "$UUID" "$MOUNT_POINT"
if [ $? -eq 0 ]; then
echo -e "${GREEN}✅ 挂载成功!${NC}"
df -h | grep "$MOUNT_POINT"
else
echo -e "${RED}❌ 挂载失败!${NC}"
echo "可能原因:文件系统类型不支持或需要 fsck 修复。"
echo "尝试手动修复sudo fsck -y $DEVICE"
fi
;;
2)
echo "正在卸载 $MOUNT_POINT ..."
umount "$MOUNT_POINT"
if [ $? -eq 0 ]; then
echo -e "${GREEN}✅ 卸载成功!${NC}"
else
echo -e "${RED}❌ 卸载失败!${NC} 可能有程序正在使用该目录 (如日志写入、视频录制)。"
echo "提示:使用 'lsof +D $MOUNT_POINT' 查看占用进程。"
fi
;;
3)
# 检查 fstab 中是否已经存在该 UUID 的记录
if grep -q "$UUID" "$FSTAB_FILE"; then
echo -e "${YELLOW}⚠️ 开机自动挂载已存在 (基于 UUID),无需重复设置。${NC}"
else
# 构造 fstab 行UUID=xxxx /videos auto defaults,nofail,x-systemd.device-timeout=30 0 2
FSTAB_ENTRY="UUID=$UUID $MOUNT_POINT $FS_TYPE defaults,nofail,x-systemd.device-timeout=30 0 2"
echo "即将写入配置:"
echo " $FSTAB_ENTRY"
read -p "确认写入?(y/n): " confirm
if [ "$confirm" == "y" ] || [ "$confirm" == "Y" ]; then
echo "$FSTAB_ENTRY" >> "$FSTAB_FILE"
echo -e "${GREEN}✅ 开机自动挂载已启用!${NC}"
# 尝试立即应用配置测试
echo "正在测试配置有效性..."
umount "$MOUNT_POINT" 2>/dev/null
if mount -a; then
echo -e "${GREEN}✅ 配置测试通过mount -a 执行成功。${NC}"
else
echo -e "${RED}⚠️ 警告mount -a 测试失败,请检查 /etc/fstab 格式。${NC}"
fi
else
echo "操作已取消。"
fi
fi
;;
4)
# 删除包含该 UUID 的行
if grep -q "$UUID" "$FSTAB_FILE"; then
sed -i "/$UUID/d" "$FSTAB_FILE"
echo -e "${GREEN}✅ 开机自动挂载已清理!${NC}"
else
echo "未找到该 UUID 的配置记录。"
fi
;;
5)
echo "--- 当前磁盘使用情况 ---"
df -hT | grep -E "Filesystem|$MOUNT_POINT|/dev/sd"
echo ""
echo "--- /etc/fstab 内容预览 ---"
tail -n 5 "$FSTAB_FILE"
;;
6)
echo "退出脚本。"
exit 0
;;
*)
echo -e "${RED}❌ 错误:输入无效。${NC}"
;;
esac

155
bash/video_tool.sh Normal file
View File

@@ -0,0 +1,155 @@
#!/bin/bash
# 自动以 root 权限运行
if [ "$EUID" -ne 0 ]; then
echo "需要 root 权限,正在提权..."
exec sudo bash "$0" "$@"
fi
# 配置区
APP_NAME="lj360_camera"
WORK_DIR="/home/ztl/LJ360"
PYTHON_SCRIPT="web.py"
USER="ztl"
SYSTEMD_SERVICE_FILE="/etc/systemd/system/${APP_NAME}.service"
SCRIPT_PATH="$WORK_DIR/camera_manager.sh"
# 日志文件(保活日志)
LOG_FILE="$WORK_DIR/${APP_NAME}_keepalive.log"
# 启动主程序(带保活循环)
start_app() {
cd "$WORK_DIR" || exit 1
echo "$(date): 启动 $APP_NAME 应用..." >> "$LOG_FILE"
while true; do
if sudo -u ztl python3 "$PYTHON_SCRIPT"; then
echo "$(date): 程序正常退出,即将重启..." >> "$LOG_FILE"
else
echo "$(date): 程序异常退出(代码 $?5秒后重启..." >> "$LOG_FILE"
fi
sleep 5
done
}
# 创建 systemd 服务文件
create_systemd_service() {
cat > "$SYSTEMD_SERVICE_FILE" <<EOF
[Unit]
Description=LJ360 Four-Camera BirdView System
After=multi-user.target
[Service]
Type=simple
User=$USER
Group=$USER
WorkingDirectory=$WORK_DIR
ExecStart=/bin/bash $SCRIPT_PATH run
Restart=always
RestartSec=5
StandardOutput=append:$LOG_FILE
StandardError=append:$LOG_FILE
Environment=HOME=/home/ztl
Environment=DISPLAY=:0
[Install]
WantedBy=multi-user.target
EOF
chmod 644 "$SYSTEMD_SERVICE_FILE"
systemctl daemon-reload
echo "✅ systemd 服务已创建: $APP_NAME"
}
# 启用开机自启
enable_autostart() {
# 确保 ztl 用户有串口和摄像头权限
usermod -aG dialout ztl 2>/dev/null
usermod -aG video ztl 2>/dev/null
# 确保日志文件归属正确
touch "$LOG_FILE"
chown ztl:ztl "$LOG_FILE"
create_systemd_service
systemctl enable "$APP_NAME".service
systemctl start "$APP_NAME".service
echo "✅ 已启用开机自启,并以 ztl 用户启动服务"
}
# 禁用开机自启
disable_autostart() {
systemctl stop "$APP_NAME".service 2>/dev/null
systemctl disable "$APP_NAME".service 2>/dev/null
rm -f "$SYSTEMD_SERVICE_FILE"
systemctl daemon-reload
echo "✅ 已禁用开机自启,并移除服务"
}
# 显示状态
show_status() {
if systemctl is-active --quiet "$APP_NAME".service; then
echo "🟢 服务正在运行(开机自启已启用)"
systemctl status "$APP_NAME".service --no-pager
elif [ -f "$SYSTEMD_SERVICE_FILE" ]; then
echo "🟠 服务已安装但未运行"
else
echo "🔴 服务未安装(开机自启已关闭)"
fi
}
# 主菜单
show_menu() {
echo "========================================"
echo " LJ360 摄像头系统管理工具"
echo "========================================"
show_status
echo ""
echo "请选择操作:"
echo " 1) 启用开机自启并启动服务ztl 用户)"
echo " 2) 禁用开机自启并停止服务"
echo " 3) 仅手动运行一次(不保活)"
echo " 4) 查看日志"
echo " 0) 退出"
echo -n "请输入编号 (0-4): "
}
# 处理命令行参数(用于 systemd 调用)
if [ "$1" = "run" ]; then
start_app
exit 0
fi
# 交互式主程序
while true; do
show_menu
read -r choice
case $choice in
1)
enable_autostart
;;
2)
disable_autostart
;;
3)
echo "🚀 手动运行一次Ctrl+C 可退出)..."
cd "$WORK_DIR" && sudo -u ztl python3 "$PYTHON_SCRIPT"
;;
4)
echo "📄 最近日志(最后 20 行):"
tail -n 20 "$LOG_FILE"
echo -n "按回车返回菜单..."
read -r
;;
0)
echo "退出。"
exit 0
;;
*)
echo "❌ 无效选项,请输入 0-4"
sleep 1
;;
esac
echo ""
done

213
bash/ztlOut_rootfs.sh Normal file
View File

@@ -0,0 +1,213 @@
#!/bin/bash
function showhelp()
{
echo -e "
ff_export_rootfs </path/to/store> [-t <ext4|btrfs>]
"
exit -1
}
function prepare_rootfs()
{
systemctl enable resize-helper.service > /dev/null 2>&1
apt-get clean -y
}
function clead_target_rootfs()
{
ROOT_DIR=$1
rm -rf ${ROOT_DIR}/usr/share/doc/*
rm -rf ${ROOT_DIR}/var/lib/apt/lists/*
rm -rf ${ROOT_DIR}/tmp/*
find ${ROOT_DIR}/usr/share/man/ -name "*.gz" -exec rm {} \;
find ${ROOT_DIR}/var/log/ -type f -name "*.gz" -exec rm {} \;
find ${ROOT_DIR}/var/log/ -type f -exec truncate -s 0 {} \;
for item in $(ls ${ROOT_DIR}/media/ 2>/dev/null); do
if id ${item} > /dev/null 2>&1
then
chown -R ${item}:${item} ${ROOT_DIR}/media/${item}
fi
done
if [[ -L "${ROOT_DIR}/var/lib/docker" ]]
then
orig_dir=$(readlink "${ROOT_DIR}/var/lib/docker")
if [[ ! -d ${ROOT_DIR}/${orig_dir} ]]
then
rm -rf "${ROOT_DIR}/var/lib/docker"
rsync -aqx "${orig_dir}/" "${ROOT_DIR}/var/lib/docker"
fi
fi
}
function umount_img() {
if mountpoint -q $TEMP_MOUNT_POINT; then
umount $TEMP_MOUNT_POINT
fi
rm -rf $TEMP_MOUNT_POINT
}
function finish() {
umount_img
exit -1
}
[[ $UID -ne 0 ]] && echo -e "\033[31m should run as root \033[0m" && showhelp
[[ -n "$(which rsync)" ]] || { echo -e " rsync not found\n\033[31m apt install rsync \033[0m"; exit -1; }
[[ $# -lt 1 ]] && showhelp;
[[ "$1" == "-u" ]] && { PORT_UPPER="1"; shift; }
DEST_PATH=$1
MEDIA_DEVICE=
STORE_FS_TYPE=
[[ -d $DEST_PATH ]] || { echo -e "\033[31m store path not exist \033[0m"; exit -1; }
if [[ $# -eq 3 ]] && [[ $2 == "-t" ]]; then
case $3 in
ext4|btrfs)
STORE_FS_TYPE=$3
;;
*)
showhelp
;;
esac
fi
PORT_OVERLAYROOT=false
ROOTFS_DEVICE=`findmnt -n -o SOURCE --target /`
if [[ "${ROOTFS_DEVICE}" == "overlayroot" ]]; then
PORT_OVERLAYROOT=true
OVERLAY_MOUNT=$(mount | sed -n '/^overlayroot/p')
UPPER_MOUNTPOINT=$(echo ${OVERLAY_MOUNT} | grep -Eo "upperdir.*" | cut -f 1 -d , | cut -f 2 -d =)
LOWER_MOUNTPOINT=$(echo ${OVERLAY_MOUNT} | grep -Eo "lowerdir.*" | cut -f 1 -d , | cut -f 2 -d =)
LOWER_DEVICE=$(findmnt -n -o SOURCE --target ${LOWER_MOUNTPOINT})
UPPER_DEVICE=$(findmnt -n -o SOURCE --target ${UPPER_MOUNTPOINT})
if [[ "${PORT_UPPER}" == "1" ]]; then
PORT_OVERLAYROOT=false
ROOTFS_DEVICE=${UPPER_DEVICE}
ROOTFS_MOUNTPOINT=${UPPER_MOUNTPOINT}/
ROOTFS_TYPE=$(blkid -o value -s TYPE ${ROOTFS_DEVICE})
ROOTFS_PARTLABEL=$(lsblk -n -o PARTLABEL ${ROOTFS_DEVICE})
[[ -n "${ROOTFS_PARTLABEL}" ]] || ROOTFS_PARTLABEL="userdata"
else
ROOTFS_MOUNTPOINT="/"
ROOTFS_TYPE=$(blkid -o value -s TYPE ${LOWER_DEVICE})
ROOTFS_PARTLABEL=$(lsblk -n -o PARTLABEL ${LOWER_DEVICE})
[[ -n "${ROOTFS_PARTLABEL}" ]] || ROOTFS_PARTLABEL="rootfs"
fi
else
if [[ "${ROOTFS_DEVICE}" == "/dev/root" ]]; then
MAJOR_ROOTFS=$(mountpoint -d / | cut -f 1 -d ":")
MINOR_ROOTFS=$(mountpoint -d / | cut -f 2 -d ":")
DEV_ROOTFS=$(cat /proc/partitions | awk {'if ($1 == "'${MAJOR_ROOTFS}'" && $2 == "'${MINOR_ROOTFS}'") print $4 '})
ROOTFS_DEVICE=/dev/${DEV_ROOTFS}
fi
PORT_OVERLAYROOT=false
ROOTFS_MOUNTPOINT="/"
ROOTFS_TYPE=$(blkid -o value -s TYPE ${ROOTFS_DEVICE})
ROOTFS_PARTLABEL=$(lsblk -n -o PARTLABEL ${ROOTFS_DEVICE})
[[ -n "${ROOTFS_PARTLABEL}" ]] || ROOTFS_PARTLABEL="rootfs"
fi
#MEDIA_DEVICE=`findmnt -n -o SOURCE --target $DEST_PATH`
MEDIA_FS_TYPE=`df $DEST_PATH --output=fstype | awk 'NR==2 {print $1}'`
MEDIA_FREE_SPACE=`df $DEST_PATH -m --output=avail | awk 'NR==2 {print $1}'`
#[[ $MEDIA_DEVICE != $ROOTFS_DEVICE ]] || { echo -e "\033[31m can not store in local device \033[0m"; exit -1; }
#[[ $MEDIA_FS_TYPE != "vfat" ]] || { echo -e "\033[31m store media fs type cannot be FAT \033[0m"; exit -1; }
prepare_rootfs
if [[ $ROOTFS_TYPE == "btrfs" ]]; then
ROOTFS_SIZE=`btrfs filesystem usage -m / | grep "Device allocated" | awk '{print $3}'`
else
if [[ "${PORT_OVERLAYROOT}" == "true" ]]; then
LOWER_SIZE=$(du -s -k "${LOWER_MOUNTPOINT}" |sed -r -e 's/[[:space:]]+.*$//')
UPPER_SIZE=$(du -s -k "${UPPER_MOUNTPOINT}" |sed -r -e 's/[[:space:]]+.*$//')
DOCKER_SIZE=0
if [[ -L "/var/lib/docker" ]]
then
DOCKER_FINAL_DIR=$(readlink "/var/lib/docker")
if [[ -d ${DOCKER_FINAL_DIR} ]]
then
DOCKER_SIZE=$(du -s -k "${DOCKER_FINAL_DIR}" |sed -r -e 's/[[:space:]]+.*$//')
fi
fi
ROOTFS_SIZE=$((LOWER_SIZE+UPPER_SIZE+DOCKER_SIZE))
else
ROOTFS_SIZE=`df ${ROOTFS_MOUNTPOINT} -k --output=used | awk 'NR==2 {print $1}'`
fi
fi
IMAGE_SIZE=$((ROOTFS_SIZE>>10))
IMAGE_SIZE=$((IMAGE_SIZE+IMAGE_SIZE/10+300))
echo -e "MEDIA FREE SPACE SIZE \t $MEDIA_FREE_SPACE \t MBytes"
echo -e "EXPORT IMAGE SIZE \t $IMAGE_SIZE \t MBytes"
if [[ $MEDIA_FREE_SPACE -lt $IMAGE_SIZE ]]; then
echo -e "\033[31m No enough free space on $MOUNT_POINT \033[0m"
exit -1
fi
[[ $STORE_FS_TYPE != "" ]] || STORE_FS_TYPE=$ROOTFS_TYPE
CURDATE=`date "+%Y%m%d%H%M"`
OS_D=`lsb_release -d -s`
IMAGE_FILE=$DEST_PATH/${OS_D// /}_ztl_${STORE_FS_TYPE}_$CURDATE.img
TEMP_MOUNT_POINT=`mktemp -d -p $DEST_PATH`
set -e
trap finish ERR INT
if [[ $STORE_FS_TYPE == "btrfs" ]]; then
truncate -s ${IMAGE_SIZE}M $IMAGE_FILE
mkfs.btrfs -fq -L $ROOTFS_PARTLABEL $IMAGE_FILE
mount -t btrfs -o noatime,compress=lzo $IMAGE_FILE $TEMP_MOUNT_POINT
btrfs subvolume create $TEMP_MOUNT_POINT/root
umount $TEMP_MOUNT_POINT
mount -t btrfs -o noatime,compress=lzo,subvol=root $IMAGE_FILE $TEMP_MOUNT_POINT
else
INODE_COUNT=$(find "${ROOTFS_MOUNTPOINT}" 2>/dev/null | wc -l)
INODE_COUNT=$((INODE_COUNT+512))
BLOCK_COUNT=$((2000+(ROOTFS_SIZE+INODE_COUNT/4)*12/10))
echo BLOCK_COUNT $BLOCK_COUNT
echo INODE_COUNT $INODE_COUNT
mkfs.ext4 -Fq -L $ROOTFS_PARTLABEL -b 1024 -I 128 -N $INODE_COUNT $IMAGE_FILE $BLOCK_COUNT
mount $IMAGE_FILE $TEMP_MOUNT_POINT
fi
echo "sync..."
rsync -aqx --delete --exclude={"$IMAGE_FILE","$TEMP_MOUNT_POINT"} ${ROOTFS_MOUNTPOINT} ${TEMP_MOUNT_POINT}
echo "sync finish"
set +e
trap - ERR
clead_target_rootfs ${TEMP_MOUNT_POINT}
while IFS= read -r LINE
do
[ "${LINE}" == "/" ] && continue
E_FILE="${TEMP_MOUNT_POINT}/${LINE}"
[[ -e "${E_FILE}" ]] && rm -rf "${E_FILE}"
done < <(findmnt -n -lo TARGET -t ext4,ext3,ext2,vat)
umount_img
if [[ "${STORE_FS_TYPE}" == "ext4" ]]; then
e2fsck -fy ${IMAGE_FILE} 2>&1 > /dev/null
tune2fs -C 1 -c 0 -i 0 -e "remount-ro" ${IMAGE_FILE} 2>&1 > /dev/null
fi
echo -e "\033[31m Export rootfs to $IMAGE_FILE Success \033[0m"

138
camera_manager.sh Executable file
View File

@@ -0,0 +1,138 @@
#!/bin/bash
# 配置区
APP_NAME="lj360_camera"
WORK_DIR="/home/ztl/LJ360"
PYTHON_SCRIPT="web.py"
USER="ztl" # 替换为实际用户名(当前是 root但建议用普通用户
SYSTEMD_SERVICE_FILE="/etc/systemd/system/${APP_NAME}.service"
SCRIPT_PATH="$WORK_DIR/camera_manager.sh"
# 日志文件(保活日志)
LOG_FILE="$WORK_DIR/${APP_NAME}_keepalive.log"
# 启动主程序(带保活循环)
start_app() {
cd "$WORK_DIR" || exit 1
echo "$(date): 启动 $APP_NAME 应用..." >> "$LOG_FILE"
while true; do
if python3 "$PYTHON_SCRIPT"; then
echo "$(date): 程序正常退出,即将重启..." >> "$LOG_FILE"
else
echo "$(date): 程序异常退出(代码 $?5秒后重启..." >> "$LOG_FILE"
fi
sleep 5
done
}
# 创建 systemd 服务文件
create_systemd_service() {
cat > "$SYSTEMD_SERVICE_FILE" <<EOF
[Unit]
Description=LJ360 Four-Camera BirdView System
After=multi-user.target
[Service]
Type=simple
User=$USER
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
EOF
chmod 644 "$SYSTEMD_SERVICE_FILE"
systemctl daemon-reload
echo "✅ systemd 服务已创建: $APP_NAME"
}
# 启用开机自启
enable_autostart() {
create_systemd_service
systemctl enable "$APP_NAME".service
systemctl start "$APP_NAME".service
echo "✅ 已启用开机自启,并启动服务"
}
# 禁用开机自启
disable_autostart() {
systemctl stop "$APP_NAME".service 2>/dev/null
systemctl disable "$APP_NAME".service 2>/dev/null
rm -f "$SYSTEMD_SERVICE_FILE"
systemctl daemon-reload
echo "✅ 已禁用开机自启,并移除服务"
}
# 显示状态
show_status() {
if systemctl is-active --quiet "$APP_NAME".service; then
echo "🟢 服务正在运行(开机自启已启用)"
systemctl status "$APP_NAME".service --no-pager
elif [ -f "$SYSTEMD_SERVICE_FILE" ]; then
echo "🟠 服务已安装但未运行"
else
echo "🔴 服务未安装(开机自启已关闭)"
fi
}
# 主菜单
show_menu() {
echo "========================================"
echo " LJ360 摄像头系统管理工具"
echo "========================================"
show_status
echo ""
echo "请选择操作:"
echo " 1) 启用开机自启并启动服务"
echo " 2) 禁用开机自启并停止服务"
echo " 3) 仅手动运行一次(不保活)"
echo " 4) 查看日志"
echo " 0) 退出"
echo -n "请输入编号 (0-4): "
}
# 处理命令行参数(用于 systemd 调用)
if [ "$1" = "run" ]; then
start_app
exit 0
fi
# 交互式主程序
while true; do
show_menu
read -r choice
case $choice in
1)
enable_autostart
;;
2)
disable_autostart
;;
3)
echo "🚀 手动运行一次Ctrl+C 可退出)..."
cd "$WORK_DIR" && python3 "$PYTHON_SCRIPT"
;;
4)
echo "📄 最近日志(最后 20 行):"
tail -n 20 "$LOG_FILE"
echo -n "按回车返回菜单..."
read -r
;;
0)
echo "退出。"
exit 0
;;
*)
echo "❌ 无效选项,请输入 0-4"
sleep 1
;;
esac
echo ""
done

14
close_shell.sh Normal file
View File

@@ -0,0 +1,14 @@
#查找进场pid
pname=$1
pid=`ps -ef|grep ${pname}|grep -v "grep"|awk '{print $2}'`
echo $pid
#pid转换数组
pids=($pid)
#循环kill
for pid in ${pids[@]}
do
echo kill${pid}
kill -9 $pid
echo kill${pid}success
sleep 1
done

995
demo1.py Normal file
View File

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

164
getimg.py Normal file
View File

@@ -0,0 +1,164 @@
# 相机参考工具 (Optimized by XSimple)
# 作用: 实时预览指定摄像头,并在主界面对比原始画面与去畸变画面,支持按键抓拍
# 特性: 彻底修复 RK3588 退出后的 MIPI_CSI2 内核报错刷屏问题
import cv2
import threading
import sys
import os
import argparse
import numpy as np
import time # <--- 修复:引入 time 模块以处理底层 timeout 重试
# ==================== 全局控制与参数 ====================
running = True
capture_flag = False
# Front 相机内参 (K) 与 畸变系数 (D)
K = np.array([[ 3.9875655282072148e+02, 0., 6.3788553583130647e+02],
[0.,3.7214138046449295e+02, 3.4915903287538981e+02],
[ 0., 0., 1. ]])
D = np.array([ 8.6662165805452579e-02, -2.8155159006680735e-02,
-3.5111914522638211e-02, 1.4621518110074484e-02 ])
W, H = 1280, 720
# ==================== 终端指令后台监听 ====================
def input_thread():
"""
负责在后台默默倾听 SSH 终端的输入。
由于 input() 是阻塞式的,将其放入独立后台线程可以防止卡死画面。
"""
global running, capture_flag
print("\n[XSimple Tool] Terminal commands Ready: 's' = screenshot, 'q' = quit")
while running:
try:
cmd = input().strip().lower()
if cmd == 's':
capture_flag = True
elif cmd == 'q':
print("[SSH] Quit command received. Shutting down...")
running = False
break
except EOFError:
break
except Exception:
pass
# ==================== 主控与渲染流程 ====================
def main():
global running, capture_flag
# 1. 解析参数
parser = argparse.ArgumentParser(description="Multi-Camera Hardware Reference Tool")
parser.add_argument("--i", type=str, required=True,
choices=["front", "back", "left", "right"],
help="摄像头方位 (front, back, left, right)")
args = parser.parse_args()
camera_mapping = {"front": 0, "left": 1, "back": 2, "right": 3}
which_camera = camera_mapping[args.i]
cam_name = args.i.upper()
print(f"[INFO] Initializing Camera: {cam_name} (Node: /dev/video{which_camera})")
# 2. 检查输出文件夹
save_dir = os.path.join(os.getcwd(), "images")
if not os.path.exists(save_dir):
os.makedirs(save_dir)
# 3. 开启终端监听线程
cmd_listener = threading.Thread(target=input_thread, daemon=True)
cmd_listener.start()
# 4. 初始化 V4L2 摄像头
cap = None
try:
cap = cv2.VideoCapture(which_camera, cv2.CAP_V4L2)
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*"YUYV"))
cap.set(cv2.CAP_PROP_FRAME_WIDTH, W)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, H)
# 【关键防内核报错修复】:保证底层的 V4L2 有充足缓冲池
cap.set(cv2.CAP_PROP_BUFFERSIZE, 3)
if not cap.isOpened():
print(f"[ERROR] Cannot open camera /dev/video{which_camera}", file=sys.stderr)
running = False
return
# 预计算鱼眼镜头的抗畸变映射矩阵
map1, map2 = cv2.fisheye.initUndistortRectifyMap(K, D, np.eye(3), K, (W, H), cv2.CV_16SC2)
cv2.namedWindow('Video old vs new', cv2.WND_PROP_FULLSCREEN)
cv2.setWindowProperty('Video old vs new', cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
print("[INFO] Stream started. Press 'q' on windows or terminal to exit.")
# 5. 主线程 UI 渲染循环 (OpenCV 原生归属)
while running:
ret, f = cap.read()
if not ret or f is None:
# 出现 select() timeout 时,给物理层重试的机会,不直接崩溃
time.sleep(0.01)
continue
raw_frame = f.copy()
# —— 截图逻辑拦截 ——
if capture_flag:
filename = os.path.join(save_dir, f"{args.i.lower()}.png")
cv2.imwrite(filename, raw_frame)
print(f"[SUCCESS] Image saved accurately: {filename}")
capture_flag = False
# —— 图像处理与拼接 ——
undistorted = cv2.remap(f, map1, map2, interpolation=cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT)
comparison = np.hstack((f, undistorted))
# 等比例缩放以适应屏幕
scale = min(1920 / comparison.shape[1], 1080 / comparison.shape[0])
if scale < 1:
comparison = cv2.resize(comparison, (int(comparison.shape[1] * scale), int(comparison.shape[0] * scale)))
# 为了绘制字体时流畅,确保内存连续并转换类型
comparison = np.ascontiguousarray(comparison, dtype=np.uint8)
# 绘制 OSD 信息层
text_info = f"Camera: {cam_name} | Press 'q'='Quit', 's'='Screenshot'"
cv2.putText(comparison, text_info, (20, 50),
cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 255, 255), 3, cv2.LINE_AA)
# 区分原本与去畸变后的区域
w_half = comparison.shape[1] // 2
h_bottom = comparison.shape[0] - 30
cv2.putText(comparison, "RAW (FISHEYE)", (20, h_bottom),
cv2.FONT_HERSHEY_SIMPLEX, 1.5, (0, 255, 0), 3)
cv2.putText(comparison, "UNDISTORTED", (w_half + 20, h_bottom),
cv2.FONT_HERSHEY_SIMPLEX, 1.5, (0, 255, 0), 3)
# —— 显示与按键监控 ——
cv2.imshow('Video old vs new', comparison)
key = cv2.waitKey(1) & 0xFF
if key == ord('q'):
print("[GUI] Quit command triggered by UI window.")
running = False
break
elif key == ord('s'):
capture_flag = True
finally:
# 6. 安全清理底层资源 (无论遇到什么错误或中断,此代码块必会执行)
print("\n[INFO] Exiting and safely releasing hardware resources...")
running = False
if cap is not None and cap.isOpened():
cap.release()
cv2.destroyAllWindows()
print("[INFO] V4L2 Hardware closed peacefully. Goodbye.")
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n[INFO] Ctrl+C Detected. Terminating safely...")
running = False

BIN
images/back copy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

BIN
images/back.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
images/car.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

BIN
images/front.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
images/left.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
images/right.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
lib/librknn_api.so Normal file

Binary file not shown.

BIN
lib/librknnrt.so Normal file

Binary file not shown.

579910
lj360_camera_keepalive.log Normal file

File diff suppressed because it is too large Load Diff

731
lj360_manager.sh Normal file
View File

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

BIN
masks.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

18
model/anchors_yolov5.txt Normal file
View File

@@ -0,0 +1,18 @@
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 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

View File

@@ -0,0 +1,80 @@
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
model/download_model.sh Normal file
View File

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

0
py_utils/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

176
py_utils/coco_utils.py Normal file
View File

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

103
py_utils/onnx_executor.py Normal file
View File

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

View File

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

View File

@@ -0,0 +1,31 @@
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

26
py_utils/rknn_executor.py Normal file
View File

@@ -0,0 +1,26 @@
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

259
run_calibrate_camera.py Normal file
View File

@@ -0,0 +1,259 @@
"""
~~~~~~~~~~~~~~~~~~~~~~~~~~
Fisheye Camera calibration
~~~~~~~~~~~~~~~~~~~~~~~~~~
Usage:
python calibrate_camera.py \
-i 0 \
-grid 9x6 \
-out fisheye.yaml \
-framestep 20 \
--resolution 640x480
--fisheye
"""
import argparse
import os
import numpy as np
import cv2
from surround_view import CaptureThread, MultiBufferManager
import surround_view.utils as utils
# we will save the camera param file to this directory
TARGET_DIR = os.path.join(os.getcwd(), "yaml")
# default param file
DEFAULT_PARAM_FILE = os.path.join(TARGET_DIR, "camera_params.yaml")
def main():
parser = argparse.ArgumentParser()
# input video stream
parser.add_argument("-i", "--input", type=int, default=0,
help="input camera device")
# chessboard pattern size
parser.add_argument("-grid", "--grid", default="9x6",
help="size of the calibrate grid pattern")
parser.add_argument("-r", "--resolution", default="1280x720",
help="resolution of the camera image")
parser.add_argument("-framestep", type=int, default=20,
help="use every nth frame in the video")
parser.add_argument("-o", "--output", default=DEFAULT_PARAM_FILE,
help="path to output yaml file")
parser.add_argument("-fisheye", "--fisheye", action="store_true",
help="set true if this is a fisheye camera")
parser.add_argument("-flip", "--flip", default=0, type=int,
help="flip method of the camera")
parser.add_argument("--no_gst", action="store_true",
help="set true if not use gstreamer for the camera capture")
args = parser.parse_args()
if not os.path.exists(TARGET_DIR):
os.mkdir(TARGET_DIR)
text1 = "press c to calibrate"
text2 = "press q to quit"
text3 = "device: {}".format(args.input)
font = cv2.FONT_HERSHEY_SIMPLEX
fontscale = 0.6
resolution_str = args.resolution.split("x")
W = int(resolution_str[0])
H = int(resolution_str[1])
grid_size = tuple(int(x) for x in args.grid.split("x"))
grid_points = np.zeros((1, np.prod(grid_size), 3), np.float32)
grid_points[0, :, :2] = np.indices(grid_size).T.reshape(-1, 2)
objpoints = [] # 3d point in real world space
imgpoints = [] # 2d points in image plane
device = args.input
cap_thread = CaptureThread(device_id=device,
# flip_method=args.flip,
resolution=(W, H),
)
buffer_manager = MultiBufferManager()
buffer_manager.bind_thread(cap_thread, buffer_size=8)
if cap_thread.connect_camera():
cap_thread.start()
else:
print("cannot open device")
return
quit = False
do_calib = False
i = -1
while True:
i += 1
img = buffer_manager.get_device(device).get().image
if i % args.framestep != 0:
continue
print("searching for chessboard corners in frame " + str(i) + "...")
# img_bgr = cv2.cvtColor(img, cv2.COLOR_YUV2BGR_YUYV)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
found, corners = cv2.findChessboardCorners(
gray,
grid_size,
cv2.CALIB_CB_ADAPTIVE_THRESH +
cv2.CALIB_CB_NORMALIZE_IMAGE +
cv2.CALIB_CB_FILTER_QUADS +
cv2.CALIB_CB_FAST_CHECK
)
if found:
term = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_COUNT, 30, 0.01)
cv2.cornerSubPix(gray, corners, (5, 5), (-1, -1), term)
last_valid_bgr = img.copy()
print("✅检测到角点")
imgpoints.append(corners)
objpoints.append(grid_points)
cv2.drawChessboardCorners(img, grid_size, corners, found)
# cv2.putText(img, text1, (20, 70), font, fontscale, (255, 200, 0), 2)
# cv2.putText(img, text2, (20, 110), font, fontscale, (255, 200, 0), 2)
# cv2.putText(img, text3, (20, 30), font, fontscale, (255, 200, 0), 2)
# cv2.putText(img, f"shape: {img.shape[1]}x{img.shape[0]} | succecc farme: {len(objpoints)}",
# (20, 150), font, fontscale, (255, 200, 0), 2)
max_display_width = 1280
max_display_height = 720
scale = min(max_display_width / img.shape[1], max_display_height / img.shape[0])
if scale < 1:
img = cv2.resize(img, (int(img.shape[1] * scale), int(img.shape[0] * scale)))
cv2.imshow("corners", img)
last_valid_bgr = img.copy()
key = cv2.waitKey(1) & 0xFF
if key == ord("c"):
print("\nPerforming calibration...\n")
N_OK = len(objpoints)
if N_OK < 12:
print("Less than 12 corners (%d) detected, calibration failed" %(N_OK))
continue
else:
do_calib = True
break
elif key == ord("q"):
quit = True
break
if quit:
cap_thread.stop()
cap_thread.disconnect_camera()
cv2.destroyAllWindows()
if do_calib:
N_OK = len(objpoints)
K = np.zeros((3, 3))
D = np.zeros((4, 1))
rvecs = [np.zeros((1, 1, 3), dtype=np.float64) for _ in range(N_OK)]
tvecs = [np.zeros((1, 1, 3), dtype=np.float64) for _ in range(N_OK)]
calibration_flags = (cv2.fisheye.CALIB_RECOMPUTE_EXTRINSIC +
cv2.fisheye.CALIB_CHECK_COND +
cv2.fisheye.CALIB_FIX_SKEW)
if args.fisheye:
ret, mtx, dist, rvecs, tvecs = cv2.fisheye.calibrate(
objpoints,
imgpoints,
(W, H),
K,
D,
rvecs,
tvecs,
calibration_flags,
(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-6)
)
else:
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints,
imgpoints,
(W, H),
None,
None)
if ret:
fs = cv2.FileStorage(args.output, cv2.FILE_STORAGE_WRITE)
fs.write("resolution", np.int32([W, H]))
fs.write("camera_matrix", K)
fs.write("dist_coeffs", D)
fs.release()
print(f"✔标定成功!")
print(f" 保存至: {args.output}")
print(f" 使用的有效帧数: {N_OK}")
# 显示标定结果
print("\n=== 标定结果 ===")
print("相机内参矩阵 (K):")
print(np.round(K, 4))
print("\n畸变系数 (D):")
print(np.round(D.T, 6))
# test_img = buffer_manager.get_device(device).get().image
# test_img = cv2.cvtColor(test_img, cv2.COLOR_BGR2GRAY)
if last_valid_bgr is not None:
cv2.imshow("1 vs 2", last_valid_bgr)
map1, map2 = cv2.fisheye.initUndistortRectifyMap(
K, D, np.eye(3), K, (W, H), cv2.CV_16SC2)
undistorted = cv2.remap(last_valid_bgr, map1, map2,
interpolation=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT)
# # 拼接对比图并显示
# comparison = np.hstack((last_valid_bgr, last_valid_bgr))
# # 缩放对比图适配屏幕
# scale = min(1920 / comparison.shape[1], 1080 / comparison.shape[0])
# if scale < 1:
# comparison = cv2.resize(comparison,
# (int(comparison.shape[1] * scale),
# int(comparison.shape[0] * scale)))
# cv2.imshow("标定结果: 原始图像 vs 校正后图像", comparison)
# cv2.waitKey(50000) # 显示5秒
# cv2.destroyAllWindows()
# 确保是 uint8
last_valid_bgr = np.clip(last_valid_bgr, 0, 255).astype(np.uint8)
undistorted = np.clip(undistorted, 0, 255).astype(np.uint8)
# 拼接两个原图测试
comparison = np.hstack((last_valid_bgr, undistorted))
# 缩放(如果需要)
scale = min(1280 / comparison.shape[1], 720 / comparison.shape[0])
if scale < 1:
comparison = cv2.resize(comparison, (int(comparison.shape[1] * scale), int(comparison.shape[0] * scale)))
# 再次确保显示前是 uint8
comparison = comparison.astype(np.uint8)
cv2.imshow("Test Comparison", comparison)
cv2.waitKey(0)
cv2.waitKey(0)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,71 @@
"""
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Manually select points to get the projection map
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"""
import argparse
import os
import numpy as np
import cv2
from surround_view import FisheyeCameraModel, PointSelector, display_image
import surround_view.param_settings as settings
def get_projection_map(camera_model, image):
und_image = camera_model.undistort(image)
name = camera_model.camera_name
gui = PointSelector(und_image, title=name)
dst_points = settings.project_keypoints[name]
choice = gui.loop()
if choice > 0:
src = np.float32(gui.keypoints)
dst = np.float32(dst_points)
camera_model.project_matrix = cv2.getPerspectiveTransform(src, dst)
proj_image = camera_model.project(und_image)
ret = display_image("Bird's View", proj_image)
if ret > 0:
return True
if ret < 0:
cv2.destroyAllWindows()
return False
def main():
parser = argparse.ArgumentParser()
parser.add_argument("-camera", required=True,
choices=["front", "back", "left", "right"],
help="The camera view to be projected")
parser.add_argument("-scale", nargs="+", default=None,
help="scale the undistorted image")
parser.add_argument("-shift", nargs="+", default=None,
help="shift the undistorted image")
args = parser.parse_args()
if args.scale is not None:
scale = [float(x) for x in args.scale]
else:
scale = (1.0, 1.0)
if args.shift is not None:
shift = [float(x) for x in args.shift]
else:
shift = (0, 0)
camera_name = args.camera
camera_file = os.path.join(os.getcwd(), "yaml", camera_name + ".yaml")
image_file = os.path.join(os.getcwd(), "images", camera_name + ".png")
image = cv2.imread(image_file)
camera = FisheyeCameraModel(camera_file, camera_name)
camera.set_scale_and_shift(scale, shift)
success = get_projection_map(camera, image)
if success:
print("saving projection matrix to yaml")
camera.save_data()
else:
print("failed to compute the projection map")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,53 @@
import os
import numpy as np
import cv2
from PIL import Image
from surround_view import FisheyeCameraModel, display_image, BirdView
import surround_view.param_settings as settings
def main():
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.make_luminance_balance().stitch_all_parts()
birdview.stitch_all_parts()
birdview.make_white_balance()
birdview.copy_car_image()
img_samll = cv2.resize(birdview.image,(320,600))
cv2.imshow("BirdView Result", img_samll)
while True:
key = cv2.waitKey(1) & 0xFF
if key == ord("q"):
return -1
# cv2.waitKey(1)
# cmd = input().strip().lower()
# if cmd == 'q':
# cv2.destroyAllWindows()
# return -1
# birdview.copy_car_image()
# ret = display_image("BirdView Result", birdview.image)
# if ret > 0:
# Image.fromarray((Gmat * 255).astype(np.uint8)).save("weights.png")
# Image.fromarray(Mmat.astype(np.uint8)).save("masks.png")
if __name__ == "__main__":
main()

403
saveimg.py Normal file
View File

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

40
shared_buffer.py Normal file
View File

@@ -0,0 +1,40 @@
# 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, [], []

8
surround_config.json Normal file
View File

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

View File

@@ -0,0 +1,6 @@
from .fisheye_camera import FisheyeCameraModel
from .imagebuffer import MultiBufferManager
from .capture_thread import CaptureThread
from .process_thread import CameraProcessingThread
from .simple_gui import display_image, PointSelector
from .birdview import BirdView, ProjectedImageBuffer

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,52 @@
from queue import Queue
import cv2
from PyQt5.QtCore import (QThread, QTime, QMutex, pyqtSignal, QMutexLocker)
from .structures import ThreadStatisticsData
class BaseThread(QThread):
"""
Base class for all types of threads (capture, processing, stitching, ...,
etc). Mainly for collecting statistics of the threads.
"""
FPS_STAT_QUEUE_LENGTH = 32
update_statistics_gui = pyqtSignal(ThreadStatisticsData)
def __init__(self, parent=None):
super(BaseThread, self).__init__(parent)
self.init_commons()
def init_commons(self):
self.stopped = False
self.stop_mutex = QMutex()
self.clock = QTime()
self.fps = Queue()
self.processing_time = 0
self.processing_mutex = QMutex()
self.fps_sum = 0
self.stat_data = ThreadStatisticsData()
def stop(self):
with QMutexLocker(self.stop_mutex):
self.stopped = True
def update_fps(self, dt):
# add instantaneous fps value to queue
if dt > 0:
self.fps.put(1000 / dt)
# discard redundant items in the fps queue
if self.fps.qsize() > self.FPS_STAT_QUEUE_LENGTH:
self.fps.get()
# update statistics
if self.fps.qsize() == self.FPS_STAT_QUEUE_LENGTH:
while not self.fps.empty():
self.fps_sum += self.fps.get()
self.stat_data.average_fps = round(self.fps_sum / self.FPS_STAT_QUEUE_LENGTH, 2)
self.fps_sum = 0

386
surround_view/birdview.py Normal file
View File

@@ -0,0 +1,386 @@
import os
import numpy as np
import cv2
from PIL import Image
from PyQt5.QtCore import QMutex, QWaitCondition, QMutexLocker
from .base_thread import BaseThread
from .imagebuffer import Buffer
from . import param_settings as settings
from .param_settings import xl, xr, yt, yb
from . import utils
class ProjectedImageBuffer(object):
"""
Class for synchronizing processing threads from different cameras.
"""
def __init__(self, drop_if_full=True, buffer_size=8):
self.drop_if_full = drop_if_full
self.buffer = Buffer(buffer_size)
self.sync_devices = set()
self.wc = QWaitCondition()
self.mutex = QMutex()
self.arrived = 0
self.current_frames = dict()
def bind_thread(self, thread):
with QMutexLocker(self.mutex):
self.sync_devices.add(thread.device_id)
name = thread.camera_model.camera_name
shape = settings.project_shapes[name]
self.current_frames[thread.device_id] = np.zeros(shape[::-1] + (3,), np.uint8)
thread.proc_buffer_manager = self
def get(self):
return self.buffer.get()
def set_frame_for_device(self, device_id, frame):
if device_id not in self.sync_devices:
raise ValueError("Device not held by the buffer: {}".format(device_id))
self.current_frames[device_id] = frame
def sync(self, device_id):
# only perform sync if enabled for specified device/stream
self.mutex.lock()
if device_id in self.sync_devices:
# increment arrived count
self.arrived += 1
# we are the last to arrive: wake all waiting threads
if self.arrived == len(self.sync_devices):
self.buffer.add(self.current_frames, self.drop_if_full)
self.wc.wakeAll()
# still waiting for other streams to arrive: wait
else:
self.wc.wait(self.mutex)
# decrement arrived count
self.arrived -= 1
self.mutex.unlock()
def wake_all(self):
with QMutexLocker(self.mutex):
self.wc.wakeAll()
def __contains__(self, device_id):
return device_id in self.sync_devices
def __str__(self):
return (self.__class__.__name__ + ":\n" + \
"devices: {}\n".format(self.sync_devices))
def FI(front_image):
return front_image[:, :xl]
def FII(front_image):
return front_image[:, xr:]
def FM(front_image):
return front_image[:, xl:xr]
def BIII(back_image):
return back_image[:, :xl]
def BIV(back_image):
return back_image[:, xr:]
def BM(back_image):
return back_image[:, xl:xr]
def LI(left_image):
return left_image[:yt, :]
def LIII(left_image):
return left_image[yb:, :]
def LM(left_image):
return left_image[yt:yb, :]
def RII(right_image):
return right_image[:yt, :]
def RIV(right_image):
return right_image[yb:, :]
def RM(right_image):
return right_image[yt:yb, :]
class BirdView(BaseThread):
def __init__(self,
proc_buffer_manager=None,
drop_if_full=True,
buffer_size=8,
parent=None):
super(BirdView, self).__init__(parent)
self.proc_buffer_manager = proc_buffer_manager
self.drop_if_full = drop_if_full
self.buffer = Buffer(buffer_size)
self.image = np.zeros((settings.total_h, settings.total_w, 3), np.uint8)
self.weights = None
self.masks = None
self.car_image = settings.car_image
self.frames = None
def get(self):
return self.buffer.get()
def update_frames(self, images):
self.frames = images
def load_weights_and_masks(self, weights_image, masks_image):
GMat = np.asarray(Image.open(weights_image).convert("RGBA"), dtype=np.float) / 255.0
self.weights = [np.stack((GMat[:, :, k],
GMat[:, :, k],
GMat[:, :, k]), axis=2)
for k in range(4)]
Mmat = np.asarray(Image.open(masks_image).convert("RGBA"), dtype=np.float)
Mmat = utils.convert_binary_to_bool(Mmat)
self.masks = [Mmat[:, :, k] for k in range(4)]
def merge(self, imA, imB, k):
G = self.weights[k]
return (imA * G + imB * (1 - G)).astype(np.uint8)
@property
def FL(self):
return self.image[:yt, :xl]
@property
def F(self):
return self.image[:yt, xl:xr]
@property
def FR(self):
return self.image[:yt, xr:]
@property
def BL(self):
return self.image[yb:, :xl]
@property
def B(self):
return self.image[yb:, xl:xr]
@property
def BR(self):
return self.image[yb:, xr:]
@property
def L(self):
return self.image[yt:yb, :xl]
@property
def R(self):
return self.image[yt:yb, xr:]
@property
def C(self):
return self.image[yt:yb, xl:xr]
def stitch_all_parts(self):
front, back, left, right = self.frames
np.copyto(self.F, FM(front))
np.copyto(self.B, BM(back))
np.copyto(self.L, LM(left))
np.copyto(self.R, RM(right))
np.copyto(self.FL, self.merge(FI(front), LI(left), 0))
np.copyto(self.FR, self.merge(FII(front), RII(right), 1))
np.copyto(self.BL, self.merge(BIII(back), LIII(left), 2))
np.copyto(self.BR, self.merge(BIV(back), RIV(right), 3))
# def copy_car_image(self):
# np.copyto(self.C, self.car_image)
def copy_car_image(self):
car_rgba = settings.car_image_rgba
x1 = settings.car_x
y1 = settings.car_y
h, w = car_rgba.shape[:2]
x2 = x1 + w
y2 = y1 + h
img_h, img_w = self.image.shape[:2]
# 裁剪到图像边界
x1c = max(x1, 0)
y1c = max(y1, 0)
x2c = min(x2, img_w)
y2c = min(y2, img_h)
# 对应裁剪 car_rgba
cx1 = x1c - x1
cy1 = y1c - y1
cx2 = cx1 + (x2c - x1c)
cy2 = cy1 + (y2c - y1c)
car_crop = car_rgba[cy1:cy2, cx1:cx2]
roi = self.image[y1c:y2c, x1c:x2c]
# ★ 强制对齐尺寸,避免广播错误
rh, rw = roi.shape[:2]
ch, cw = car_crop.shape[:2]
if rh != ch or rw != cw:
min_h = min(rh, ch)
min_w = min(rw, cw)
car_crop = car_crop[:min_h, :min_w]
roi = self.image[y1c:y1c + min_h, x1c:x1c + min_w]
alpha = car_crop[:, :, 3].astype(np.float32) / 255.0
alpha = np.stack([alpha, alpha, alpha], axis=2)
car_bgr = car_crop[:, :, :3].astype(np.float32)
roi_f = roi.astype(np.float32)
blended = (car_bgr * alpha + roi_f * (1.0 - alpha)).astype(np.uint8)
np.copyto(self.image[y1c:y1c + blended.shape[0], x1c:x1c + blended.shape[1]], blended)
def make_luminance_balance(self):
def tune(x):
if x >= 1:
return x * np.exp((1 - x) * 0.5)
else:
return x * np.exp((1 - x) * 0.8)
front, back, left, right = self.frames
m1, m2, m3, m4 = self.masks
Fb, Fg, Fr = cv2.split(front)
Bb, Bg, Br = cv2.split(back)
Lb, Lg, Lr = cv2.split(left)
Rb, Rg, Rr = cv2.split(right)
a1 = utils.mean_luminance_ratio(RII(Rb), FII(Fb), m2)
a2 = utils.mean_luminance_ratio(RII(Rg), FII(Fg), m2)
a3 = utils.mean_luminance_ratio(RII(Rr), FII(Fr), m2)
b1 = utils.mean_luminance_ratio(BIV(Bb), RIV(Rb), m4)
b2 = utils.mean_luminance_ratio(BIV(Bg), RIV(Rg), m4)
b3 = utils.mean_luminance_ratio(BIV(Br), RIV(Rr), m4)
c1 = utils.mean_luminance_ratio(LIII(Lb), BIII(Bb), m3)
c2 = utils.mean_luminance_ratio(LIII(Lg), BIII(Bg), m3)
c3 = utils.mean_luminance_ratio(LIII(Lr), BIII(Br), m3)
d1 = utils.mean_luminance_ratio(FI(Fb), LI(Lb), m1)
d2 = utils.mean_luminance_ratio(FI(Fg), LI(Lg), m1)
d3 = utils.mean_luminance_ratio(FI(Fr), LI(Lr), m1)
t1 = (a1 * b1 * c1 * d1)**0.25
t2 = (a2 * b2 * c2 * d2)**0.25
t3 = (a3 * b3 * c3 * d3)**0.25
x1 = t1 / (d1 / a1)**0.5
x2 = t2 / (d2 / a2)**0.5
x3 = t3 / (d3 / a3)**0.5
x1 = tune(x1)
x2 = tune(x2)
x3 = tune(x3)
Fb = utils.adjust_luminance(Fb, x1)
Fg = utils.adjust_luminance(Fg, x2)
Fr = utils.adjust_luminance(Fr, x3)
y1 = t1 / (b1 / c1)**0.5
y2 = t2 / (b2 / c2)**0.5
y3 = t3 / (b3 / c3)**0.5
y1 = tune(y1)
y2 = tune(y2)
y3 = tune(y3)
Bb = utils.adjust_luminance(Bb, y1)
Bg = utils.adjust_luminance(Bg, y2)
Br = utils.adjust_luminance(Br, y3)
z1 = t1 / (c1 / d1)**0.5
z2 = t2 / (c2 / d2)**0.5
z3 = t3 / (c3 / d3)**0.5
z1 = tune(z1)
z2 = tune(z2)
z3 = tune(z3)
Lb = utils.adjust_luminance(Lb, z1)
Lg = utils.adjust_luminance(Lg, z2)
Lr = utils.adjust_luminance(Lr, z3)
w1 = t1 / (a1 / b1)**0.5
w2 = t2 / (a2 / b2)**0.5
w3 = t3 / (a3 / b3)**0.5
w1 = tune(w1)
w2 = tune(w2)
w3 = tune(w3)
Rb = utils.adjust_luminance(Rb, w1)
Rg = utils.adjust_luminance(Rg, w2)
Rr = utils.adjust_luminance(Rr, w3)
self.frames = [cv2.merge((Fb, Fg, Fr)),
cv2.merge((Bb, Bg, Br)),
cv2.merge((Lb, Lg, Lr)),
cv2.merge((Rb, Rg, Rr))]
return self
def get_weights_and_masks(self, images):
front, back, left, right = images
G0, M0 = utils.get_weight_mask_matrix(FI(front), LI(left))
G1, M1 = utils.get_weight_mask_matrix(FII(front), RII(right))
G2, M2 = utils.get_weight_mask_matrix(BIII(back), LIII(left))
G3, M3 = utils.get_weight_mask_matrix(BIV(back), RIV(right))
self.weights = [np.stack((G, G, G), axis=2) for G in (G0, G1, G2, G3)]
self.masks = [(M / 255.0).astype(int) for M in (M0, M1, M2, M3)]
return np.stack((G0, G1, G2, G3), axis=2), np.stack((M0, M1, M2, M3), axis=2)
def make_white_balance(self):
self.image = utils.make_white_balance(self.image)
def run(self):
if self.proc_buffer_manager is None:
raise ValueError("This thread requires a buffer of projected images to run")
while True:
self.stop_mutex.lock()
if self.stopped:
self.stopped = False
self.stop_mutex.unlock()
break
self.stop_mutex.unlock()
self.processing_time = self.clock.elapsed()
self.clock.start()
self.processing_mutex.lock()
self.update_frames(self.proc_buffer_manager.get().values())
self.stitch_all_parts() # 直接拼接原始投影图像
# self.make_luminance_balance().stitch_all_parts()
self.make_white_balance()
self.copy_car_image()
self.buffer.add(self.image.copy(), self.drop_if_full)
self.processing_mutex.unlock()
# update statistics
self.update_fps(self.processing_time)
self.stat_data.frames_processed_count += 1
# inform GUI of updated statistics
self.update_statistics_gui.emit(self.stat_data)

View File

@@ -0,0 +1,113 @@
import cv2
from PyQt5.QtCore import qDebug
from .base_thread import BaseThread
from .structures import ImageFrame
class CaptureThread(BaseThread):
def __init__(self,
device_id,
flip_method=0,
drop_if_full=True,
api_preference=cv2.CAP_V4L2,
resolution=None,
parent=None):
"""
device_id: device number of the camera.
flip_method: 0 for identity, 2 for 180 degree rotation (if the camera is installed
up-side-down).
drop_if_full: drop the frame if buffer is full.
api_preference: cv2.CAP_GSTREAMER for csi cameras, usually cv2.CAP_ANY would suffice.
resolution: camera resolution (width, height).
"""
super(CaptureThread, self).__init__(parent)
self.device_id = device_id
self.flip_method = 0
self.drop_if_full = drop_if_full
self.api_preference = api_preference
self.resolution = resolution
self.cap = cv2.VideoCapture()
# an instance of the MultiBufferManager object,
# for synchronizing this thread with other cameras.
self.buffer_manager = None
def run(self):
if self.buffer_manager is None:
raise ValueError("This thread has not been binded to any buffer manager yet")
while True:
self.stop_mutex.lock()
if self.stopped:
self.stopped = False
self.stop_mutex.unlock()
break
self.stop_mutex.unlock()
# save capture time
self.processing_time = self.clock.elapsed()
# start timer (used to calculate capture rate)
self.clock.start()
# synchronize with other streams (if enabled for this stream)
self.buffer_manager.sync(self.device_id)
if not self.cap.grab():
continue
# retrieve frame and add it to buffer
_, frame = self.cap.read()
# Skip empty frames (e.g., when camera capture times out)
if frame is None or frame.size == 0:
continue
# Apply image flip if needed
if self.flip_method == 2:
frame = cv2.rotate(frame, cv2.ROTATE_180)
# frame = cv2.resize(frame, (960, 640))
img_frame = ImageFrame(self.clock.msecsSinceStartOfDay(), frame)
self.buffer_manager.get_device(self.device_id).add(img_frame, self.drop_if_full)
# update statistics
# self.update_fps(self.processing_time)
# self.stat_data.frames_processed_count += 1
# # inform GUI of updated statistics
# self.update_statistics_gui.emit(self.stat_data)
qDebug("Stopping capture thread...")
def connect_camera(self):
self.cap.open(self.device_id)
# return false if failed to open camera
if not self.cap.isOpened():
qDebug("Cannot open camera {}".format(self.device_id))
return False
else:
# try to set camera resolution
if self.resolution is not None:
width, height = self.resolution
self.cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*"YUYV"))
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
# some camera may become closed if the resolution is not supported
if not self.cap.isOpened():
qDebug("Resolution not supported by camera device: {}".format(self.resolution))
return False
return True
def disconnect_camera(self):
# disconnect camera if it's already opened.
if self.cap.isOpened():
self.cap.release()
return True
# else do nothing and return
else:
return False
def is_camera_connected(self):
return self.cap.isOpened()

View File

@@ -0,0 +1,128 @@
import os
import numpy as np
import cv2
from . import param_settings as settings
class FisheyeCameraModel(object):
"""
Fisheye camera model, for undistorting, projecting and flipping camera frames.
"""
def __init__(self, camera_param_file, camera_name):
if not os.path.isfile(camera_param_file):
raise ValueError("Cannot find camera param file")
if camera_name not in settings.camera_names:
raise ValueError("Unknown camera name: {}".format(camera_name))
self.camera_file = camera_param_file
self.camera_name = camera_name
self.scale_xy = (1.0, 1.0)
self.shift_xy = (0, 0)
self.undistort_maps = None
self.project_matrix = None
self.project_shape = settings.project_shapes[self.camera_name]
self.load_camera_params()
def load_camera_params(self):
fs = cv2.FileStorage(self.camera_file, cv2.FILE_STORAGE_READ)
self.camera_matrix = fs.getNode("camera_matrix").mat()
self.dist_coeffs = fs.getNode("dist_coeffs").mat()
self.resolution = fs.getNode("resolution").mat().flatten()
scale_xy = fs.getNode("scale_xy").mat()
if scale_xy is not None:
self.scale_xy = scale_xy
shift_xy = fs.getNode("shift_xy").mat()
if shift_xy is not None:
self.shift_xy = shift_xy
project_matrix = fs.getNode("project_matrix").mat()
if project_matrix is not None:
self.project_matrix = project_matrix
fs.release()
self.update_undistort_maps()
def update_undistort_maps(self):
new_matrix = self.camera_matrix.copy()
new_matrix[0, 0] *= self.scale_xy[0]
new_matrix[1, 1] *= self.scale_xy[1]
new_matrix[0, 2] += self.shift_xy[0]
new_matrix[1, 2] += self.shift_xy[1]
width, height = self.resolution
self.undistort_maps = cv2.fisheye.initUndistortRectifyMap(
self.camera_matrix,
self.dist_coeffs,
np.eye(3),
new_matrix,
(width, height),
cv2.CV_16SC2
)
return self
def set_scale_and_shift(self, scale_xy=(1.0, 1.0), shift_xy=(0, 0)):
self.scale_xy = scale_xy
self.shift_xy = shift_xy
self.update_undistort_maps()
return self
def undistort(self, image):
result = cv2.remap(image, *self.undistort_maps, interpolation=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT)
return result
# def undistort(self, image):
# uimg = cv2.UMat(image)
# uresult = cv2.remap(
# uimg,
# *self.undistort_maps,
# interpolation=cv2.INTER_LINEAR,
# borderMode=cv2.BORDER_CONSTANT,
# borderValue=(0, 0, 0)
# )
# return uresult.get()
# def project(self, image):
# result = cv2.warpPerspective(image, self.project_matrix, self.project_shape)
# return result
def project(self, image):
# 转为 UMat触发 GPU 路径)
uimg = cv2.UMat(image)
uresult = cv2.warpPerspective(
uimg,
self.project_matrix,
self.project_shape,
flags=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT,
borderValue=(0, 0, 0)
)
return uresult.get() # 转回 numpy array 供后续使用
def flip(self, image):
if self.camera_name == "front":
return image.copy()
elif self.camera_name == "back":
return image.copy()[::-1, ::-1, :]
elif self.camera_name == "left":
return cv2.transpose(image)[::-1]
else:
return cv2.transpose(image)[:,::-1]
def save_data(self):
fs = cv2.FileStorage(self.camera_file, cv2.FILE_STORAGE_WRITE)
fs.write("camera_matrix", self.camera_matrix)
fs.write("dist_coeffs", self.dist_coeffs)
fs.write("resolution", self.resolution)
fs.write("project_matrix", self.project_matrix)
fs.write("scale_xy", np.float32(self.scale_xy))
fs.write("shift_xy", np.float32(self.shift_xy))
fs.release()

View File

@@ -0,0 +1,161 @@
from PyQt5.QtCore import QSemaphore, QMutex
from PyQt5.QtCore import QMutexLocker, QWaitCondition
from queue import Queue
class Buffer(object):
def __init__(self, buffer_size=5):
self.buffer_size = buffer_size
self.free_slots = QSemaphore(self.buffer_size)
self.used_slots = QSemaphore(0)
self.clear_buffer_add = QSemaphore(1)
self.clear_buffer_get = QSemaphore(1)
self.queue_mutex = QMutex()
self.queue = Queue(self.buffer_size)
def add(self, data, drop_if_full=False):
self.clear_buffer_add.acquire()
if drop_if_full:
if self.free_slots.tryAcquire():
self.queue_mutex.lock()
self.queue.put(data)
self.queue_mutex.unlock()
self.used_slots.release()
else:
self.free_slots.acquire()
self.queue_mutex.lock()
self.queue.put(data)
self.queue_mutex.unlock()
self.used_slots.release()
self.clear_buffer_add.release()
def get(self):
# acquire semaphores
self.clear_buffer_get.acquire()
self.used_slots.acquire()
self.queue_mutex.lock()
data = self.queue.get()
self.queue_mutex.unlock()
# release semaphores
self.free_slots.release()
self.clear_buffer_get.release()
# return item to caller
return data
def clear(self):
# check if buffer contains items
if self.queue.qsize() > 0:
# stop adding items to buffer (will return false if an item is currently being added to the buffer)
if self.clear_buffer_add.tryAcquire():
# stop taking items from buffer (will return false if an item is currently being taken from the buffer)
if self.clear_buffer_get.tryAcquire():
# release all remaining slots in queue
self.free_slots.release(self.queue.qsize())
# acquire all queue slots
self.free_slots.acquire(self.buffer_size)
# reset used_slots to zero
self.used_slots.acquire(self.queue.qsize())
# clear buffer
for _ in range(self.queue.qsize()):
self.queue.get()
# release all slots
self.free_slots.release(self.buffer_size)
# allow get method to resume
self.clear_buffer_get.release()
else:
return False
# allow add method to resume
self.clear_buffer_add.release()
return True
else:
return False
else:
return False
def size(self):
return self.queue.qsize()
def maxsize(self):
return self.buffer_size
def isfull(self):
return self.queue.qsize() == self.buffer_size
def isempty(self):
return self.queue.qsize() == 0
class MultiBufferManager(object):
"""
Class for synchronizing capture threads from different cameras.
"""
def __init__(self, do_sync=True):
self.sync_devices = set()
self.do_sync = do_sync
self.wc = QWaitCondition()
self.mutex = QMutex()
self.arrived = 0
self.buffer_maps = dict()
def bind_thread(self, thread, buffer_size, sync=True):
self.create_buffer_for_device(thread.device_id, buffer_size, sync)
thread.buffer_manager = self
def create_buffer_for_device(self, device_id, buffer_size, sync=True):
if sync:
with QMutexLocker(self.mutex):
self.sync_devices.add(device_id)
self.buffer_maps[device_id] = Buffer(buffer_size)
def get_device(self, device_id):
return self.buffer_maps[device_id]
def remove_device(self, device_id):
self.buffer_maps.pop(device_id)
with QMutexLocker(self.mutex):
if device_id in self.sync_devices:
self.sync_devices.remove(device_id)
self.wc.wakeAll()
def sync(self, device_id):
# only perform sync if enabled for specified device/stream
self.mutex.lock()
if device_id in self.sync_devices:
# increment arrived count
self.arrived += 1
# we are the last to arrive: wake all waiting threads
if self.do_sync and self.arrived == len(self.sync_devices):
self.wc.wakeAll()
# still waiting for other streams to arrive: wait
else:
self.wc.wait(self.mutex)
# decrement arrived count
self.arrived -= 1
self.mutex.unlock()
def wake_all(self):
with QMutexLocker(self.mutex):
self.wc.wakeAll()
def set_sync(self, enable):
self.do_sync = enable
def sync_enabled(self):
return self.do_sync
def sync_enabled_for_device(self, device_id):
return device_id in self.sync_devices
def __contains__(self, device_id):
return device_id in self.buffer_maps
def __str__(self):
return (self.__class__.__name__ + ":\n" + \
"sync: {}\n".format(self.do_sync) + \
"devices: {}\n".format(tuple(self.buffer_maps.keys())) + \
"sync enabled devices: {}".format(self.sync_devices))

View File

@@ -0,0 +1,118 @@
import os
import cv2
camera_names = ["front", "back", "left", "right"]
# --------------------------------------------------------------------
# (shift_width, shift_height): 鸟瞰图在水平和垂直方向上超出标定图案的距离
shift_w = 100
shift_h = 100
# 标定图案与车辆之间在水平和垂直方向上的间隙大小
inn_shift_w = 8
inn_shift_h = 30
# 拼接图像的总宽度/高度
total_w = 260 + 2 * shift_w
total_h = 350 + 2 * shift_h
# 计算车辆在全景图中的位置
xl = shift_w + 55 + inn_shift_w
xr = total_w - xl
print(xl, xr)
yt = shift_h + 55 + inn_shift_h
yb = total_h - yt
# --------------------------------------------------------------------
project_shapes = {
"front": (total_w, yt),
"back": (total_w, yt),
"left": (total_h, xl),
"right": (total_h, xl)
}
# 要选取的四个像素点的位置。
# 运行 get_projection_map.py 脚本时,必须按相同顺序点击这些像素点。
project_keypoints = {
"front": [(shift_w + 0, shift_h),
(shift_w + 260, shift_h),
(shift_w + 0, shift_h + 100),
(shift_w + 260, shift_h + 100)],
"back": [(shift_w + 0, shift_h),
(shift_w + 260, shift_h),
(shift_w + 0, shift_h + 80),
(shift_w + 260, shift_h + 80)],
"left": [(shift_h + 60, shift_w),
(shift_h + 300, shift_w),
(shift_h + 60, shift_w + 50),
(shift_h + 300, shift_w + 50)],
"right": [(shift_h + 80, shift_w),
(shift_h + 350, shift_w),
(shift_h + 80, shift_w + 50),
(shift_h + 350, shift_w + 50)]
}
# car_image = cv2.imread(os.path.join(os.getcwd(), "images", "car.png"))
# car_image = cv2.resize(car_image, (xr - xl, yb - yt))
# 放大系数调整这个值控制车辆图大小1.0 = 原始大小1.3 = 放大30%
CAR_SCALE = 1.3
car_w = xr - xl
car_h = yb - yt
# 放大后的车辆图尺寸
car_display_w = int(car_w * CAR_SCALE)
car_display_h = int(car_h * CAR_SCALE)
# 放大后车辆图在全景图中的起始坐标(居中对齐)
car_x = xl - (car_display_w - car_w) // 2
car_y = yt - (car_display_h - car_h) // 2
def _make_car_image_rgba(path, w, h, tolerance=30):
# 用 IMREAD_UNCHANGED 保留 PNG 透明通道
img = cv2.imread(path, cv2.IMREAD_UNCHANGED)
if img is None:
return np.zeros((h, w, 4), np.uint8)
img = cv2.resize(img, (w, h))
# 如果图片有 alpha 通道,直接用
if img.shape[2] == 4:
rgba = img.copy()
else:
# 没有 alpha 通道,白色转透明
rgba = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)
white_mask = (
(rgba[:, :, 0] >= 255 - tolerance) &
(rgba[:, :, 1] >= 255 - tolerance) &
(rgba[:, :, 2] >= 255 - tolerance)
)
rgba[white_mask, 3] = 0
# 边缘平滑
alpha = rgba[:, :, 3].astype(np.uint8)
alpha = cv2.GaussianBlur(alpha, (5, 5), 0)
rgba[:, :, 3] = alpha
return rgba
import numpy as np
car_image_path = os.path.join(os.getcwd(), "images", "car.png")
# 原始尺寸(保持兼容)
car_image = cv2.imread(car_image_path)
if car_image is not None:
car_image = cv2.resize(car_image, (car_w, car_h))
# 放大版带透明通道
car_image_rgba = _make_car_image_rgba(car_image_path, car_display_w, car_display_h)

View File

@@ -0,0 +1,81 @@
import cv2
from PyQt5.QtCore import qDebug, QMutex
from .base_thread import BaseThread
from . import param_settings as settings
class CameraProcessingThread(BaseThread):
"""
Thread for processing individual camera images, i.e. undistort, project and flip.
"""
def __init__(self,
capture_buffer_manager,
device_id,
camera_model,
drop_if_full=True,
parent=None):
"""
capture_buffer_manager: an instance of the `MultiBufferManager` object.
device_id: device number of the camera to be processed.
camera_model: an instance of the `FisheyeCameraModel` object.
drop_if_full: drop if the buffer is full.
"""
super(CameraProcessingThread, self).__init__(parent)
self.capture_buffer_manager = capture_buffer_manager
self.device_id = device_id
self.camera_model = camera_model
self.drop_if_full = drop_if_full
# an instance of the `ProjectedImageBuffer` object
self.proc_buffer_manager = None
def run(self):
if self.proc_buffer_manager is None:
raise ValueError("This thread has not been binded to any processing thread yet")
while True:
self.stop_mutex.lock()
if self.stopped:
self.stopped = False
self.stop_mutex.unlock()
break
self.stop_mutex.unlock()
self.processing_time = self.clock.elapsed()
self.clock.start()
self.processing_mutex.lock()
raw_frame = self.capture_buffer_manager.get_device(self.device_id).get()
# Validate frame before processing
if raw_frame.image is None or raw_frame.image.size == 0:
self.processing_mutex.unlock()
continue
und_frame = self.camera_model.undistort(raw_frame.image)
pro_frame = self.camera_model.project(und_frame)
flip_frame = self.camera_model.flip(pro_frame)
self.processing_mutex.unlock()
# Check if the processed frame has valid dimensions
name = self.camera_model.camera_name
if name in settings.project_shapes:
# For left and right cameras, the flip operation changes the shape
if name in ['left', 'right']:
expected_shape = settings.project_shapes[name] + (3,)
else:
expected_shape = settings.project_shapes[name][::-1] + (3,)
if flip_frame.shape != expected_shape:
print(f"Warning: {name} camera frame has unexpected shape {flip_frame.shape}, expected {expected_shape}")
continue
self.proc_buffer_manager.sync(self.device_id)
self.proc_buffer_manager.set_frame_for_device(self.device_id, flip_frame)
# update statistics
self.update_fps(self.processing_time)
self.stat_data.frames_processed_count += 1
# inform GUI of updated statistics
self.update_statistics_gui.emit(self.stat_data)

131
surround_view/simple_gui.py Normal file
View File

@@ -0,0 +1,131 @@
import cv2
import numpy as np
# return -1 if user press 'q'. return 1 if user press 'Enter'.
def display_image(window_title, image):
cv2.imshow(window_title, image)
while True:
click = cv2.getWindowProperty(window_title, cv2.WND_PROP_AUTOSIZE)
if click < 0:
return -1
key = cv2.waitKey(1) & 0xFF
if key == ord("q"):
return -1
# 'Enter' key is detected!
if key == 13:
return 1
class PointSelector(object):
"""
---------------------------------------------------
| A simple gui point selector. |
| Usage: |
| |
| 1. call the `loop` method to show the image. |
| 2. click on the image to select key points, |
| press `d` to delete the last points. |
| 3. press `q` to quit, press `Enter` to confirm. |
---------------------------------------------------
"""
POINT_COLOR = (0, 0, 255)
FILL_COLOR = (0, 255, 255)
def __init__(self, image, title="PointSelector"):
self.image = image
self.title = title
self.keypoints = []
def draw_image(self):
"""
Display the selected keypoints and draw the convex hull.
"""
# the trick: draw on another new image
new_image = self.image.copy()
# draw the selected keypoints
for i, pt in enumerate(self.keypoints):
cv2.circle(new_image, pt, 6, self.POINT_COLOR, -1)
cv2.putText(new_image, str(i), (pt[0], pt[1] - 15),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, self.POINT_COLOR, 2)
# draw a line if there are two points
if len(self.keypoints) == 2:
p1, p2 = self.keypoints
cv2.line(new_image, p1, p2, self.POINT_COLOR, 2)
# draw the convex hull if there are more than two points
if len(self.keypoints) > 2:
mask = self.create_mask_from_pixels(self.keypoints,
self.image.shape)
new_image = self.draw_mask_on_image(new_image, mask)
cv2.imshow(self.title, new_image)
def onclick(self, event, x, y, flags, param):
"""
Click on a point (x, y) will add this points to the list
and re-draw the image.
"""
if event == cv2.EVENT_LBUTTONDOWN:
print("click ({}, {})".format(x, y))
self.keypoints.append((x, y))
self.draw_image()
def loop(self):
"""
Press "q" will exist the gui and return False
press "d" will delete the last selected point.
Press "Enter" will exist the gui and return True.
"""
cv2.namedWindow(self.title)
cv2.setMouseCallback(self.title, self.onclick, param=())
cv2.imshow(self.title, self.image)
while True:
click = cv2.getWindowProperty(self.title, cv2.WND_PROP_AUTOSIZE)
if click < 0:
return False
key = cv2.waitKey(1) & 0xFF
# press q to return False
if key == ord("q"):
return False
# press d to delete the last point
if key == ord("d"):
if len(self.keypoints) > 0:
x, y = self.keypoints.pop()
print("Delete ({}, {})".format(x, y))
self.draw_image()
# press Enter to confirm
if key == 13:
return True
def create_mask_from_pixels(self, pixels, image_shape):
"""
Create mask from the convex hull of a list of pixels.
"""
pixels = np.int32(pixels).reshape(-1, 2)
hull = cv2.convexHull(pixels)
mask = np.zeros(image_shape[:2], np.int8)
cv2.fillConvexPoly(mask, hull, 1, lineType=8, shift=0)
mask = mask.astype(bool)
return mask
def draw_mask_on_image(self, image, mask):
"""
Paint the region defined by a given mask on an image.
"""
new_image = np.zeros_like(image)
new_image[:, :] = self.FILL_COLOR
mask = np.array(mask, dtype=np.uint8)
new_mask = cv2.bitwise_and(new_image, new_image, mask=mask)
cv2.addWeighted(image, 1.0, new_mask, 0.5, 0.0, image)
return image

View File

@@ -0,0 +1,12 @@
class ImageFrame(object):
def __init__(self, timestamp, image):
self.timestamp = timestamp
self.image = image
class ThreadStatisticsData(object):
def __init__(self):
self.average_fps = 0
self.frames_processed_count = 0

137
surround_view/utils.py Normal file
View File

@@ -0,0 +1,137 @@
import cv2
import numpy as np
def gstreamer_pipeline(cam_id=0,
capture_width=1920,
capture_height=1080,
framerate=30,
format="YUYV",
):
print("11111111233333333333545656646464646")
"""
Use libgstreamer to open csi-cameras.
"""
return (
f"v4l2src device=/dev/video{cam_id} ! "
f"video/x-raw,format={format},width={capture_width},height={capture_height},framerate={framerate}/1 ! "
"videoconvert ! "
"video/x-raw,format=YUYV ! " # 转为 OpenCV 能直接用的 BGR 格式
"appsink"
)
def convert_binary_to_bool(mask):
"""
Convert a binary image (only one channel and pixels are 0 or 255) to
a bool one (all pixels are 0 or 1).
"""
return (mask.astype(np.float) / 255.0).astype(int)
def adjust_luminance(gray, factor):
"""
Adjust the luminance of a grayscale image by a factor.
"""
return np.minimum((gray * factor), 255).astype(np.uint8)
def get_mean_statistisc(gray, mask):
"""
Get the total values of a gray image in a region defined by a mask matrix.
The mask matrix must have values either 0 or 1.
"""
return np.sum(gray * mask)
def mean_luminance_ratio(grayA, grayB, mask):
return get_mean_statistisc(grayA, mask) / get_mean_statistisc(grayB, mask)
def get_mask(img):
"""
Convert an image to a mask array.
"""
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, mask = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY)
return mask
def get_overlap_region_mask(imA, imB):
"""
Given two images of the save size, get their overlapping region and
convert this region to a mask array.
"""
overlap = cv2.bitwise_and(imA, imB)
mask = get_mask(overlap)
mask = cv2.dilate(mask, np.ones((2, 2), np.uint8), iterations=2)
return mask
def get_outmost_polygon_boundary(img):
"""
Given a mask image with the mask describes the overlapping region of
two images, get the outmost contour of this region.
"""
mask = get_mask(img)
mask = cv2.dilate(mask, np.ones((2, 2), np.uint8), iterations=2)
cnts, hierarchy = cv2.findContours(
mask,
cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)[-2:]
# get the contour with largest aera
C = sorted(cnts, key=lambda x: cv2.contourArea(x), reverse=True)[0]
# polygon approximation
polygon = cv2.approxPolyDP(C, 0.009 * cv2.arcLength(C, True), True)
return polygon
def get_weight_mask_matrix(imA, imB, dist_threshold=5):
"""
Get the weight matrix G that combines two images imA, imB smoothly.
"""
overlapMask = get_overlap_region_mask(imA, imB)
overlapMaskInv = cv2.bitwise_not(overlapMask)
indices = np.where(overlapMask == 255)
imA_diff = cv2.bitwise_and(imA, imA, mask=overlapMaskInv)
imB_diff = cv2.bitwise_and(imB, imB, mask=overlapMaskInv)
G = get_mask(imA).astype(np.float32) / 255.0
polyA = get_outmost_polygon_boundary(imA_diff)
polyB = get_outmost_polygon_boundary(imB_diff)
for y, x in zip(*indices):
# opencv requires a tuple of ints
xy_tuple = tuple([int(x), int(y)])
distToB = cv2.pointPolygonTest(polyB, xy_tuple, True)
if distToB < dist_threshold:
distToA = cv2.pointPolygonTest(polyA, xy_tuple, True)
distToB *= distToB
distToA *= distToA
G[y, x] = distToB / (distToA + distToB)
return G, overlapMask
def make_white_balance(image):
"""
Adjust white balance of an image base on the means of its channels.
"""
B, G, R = cv2.split(image)
m1 = np.mean(B)
m2 = np.mean(G)
m3 = np.mean(R)
K = (m1 + m2 + m3) / 3
c1 = K / m1
c2 = K / m2
c3 = K / m3
B = adjust_luminance(B, c1)
G = adjust_luminance(G, c2)
R = adjust_luminance(R, c3)
return cv2.merge((B, G, R))

165
test_birdview.py Normal file
View File

@@ -0,0 +1,165 @@
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,
"back": 1,
"left": 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, 5)
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):
while self.running:
frames = []
for i, (cap, camera_model) in enumerate(zip(self.caps, self.camera_models)):
ret, frame = cap.read()
if not ret or frame is None:
print(f"[ERROR] Failed to read frame from camera {self.names[i]}")
continue
processed_frame = self.process_frame(frame, camera_model)
frames.append(processed_frame)
if len(frames) == 4: # 确保所有摄像头都正常工作
try:
# 更新鸟瞰图(权重矩阵已预先初始化)
self.birdview.update_frames(frames)
self.birdview.stitch_all_parts()
self.birdview.make_white_balance()
self.birdview.copy_car_image()
# 显示鸟瞰图
birdview_display = cv2.resize(self.birdview.image, (300, 600))
cv2.imshow("Real-time Bird's Eye View", birdview_display)
if cv2.waitKey(1) & 0xFF == ord('q'):
self.running = False
break
except Exception as e:
print(f"[ERROR] Processing error: {e}", file=sys.stderr)
import traceback
traceback.print_exc()
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, (200, 480))
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()

91
test_cameras.py Normal file
View File

@@ -0,0 +1,91 @@
#!/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()

207
test_serial.py Normal file
View File

@@ -0,0 +1,207 @@
import serial
import sys
import time
import threading
import tkinter as tk
from tkinter import ttk, scrolledtext
# ==========================================================
# 串口配置
# ==========================================================
SERIAL_PORT = "/dev/ttyS3"
BAUD_RATE = 115200
TIMEOUT = 1
class SerialHexViewer:
def __init__(self):
self.ser = None
self.running = False
self.rx_thread = None
# ---- 主窗口 ----
self.root = tk.Tk()
self.root.title("串口 HEX 接收器 - {}".format(SERIAL_PORT))
self.root.geometry("700x500")
# ---- 顶部配置栏 ----
top = tk.Frame(self.root)
top.pack(fill='x', padx=8, pady=6)
tk.Label(top, text="串口:").pack(side='left')
self.port_entry = tk.Entry(top, width=12, justify='center')
self.port_entry.insert(0, SERIAL_PORT)
self.port_entry.pack(side='left', padx=4)
tk.Label(top, text="波特率:").pack(side='left')
self.baud_cb = ttk.Combobox(top, width=10,
values=["9600","19200","38400","57600","115200","230400","460800","921600"])
self.baud_cb.set(str(BAUD_RATE))
self.baud_cb.pack(side='left', padx=4)
tk.Label(top, text="数据位:").pack(side='left')
self.data_cb = ttk.Combobox(top, width=4, values=["5","6","7","8"])
self.data_cb.set("8")
self.data_cb.pack(side='left', padx=2)
tk.Label(top, text="停止位:").pack(side='left')
self.stop_cb = ttk.Combobox(top, width=4, values=["1","1.5","2"])
self.stop_cb.set("1")
self.stop_cb.pack(side='left', padx=2)
tk.Label(top, text="校验:").pack(side='left')
self.parity_cb = ttk.Combobox(top, width=6, values=["None","Odd","Even"])
self.parity_cb.set("None")
self.parity_cb.pack(side='left', padx=2)
# ---- 按钮 ----
btn_frame = tk.Frame(self.root)
btn_frame.pack(fill='x', padx=8, pady=2)
self.btn_open = tk.Button(btn_frame, text="打开串口", bg="#2ecc71", fg="white",
width=12, command=self.toggle_port)
self.btn_open.pack(side='left', padx=4)
tk.Button(btn_frame, text="清空显示", bg="#3498db", fg="white",
width=12, command=self.clear_display).pack(side='left', padx=4)
tk.Button(btn_frame, text="退出", bg="#e74c3c", fg="white",
width=8, command=self.shutdown).pack(side='right', padx=4)
# 显示格式选择
self.show_ascii = tk.BooleanVar(value=True)
tk.Checkbutton(btn_frame, text="同时显示 ASCII",
variable=self.show_ascii).pack(side='left', padx=8)
ttk.Separator(self.root, orient='horizontal').pack(fill='x')
# ---- HEX 显示区 ----
self.text_area = scrolledtext.ScrolledText(
self.root, font=("Courier New", 11),
bg="#1e1e1e", fg="#00ff00",
insertbackground="white",
state='disabled'
)
self.text_area.pack(fill='both', expand=True, padx=8, pady=6)
# ---- 状态栏 ----
self.status_var = tk.StringVar(value="状态: 未连接")
tk.Label(self.root, textvariable=self.status_var,
anchor='w', fg="gray").pack(fill='x', padx=8, pady=2)
self.rx_count = 0
# ----------------------------------------------------------
def toggle_port(self):
if not self.running:
self.open_port()
else:
self.close_port()
def open_port(self):
port = self.port_entry.get().strip()
baud = int(self.baud_cb.get())
data = int(self.data_cb.get())
stop = float(self.stop_cb.get())
parity_map = {"None": serial.PARITY_NONE,
"Odd": serial.PARITY_ODD,
"Even": serial.PARITY_EVEN}
parity = parity_map.get(self.parity_cb.get(), serial.PARITY_NONE)
try:
self.ser = serial.Serial(
port=port, baudrate=baud,
bytesize=data, stopbits=stop,
parity=parity, timeout=TIMEOUT
)
self.running = True
self.rx_count = 0
self.btn_open.config(text="关闭串口", bg="#e74c3c")
self.status_var.set("状态: 已连接 {} @ {} bps".format(port, baud))
self.rx_thread = threading.Thread(target=self._read_loop, daemon=True)
self.rx_thread.start()
except Exception as e:
self.status_var.set("状态: 打开失败 - {}".format(e))
def close_port(self):
self.running = False
if self.ser and self.ser.is_open:
self.ser.close()
self.btn_open.config(text="打开串口", bg="#2ecc71")
self.status_var.set("状态: 已断开 | 累计接收: {} 字节".format(self.rx_count))
# ----------------------------------------------------------
def _read_loop(self):
"""独立线程:持续读取串口数据"""
buf = bytearray()
last_flush = time.time()
while self.running:
try:
waiting = self.ser.in_waiting
if waiting > 0:
data = self.ser.read(waiting)
buf.extend(data)
self.rx_count += len(data)
# 每满 16 字节 或 超过 50ms 未刷新 则输出一行
now = time.time()
while len(buf) >= 16:
self._append_hex_line(bytes(buf[:16]))
buf = buf[16:]
if buf and (now - last_flush) >= 0.05:
self._append_hex_line(bytes(buf))
buf.clear()
last_flush = now
else:
# 无数据时刷新剩余缓冲
if buf and (time.time() - last_flush) >= 0.1:
self._append_hex_line(bytes(buf))
buf.clear()
last_flush = time.time()
time.sleep(0.005)
except Exception as e:
if self.running:
self.root.after(0, self.status_var.set,
"状态: 读取错误 - {}".format(e))
break
def _append_hex_line(self, data: bytes):
"""格式化为 HEX 行并追加到显示区(线程安全)"""
ts = time.strftime("%H:%M:%S")
hex_str = " ".join("{:02X}".format(b) for b in data)
# 对齐16字节 = 47字符宽
hex_padded = "{:<47}".format(hex_str)
if self.show_ascii.get():
ascii_str = "".join(chr(b) if 32 <= b < 127 else '.' for b in data)
line = "[{}] {} |{}|".format(ts, hex_padded, ascii_str)
else:
line = "[{}] {}".format(ts, hex_str)
self.root.after(0, self._insert_text, line)
self.root.after(0, self.status_var.set,
"状态: 接收中 | 累计: {} 字节".format(self.rx_count))
def _insert_text(self, line):
self.text_area.config(state='normal')
self.text_area.insert('end', line)
self.text_area.see('end') # 自动滚动到最新
self.text_area.config(state='disabled')
def clear_display(self):
self.text_area.config(state='normal')
self.text_area.delete('1.0', 'end')
self.text_area.config(state='disabled')
def shutdown(self):
self.close_port()
self.root.destroy()
def run(self):
self.root.mainloop()
if __name__ == "__main__":
viewer = SerialHexViewer()
viewer.run()

409
tools/cpu_id.py Normal file
View File

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

1734
web.py Normal file

File diff suppressed because it is too large Load Diff

925
web_back.py Normal file
View File

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

BIN
weights.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

37
yaml/back.yaml Normal file
View File

@@ -0,0 +1,37 @@
%YAML:1.0
---
camera_matrix: !!opencv-matrix
rows: 3
cols: 3
dt: d
data: [ 3.9875655282072148e+02, 0., 6.3788553583130647e+02, 0.,
3.7214138046449295e+02, 3.4915903287538981e+02, 0., 0., 1. ]
dist_coeffs: !!opencv-matrix
rows: 4
cols: 1
dt: d
data: [ 8.6662165805452579e-02, -2.8155159006680735e-02,
-3.5111914522638211e-02, 1.4621518110074484e-02 ]
resolution: !!opencv-matrix
rows: 2
cols: 1
dt: i
data: [ 1280, 720 ]
project_matrix: !!opencv-matrix
rows: 3
cols: 3
dt: d
data: [ -2.7831207035612765e-01, -7.9952425770656199e-01,
3.6823331098543429e+02, -3.7001040335334794e-02,
-8.3751782367968042e-01, 3.0809035280803914e+02,
-1.8647684116597846e-04, -3.3789868216962007e-03, 1. ]
scale_xy: !!opencv-matrix
rows: 2
cols: 1
dt: f
data: [ 6.99999988e-01, 8.00000012e-01 ]
shift_xy: !!opencv-matrix
rows: 2
cols: 1
dt: f
data: [ -90., -100. ]

37
yaml/front.yaml Normal file
View File

@@ -0,0 +1,37 @@
%YAML:1.0
---
camera_matrix: !!opencv-matrix
rows: 3
cols: 3
dt: d
data: [ 3.9875655282072148e+02, 0., 6.3788553583130647e+02, 0.,
3.7214138046449295e+02, 3.4915903287538981e+02, 0., 0., 1. ]
dist_coeffs: !!opencv-matrix
rows: 4
cols: 1
dt: d
data: [ 8.6662165805452579e-02, -2.8155159006680735e-02,
-3.5111914522638211e-02, 1.4621518110074484e-02 ]
resolution: !!opencv-matrix
rows: 2
cols: 1
dt: i
data: [ 1280, 720 ]
project_matrix: !!opencv-matrix
rows: 3
cols: 3
dt: d
data: [ -2.8912819555150965e-01, -7.9463737223081499e-01,
3.6386974611903076e+02, -3.2552806589489218e-02,
-9.2991874851752465e-01, 3.2043709447748540e+02,
-1.8365341465610607e-04, -3.5597129577879948e-03, 1. ]
scale_xy: !!opencv-matrix
rows: 2
cols: 1
dt: f
data: [ 6.99999988e-01, 8.00000012e-01 ]
shift_xy: !!opencv-matrix
rows: 2
cols: 1
dt: f
data: [ -90., -100. ]

37
yaml/left.yaml Normal file
View File

@@ -0,0 +1,37 @@
%YAML:1.0
---
camera_matrix: !!opencv-matrix
rows: 3
cols: 3
dt: d
data: [ 3.9875655282072148e+02, 0., 6.3788553583130647e+02, 0.,
3.7214138046449295e+02, 3.4915903287538981e+02, 0., 0., 1. ]
dist_coeffs: !!opencv-matrix
rows: 4
cols: 1
dt: d
data: [ 8.6662165805452579e-02, -2.8155159006680735e-02,
-3.5111914522638211e-02, 1.4621518110074484e-02 ]
resolution: !!opencv-matrix
rows: 2
cols: 1
dt: i
data: [ 1280, 720 ]
project_matrix: !!opencv-matrix
rows: 3
cols: 3
dt: d
data: [ -2.7185386118188232e-01, -1.0059509545219627e+00,
4.1343808607963717e+02, -8.4441560868839804e-03,
-7.5008165902588730e-01, 2.6485004043446787e+02,
-9.6609321693934345e-05, -3.6679719303539878e-03, 1. ]
scale_xy: !!opencv-matrix
rows: 2
cols: 1
dt: f
data: [ 6.99999988e-01, 8.00000012e-01 ]
shift_xy: !!opencv-matrix
rows: 2
cols: 1
dt: f
data: [ -90., -100. ]

37
yaml/right.yaml Normal file
View File

@@ -0,0 +1,37 @@
%YAML:1.0
---
camera_matrix: !!opencv-matrix
rows: 3
cols: 3
dt: d
data: [ 3.9875655282072148e+02, 0., 6.3788553583130647e+02, 0.,
3.7214138046449295e+02, 3.4915903287538981e+02, 0., 0., 1. ]
dist_coeffs: !!opencv-matrix
rows: 4
cols: 1
dt: d
data: [ 8.6662165805452579e-02, -2.8155159006680735e-02,
-3.5111914522638211e-02, 1.4621518110074484e-02 ]
resolution: !!opencv-matrix
rows: 2
cols: 1
dt: i
data: [ 1280, 720 ]
project_matrix: !!opencv-matrix
rows: 3
cols: 3
dt: d
data: [ -2.5424218277528121e-01, -1.0748104699116694e+00,
4.2445421761109566e+02, 1.7059949027254486e-03,
-8.1104378082894346e-01, 2.8905054031510622e+02,
7.0756810536360643e-05, -3.8683857490701360e-03, 1. ]
scale_xy: !!opencv-matrix
rows: 2
cols: 1
dt: f
data: [ 6.99999988e-01, 8.00000012e-01 ]
shift_xy: !!opencv-matrix
rows: 2
cols: 1
dt: f
data: [ -90., -100. ]

BIN
yolov5s-640-640.rknn Normal file

Binary file not shown.