拍视频版本
199
1.py
Normal 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
@@ -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
@@ -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()
|
||||||
BIN
LJ360.tar.gz
Normal file
206
README.md
Normal 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-03:YOLO融合 雷达预警
|
||||||
|
- 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文件。
|
||||||
|
|
||||||
|
## 联系方式
|
||||||
|
|
||||||
|
如有问题或建议,请联系项目维护者。
|
||||||
BIN
__pycache__/serial.cpython-38.pyc
Normal file
162
bash/disk_tool.sh
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
After Width: | Height: | Size: 2.6 MiB |
BIN
images/back.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
images/car.png
Normal file
|
After Width: | Height: | Size: 275 KiB |
BIN
images/front.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
images/left.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
images/right.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
lib/librknn_api.so
Normal file
BIN
lib/librknnrt.so
Normal file
579910
lj360_camera_keepalive.log
Normal file
731
lj360_manager.sh
Normal 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): 已启动 LJ360,PID: \$!" >> "\$LOG_FILE"
|
||||||
|
|
||||||
|
# 等待应用启动
|
||||||
|
sleep 5
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep \$CHECK_INTERVAL
|
||||||
|
done
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod +x "$WATCHDOG_SCRIPT"
|
||||||
|
chown "$CURRENT_USER:$CURRENT_USER" "$WATCHDOG_SCRIPT"
|
||||||
|
print_success "✅ 看门狗脚本已创建并授权"
|
||||||
|
|
||||||
|
# 创建看门狗 systemd 服务
|
||||||
|
print_progress "🔧 创建看门狗服务文件: /etc/systemd/system/$WATCHDOG_SERVICE"
|
||||||
|
sudo tee /etc/systemd/system/$WATCHDOG_SERVICE > /dev/null << EOF
|
||||||
|
[Unit]
|
||||||
|
Description=LJ360 Watchdog Service
|
||||||
|
After=network.target
|
||||||
|
Wants=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=$CURRENT_USER
|
||||||
|
WorkingDirectory=$PROJECT_DIR
|
||||||
|
ExecStart=$PROJECT_DIR/watchdog_lj360.sh
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
print_success "✅ 看门狗服务文件已部署"
|
||||||
|
|
||||||
|
# 启用并启动看门狗服务
|
||||||
|
print_progress "🚀 启动看门狗服务"
|
||||||
|
sudo systemctl enable $WATCHDOG_SERVICE
|
||||||
|
sudo systemctl start $WATCHDOG_SERVICE
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
if systemctl is-active --quiet $WATCHDOG_SERVICE; then
|
||||||
|
print_success "✅ 看门狗服务启动成功"
|
||||||
|
else
|
||||||
|
print_warning "⚠️ 看门狗服务启动可能有问题,请查看日志"
|
||||||
|
fi
|
||||||
|
|
||||||
|
STEP=$((STEP + 1))
|
||||||
|
read -p "🔘 按 Enter 继续..." -r
|
||||||
|
}
|
||||||
|
|
||||||
|
# --------------------------
|
||||||
|
# Step 3: 启动服务
|
||||||
|
# --------------------------
|
||||||
|
start_service() {
|
||||||
|
print_step $STEP $TOTAL_STEPS "启动 LJ360 全景监控服务"
|
||||||
|
|
||||||
|
# 检查服务文件是否存在
|
||||||
|
if [ ! -f "/etc/systemd/system/$SERVICE_NAME" ]; then
|
||||||
|
print_error "❌ 服务未安装,请先安装服务"
|
||||||
|
read -p "🔘 按 Enter 继续..." -r
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_progress "🚀 正在启动服务: $SERVICE_NAME"
|
||||||
|
sudo systemctl start "$SERVICE_NAME"
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# 检查启动状态
|
||||||
|
if systemctl is-active --quiet "$SERVICE_NAME"; then
|
||||||
|
print_success "✅ 服务启动成功"
|
||||||
|
|
||||||
|
# 检查Web端口
|
||||||
|
sleep 2
|
||||||
|
if netstat -tlnp 2>/dev/null | grep -q ":5000"; then
|
||||||
|
local IP_ADDR=$(hostname -I | awk '{print $1}')
|
||||||
|
print_success "🌐 Web 服务已启动: http://$IP_ADDR:5000"
|
||||||
|
print_success "🌐 本地访问: http://localhost:5000"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_warning "⚠️ 服务启动可能有问题,请查看日志"
|
||||||
|
fi
|
||||||
|
|
||||||
|
STEP=$((STEP + 1))
|
||||||
|
read -p "🔘 按 Enter 继续..." -r
|
||||||
|
}
|
||||||
|
|
||||||
|
# --------------------------
|
||||||
|
# Step 4: 停止服务
|
||||||
|
# --------------------------
|
||||||
|
stop_service() {
|
||||||
|
print_step $STEP $TOTAL_STEPS "停止 LJ360 全景监控服务"
|
||||||
|
|
||||||
|
# 检查服务文件是否存在
|
||||||
|
if [ ! -f "/etc/systemd/system/$SERVICE_NAME" ]; then
|
||||||
|
print_error "❌ 服务未安装,请先安装服务"
|
||||||
|
read -p "🔘 按 Enter 继续..." -r
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_progress "⏹️ 正在停止服务: $SERVICE_NAME"
|
||||||
|
sudo systemctl stop "$SERVICE_NAME"
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# 清理可能的残留进程
|
||||||
|
print_progress "🧹 清理残留进程..."
|
||||||
|
pkill -f "python3 web.py" 2>/dev/null || true
|
||||||
|
|
||||||
|
# 检查停止状态
|
||||||
|
if ! systemctl is-active --quiet "$SERVICE_NAME"; then
|
||||||
|
print_success "✅ 服务停止成功"
|
||||||
|
else
|
||||||
|
print_warning "⚠️ 服务停止可能有问题"
|
||||||
|
fi
|
||||||
|
|
||||||
|
STEP=$((STEP + 1))
|
||||||
|
read -p "🔘 按 Enter 继续..." -r
|
||||||
|
}
|
||||||
|
|
||||||
|
# --------------------------
|
||||||
|
# Step 5: 重启服务
|
||||||
|
# --------------------------
|
||||||
|
restart_service() {
|
||||||
|
print_step $STEP $TOTAL_STEPS "重启 LJ360 全景监控服务"
|
||||||
|
|
||||||
|
# 检查服务文件是否存在
|
||||||
|
if [ ! -f "/etc/systemd/system/$SERVICE_NAME" ]; then
|
||||||
|
print_error "❌ 服务未安装,请先安装服务"
|
||||||
|
read -p "🔘 按 Enter 继续..." -r
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_progress "🔄 正在重启服务: $SERVICE_NAME"
|
||||||
|
sudo systemctl restart "$SERVICE_NAME"
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# 检查重启状态
|
||||||
|
if systemctl is-active --quiet "$SERVICE_NAME"; then
|
||||||
|
print_success "✅ 服务重启成功"
|
||||||
|
|
||||||
|
# 检查Web端口
|
||||||
|
sleep 2
|
||||||
|
if netstat -tlnp 2>/dev/null | grep -q ":5000"; then
|
||||||
|
local IP_ADDR=$(hostname -I | awk '{print $1}')
|
||||||
|
print_success "🌐 Web 服务已重启: http://$IP_ADDR:5000"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_warning "⚠️ 服务重启可能有问题,请查看日志"
|
||||||
|
fi
|
||||||
|
|
||||||
|
STEP=$((STEP + 1))
|
||||||
|
read -p "🔘 按 Enter 继续..." -r
|
||||||
|
}
|
||||||
|
|
||||||
|
# --------------------------
|
||||||
|
# Step 6: 启用开机自启
|
||||||
|
# --------------------------
|
||||||
|
enable_service() {
|
||||||
|
print_step $STEP $TOTAL_STEPS "启用开机自启"
|
||||||
|
|
||||||
|
# 检查服务文件是否存在
|
||||||
|
if [ ! -f "/etc/systemd/system/$SERVICE_NAME" ]; then
|
||||||
|
print_error "❌ 服务未安装,请先安装服务"
|
||||||
|
read -p "🔘 按 Enter 继续..." -r
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_progress "🔔 正在启用开机自启: $SERVICE_NAME"
|
||||||
|
sudo systemctl enable "$SERVICE_NAME"
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
if systemctl is-enabled "$SERVICE_NAME"; then
|
||||||
|
print_success "✅ 开机自启已启用"
|
||||||
|
else
|
||||||
|
print_error "❌ 开机自启启用失败"
|
||||||
|
fi
|
||||||
|
|
||||||
|
STEP=$((STEP + 1))
|
||||||
|
read -p "🔘 按 Enter 继续..." -r
|
||||||
|
}
|
||||||
|
|
||||||
|
# --------------------------
|
||||||
|
# Step 7: 禁用开机自启
|
||||||
|
# --------------------------
|
||||||
|
disable_service() {
|
||||||
|
print_step $STEP $TOTAL_STEPS "禁用开机自启"
|
||||||
|
|
||||||
|
# 检查服务文件是否存在
|
||||||
|
if [ ! -f "/etc/systemd/system/$SERVICE_NAME" ]; then
|
||||||
|
print_error "❌ 服务未安装,请先安装服务"
|
||||||
|
read -p "🔘 按 Enter 继续..." -r
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_progress "🚫 正在禁用开机自启: $SERVICE_NAME"
|
||||||
|
sudo systemctl disable "$SERVICE_NAME"
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
if ! systemctl is-enabled "$SERVICE_NAME"; then
|
||||||
|
print_success "✅ 开机自启已禁用"
|
||||||
|
else
|
||||||
|
print_error "❌ 开机自启禁用失败"
|
||||||
|
fi
|
||||||
|
|
||||||
|
STEP=$((STEP + 1))
|
||||||
|
read -p "🔘 按 Enter 继续..." -r
|
||||||
|
}
|
||||||
|
|
||||||
|
# --------------------------
|
||||||
|
# Step 8: 查看日志
|
||||||
|
# --------------------------
|
||||||
|
view_logs() {
|
||||||
|
print_step $STEP $TOTAL_STEPS "查看实时日志"
|
||||||
|
|
||||||
|
# 检查服务文件是否存在
|
||||||
|
if [ ! -f "/etc/systemd/system/$SERVICE_NAME" ]; then
|
||||||
|
print_error "❌ 服务未安装,请先安装服务"
|
||||||
|
read -p "🔘 按 Enter 继续..." -r
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_progress "📜 请选择要查看的日志类型:"
|
||||||
|
echo "1) 📋 查看 systemd 服务日志"
|
||||||
|
echo "2) 📄 查看应用日志文件"
|
||||||
|
echo "3) ⚠️ 查看错误日志文件"
|
||||||
|
echo "4) 🛡️ 查看看门狗日志"
|
||||||
|
print_divider
|
||||||
|
|
||||||
|
read -p "👉 请选择 [1-4]: " log_choice
|
||||||
|
|
||||||
|
case $log_choice in
|
||||||
|
1)
|
||||||
|
print_progress "📜 正在打开 systemd 服务日志(按 Ctrl+C 退出)"
|
||||||
|
echo ""
|
||||||
|
sudo journalctl -u "$SERVICE_NAME" -f
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
print_progress "📄 正在查看应用日志文件: $PROJECT_DIR/lj360.log"
|
||||||
|
echo ""
|
||||||
|
if [ -f "$PROJECT_DIR/lj360.log" ]; then
|
||||||
|
tail -50 "$PROJECT_DIR/lj360.log"
|
||||||
|
echo ""
|
||||||
|
echo "按 Enter 查看更多,或按 Ctrl+C 退出..."
|
||||||
|
tail -f "$PROJECT_DIR/lj360.log"
|
||||||
|
else
|
||||||
|
print_error "❌ 日志文件不存在"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
print_progress "⚠️ 正在查看错误日志文件: $PROJECT_DIR/lj360_error.log"
|
||||||
|
echo ""
|
||||||
|
if [ -f "$PROJECT_DIR/lj360_error.log" ]; then
|
||||||
|
tail -50 "$PROJECT_DIR/lj360_error.log"
|
||||||
|
else
|
||||||
|
print_warning "⚠️ 错误日志文件不存在"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
4)
|
||||||
|
print_progress "🛡️ 正在查看看门狗日志文件: $PROJECT_DIR/watchdog.log"
|
||||||
|
echo ""
|
||||||
|
if [ -f "$PROJECT_DIR/watchdog.log" ]; then
|
||||||
|
tail -50 "$PROJECT_DIR/watchdog.log"
|
||||||
|
else
|
||||||
|
print_warning "⚠️ 看门狗日志文件不存在"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
print_error "❌ 无效选项"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
STEP=$((STEP + 1))
|
||||||
|
read -p "🔘 日志查看结束,按 Enter 继续..." -r
|
||||||
|
}
|
||||||
|
|
||||||
|
# --------------------------
|
||||||
|
# Step 9: 查看系统状态
|
||||||
|
# --------------------------
|
||||||
|
view_status() {
|
||||||
|
print_step $STEP $TOTAL_STEPS "查看详细系统状态"
|
||||||
|
|
||||||
|
echo -e "${CYAN}【📊 系统资源状态】${NC}"
|
||||||
|
print_divider
|
||||||
|
|
||||||
|
# CPU使用率
|
||||||
|
echo -e "${BLUE}● CPU 使用率:${NC}"
|
||||||
|
top -bn1 | grep "Cpu(s)" | awk '{print " " $2 "% user, " $4 "% system, " $8 "% idle"}'
|
||||||
|
|
||||||
|
# 内存使用
|
||||||
|
echo -e "${BLUE}● 内存使用:${NC}"
|
||||||
|
free -h | awk '/^Mem:/ {print " " $3 " / " $2 " (" $3/$2*100 "%) used"}'
|
||||||
|
|
||||||
|
# 磁盘空间
|
||||||
|
echo -e "${BLUE}● 磁盘空间:${NC}"
|
||||||
|
df -h /home | awk 'NR==2 {print " " $3 " / " $2 " (" $5 ") used"}'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}【🔍 进程状态】${NC}"
|
||||||
|
print_divider
|
||||||
|
|
||||||
|
# LJ360相关进程
|
||||||
|
if pgrep -f "python3 web.py" > /dev/null; then
|
||||||
|
echo -e "${GREEN}● LJ360 进程正在运行 ✅${NC}"
|
||||||
|
ps aux | grep "python3 web.py" | grep -v grep | awk '{print " PID: " $2 ", CPU: " $3 "%, MEM: " $4 "%, CMD: " $11}'
|
||||||
|
else
|
||||||
|
echo -e "${RED}● LJ360 进程未运行 ❌${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}【📈 服务运行时间】${NC}"
|
||||||
|
print_divider
|
||||||
|
|
||||||
|
if systemctl is-active --quiet $SERVICE_NAME; then
|
||||||
|
UPTIME=$(systemctl show $SERVICE_NAME --property=ActiveEnterTimestamp | awk -F= '{print $2}')
|
||||||
|
echo -e "${GREEN}● 服务启动时间: $UPTIME${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if systemctl is-active --quiet $WATCHDOG_SERVICE; then
|
||||||
|
WATCHDOG_UPTIME=$(systemctl show $WATCHDOG_SERVICE --property=ActiveEnterTimestamp | awk -F= '{print $2}')
|
||||||
|
echo -e "${GREEN}● 看门狗启动时间: $WATCHDOG_UPTIME${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
STEP=$((STEP + 1))
|
||||||
|
read -p "🔘 按 Enter 继续..." -r
|
||||||
|
}
|
||||||
|
|
||||||
|
# --------------------------
|
||||||
|
# Step 10: 卸载服务
|
||||||
|
# --------------------------
|
||||||
|
uninstall_service() {
|
||||||
|
print_step $STEP $TOTAL_STEPS "卸载 LJ360 全景监控服务"
|
||||||
|
echo -e "${RED}⚠ 警告:此操作将卸载 LJ360 全景监控服务!${NC}"
|
||||||
|
echo -n "✅ 确定要卸载吗?(y/N): "
|
||||||
|
read -r confirm
|
||||||
|
if [[ $confirm =~ ^[Yy]$ ]]; then
|
||||||
|
print_progress "⏹️ 停止服务..."
|
||||||
|
sudo systemctl stop "$SERVICE_NAME" 2>/dev/null || true
|
||||||
|
sudo systemctl stop "$WATCHDOG_SERVICE" 2>/dev/null || true
|
||||||
|
|
||||||
|
print_progress "🚫 禁用开机自启..."
|
||||||
|
sudo systemctl disable "$SERVICE_NAME" 2>/dev/null || true
|
||||||
|
sudo systemctl disable "$WATCHDOG_SERVICE" 2>/dev/null || true
|
||||||
|
|
||||||
|
print_progress "🗑️ 删除 systemd 服务文件..."
|
||||||
|
sudo rm -f /etc/systemd/system/"$SERVICE_NAME"
|
||||||
|
sudo rm -f /etc/systemd/system/"$WATCHDOG_SERVICE"
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl reset-failed
|
||||||
|
|
||||||
|
print_progress "🗑️ 删除启动脚本..."
|
||||||
|
rm -f "$PROJECT_DIR/start_lj360.sh"
|
||||||
|
rm -f "$PROJECT_DIR/watchdog_lj360.sh"
|
||||||
|
|
||||||
|
print_progress "🔪 清理进程..."
|
||||||
|
pkill -f "python3 web.py" 2>/dev/null || true
|
||||||
|
pkill -f "watchdog_lj360.sh" 2>/dev/null || true
|
||||||
|
|
||||||
|
print_success "✅ LJ360 全景监控服务卸载完成"
|
||||||
|
print_warning "📝 日志文件保留在: $PROJECT_DIR/"
|
||||||
|
print_warning " - lj360.log"
|
||||||
|
print_warning " - lj360_error.log"
|
||||||
|
print_warning " - watchdog.log"
|
||||||
|
else
|
||||||
|
print_warning "❌ 取消卸载操作"
|
||||||
|
fi
|
||||||
|
|
||||||
|
STEP=$((STEP + 1))
|
||||||
|
read -p "🔘 按 Enter 继续..." -r
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
# 启动主菜单
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
# 检查是否在正确的目录运行
|
||||||
|
if [ ! -d "$PROJECT_DIR" ]; then
|
||||||
|
print_error "❌ 项目目录不存在: $PROJECT_DIR"
|
||||||
|
print_error "请确认项目路径是否正确,或者修改脚本中的 PROJECT_DIR 变量"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
main_menu
|
||||||
18
model/anchors_yolov5.txt
Normal 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
|
After Width: | Height: | Size: 177 KiB |
80
model/coco_80_labels_list.txt
Normal 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
@@ -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
BIN
py_utils/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
py_utils/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
py_utils/__pycache__/coco_utils.cpython-313.pyc
Normal file
BIN
py_utils/__pycache__/coco_utils.cpython-38.pyc
Normal file
BIN
py_utils/__pycache__/rknn_executor.cpython-313.pyc
Normal file
BIN
py_utils/__pycache__/rknn_executor.cpython-38.pyc
Normal file
176
py_utils/coco_utils.py
Normal 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
@@ -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
|
||||||
|
|
||||||
52
py_utils/pytorch_executor.py
Normal 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
|
||||||
31
py_utils/rknn_executor (copy 1).py
Normal 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
@@ -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
@@ -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()
|
||||||
71
run_get_projection_maps.py
Normal 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()
|
||||||
53
run_get_weight_matrices.py
Normal 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
@@ -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
|
||||||
|
|
||||||
|
# 启用 OpenCL(Mali-G610)
|
||||||
|
if cv2.ocl.haveOpenCL():
|
||||||
|
cv2.ocl.setUseOpenCL(True)
|
||||||
|
print("✅ OpenCL is ON — using Mali-G610 GPU for acceleration")
|
||||||
|
else:
|
||||||
|
print("⚠️ OpenCL not available")
|
||||||
|
|
||||||
|
# ------ YOLO 配置 -----------
|
||||||
|
YOLO_MODEL_PATH = './yolov5s-640-640.rknn'
|
||||||
|
OBJ_THRESH = 0.6
|
||||||
|
NMS_THRESH = 0.6
|
||||||
|
IMG_SIZE = (640, 640)
|
||||||
|
CLASSES = ("person",)
|
||||||
|
|
||||||
|
ANCHORS_FILE = './model/anchors_yolov5.txt'
|
||||||
|
with open(ANCHORS_FILE, 'r') as f:
|
||||||
|
values = [float(_v) for _v in f.readlines()]
|
||||||
|
ANCHORS = np.array(values).reshape(3, -1, 2).tolist()
|
||||||
|
|
||||||
|
|
||||||
|
# ========== YOLO 后处理函数(保持不变)==========
|
||||||
|
def filter_boxes(boxes, box_confidences, box_class_probs):
|
||||||
|
box_confidences = box_confidences.reshape(-1)
|
||||||
|
class_max_score = np.max(box_class_probs, axis=-1)
|
||||||
|
classes = np.argmax(box_class_probs, axis=-1)
|
||||||
|
_class_pos = np.where(class_max_score * box_confidences >= OBJ_THRESH)
|
||||||
|
scores = (class_max_score * box_confidences)[_class_pos]
|
||||||
|
boxes = boxes[_class_pos]
|
||||||
|
classes = classes[_class_pos]
|
||||||
|
return boxes, classes, scores
|
||||||
|
|
||||||
|
def nms_boxes(boxes, scores):
|
||||||
|
x = boxes[:, 0]; y = boxes[:, 1]; w = boxes[:, 2] - boxes[:, 0]; h = boxes[:, 3] - boxes[:, 1]
|
||||||
|
areas = w * h; order = scores.argsort()[::-1]
|
||||||
|
keep = []
|
||||||
|
while order.size > 0:
|
||||||
|
i = order[0]; keep.append(i)
|
||||||
|
xx1 = np.maximum(x[i], x[order[1:]]); yy1 = np.maximum(y[i], y[order[1:]])
|
||||||
|
xx2 = np.minimum(x[i] + w[i], x[order[1:]] + w[order[1:]]); yy2 = np.minimum(y[i] + h[i], y[order[1:]] + h[order[1:]])
|
||||||
|
w1 = np.maximum(0.0, xx2 - xx1 + 0.00001); h1 = np.maximum(0.0, yy2 - yy1 + 0.00001)
|
||||||
|
inter = w1 * h1
|
||||||
|
ovr = inter / (areas[i] + areas[order[1:]] - inter)
|
||||||
|
inds = np.where(ovr <= NMS_THRESH)[0]
|
||||||
|
order = order[inds + 1]
|
||||||
|
return np.array(keep)
|
||||||
|
|
||||||
|
def box_process(position, anchors):
|
||||||
|
grid_h, grid_w = position.shape[2:4]
|
||||||
|
col, row = np.meshgrid(np.arange(0, grid_w), np.arange(0, grid_h))
|
||||||
|
col = col.reshape(1, 1, grid_h, grid_w); row = row.reshape(1, 1, grid_h, grid_w)
|
||||||
|
grid = np.concatenate((col, row), axis=1)
|
||||||
|
stride = np.array([IMG_SIZE[1] // grid_h, IMG_SIZE[0] // grid_w]).reshape(1, 2, 1, 1)
|
||||||
|
col = col.repeat(len(anchors), axis=0); row = row.repeat(len(anchors), axis=0)
|
||||||
|
anchors = np.array(anchors).reshape(*anchors.shape, 1, 1)
|
||||||
|
box_xy = position[:, :2, :, :] * 2 - 0.5
|
||||||
|
box_wh = pow(position[:, 2:4, :, :] * 2, 2) * anchors
|
||||||
|
box_xy += grid; box_xy *= stride
|
||||||
|
box = np.concatenate((box_xy, box_wh), axis=1)
|
||||||
|
xyxy = np.copy(box)
|
||||||
|
xyxy[:, 0, :, :] = box[:, 0, :, :] - box[:, 2, :, :] / 2
|
||||||
|
xyxy[:, 1, :, :] = box[:, 1, :, :] - box[:, 3, :, :] / 2
|
||||||
|
xyxy[:, 2, :, :] = box[:, 0, :, :] + box[:, 2, :, :] / 2
|
||||||
|
xyxy[:, 3, :, :] = box[:, 1, :, :] + box[:, 3, :, :] / 2
|
||||||
|
return xyxy
|
||||||
|
|
||||||
|
def post_process(input_data, anchors):
|
||||||
|
boxes, scores, classes_conf = [], [], []
|
||||||
|
input_data = [_in.reshape([len(anchors[0]), -1] + list(_in.shape[-2:])) for _in in input_data]
|
||||||
|
for i in range(len(input_data)):
|
||||||
|
boxes.append(box_process(input_data[i][:, :4, :, :], anchors[i]))
|
||||||
|
scores.append(input_data[i][:, 4:5, :, :])
|
||||||
|
classes_conf.append(input_data[i][:, 5:, :, :])
|
||||||
|
def sp_flatten(_in):
|
||||||
|
ch = _in.shape[1]; _in = _in.transpose(0, 2, 3, 1); return _in.reshape(-1, ch)
|
||||||
|
boxes = [sp_flatten(_v) for _v in boxes]
|
||||||
|
classes_conf = [sp_flatten(_v) for _v in classes_conf]
|
||||||
|
scores = [sp_flatten(_v) for _v in scores]
|
||||||
|
boxes = np.concatenate(boxes); classes_conf = np.concatenate(classes_conf); scores = np.concatenate(scores)
|
||||||
|
boxes, classes, scores = filter_boxes(boxes, scores, classes_conf)
|
||||||
|
nboxes, nclasses, nscores = [], [], []
|
||||||
|
for c in set(classes):
|
||||||
|
inds = np.where(classes == c)
|
||||||
|
b = boxes[inds]; c = classes[inds]; s = scores[inds]
|
||||||
|
keep = nms_boxes(b, s)
|
||||||
|
if len(keep) != 0:
|
||||||
|
nboxes.append(b[keep]); nclasses.append(c[keep]); nscores.append(s[keep])
|
||||||
|
if not nclasses: return None, None, None
|
||||||
|
boxes = np.concatenate(nboxes); classes = np.concatenate(nclasses); scores = np.concatenate(nscores)
|
||||||
|
return boxes, classes, scores
|
||||||
|
|
||||||
|
def draw_detections(image, boxes, scores, classes):
|
||||||
|
if boxes is None: return image
|
||||||
|
for box, score, cl in zip(boxes, scores, classes):
|
||||||
|
if CLASSES[cl] != "person": continue
|
||||||
|
top, left, right, bottom = [int(_b) for _b in box]
|
||||||
|
cv2.rectangle(image, (top, left), (right, bottom), (0, 255, 0), 2)
|
||||||
|
label = f'person: {score:.2f}'
|
||||||
|
(w, h), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
|
||||||
|
cv2.rectangle(image, (top, left - h - 5), (top + w, left), (0, 255, 0), -1)
|
||||||
|
cv2.putText(image, label, (top, left - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
|
||||||
|
return image
|
||||||
|
|
||||||
|
|
||||||
|
# ========== 主类 ==========
|
||||||
|
class MultiCameraBirdView:
|
||||||
|
def __init__(self):
|
||||||
|
self.running = True
|
||||||
|
self.names = settings.camera_names
|
||||||
|
self.yamls = [os.path.join(os.getcwd(), "yaml", name + ".yaml") for name in self.names]
|
||||||
|
self.camera_models = [
|
||||||
|
FisheyeCameraModel(camera_file, camera_name)
|
||||||
|
for camera_file, camera_name in zip(self.yamls, self.names)
|
||||||
|
]
|
||||||
|
self.which_cameras = {"front": 0, "back": 2, "left": 1, "right": 3}
|
||||||
|
self.caps = []
|
||||||
|
|
||||||
|
print("[INFO] 初始化摄像头...")
|
||||||
|
for name in self.names:
|
||||||
|
cap = cv2.VideoCapture(self.which_cameras[name], cv2.CAP_V4L2)
|
||||||
|
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*"YUYV"))
|
||||||
|
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)
|
||||||
|
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)
|
||||||
|
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
||||||
|
if not cap.isOpened():
|
||||||
|
print(f"[ERROR] 无法打开 {name} 摄像头", file=sys.stderr)
|
||||||
|
self.running = False
|
||||||
|
return
|
||||||
|
self.caps.append(cap)
|
||||||
|
|
||||||
|
self.birdview = BirdView()
|
||||||
|
self._initialize_weights()
|
||||||
|
|
||||||
|
# 预警状态
|
||||||
|
self.alerts = {name: False for name in self.names}
|
||||||
|
self.alert_lock = threading.Lock()
|
||||||
|
|
||||||
|
# YOLO 模型
|
||||||
|
try:
|
||||||
|
self.yolo_model = RKNN_model_container(YOLO_MODEL_PATH, target='rk3588')
|
||||||
|
self.co_helper = COCO_test_helper(enable_letter_box=True)
|
||||||
|
print("[INFO] YOLO 模型加载成功")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] YOLO 模型加载失败: {e}")
|
||||||
|
self.yolo_model = None
|
||||||
|
|
||||||
|
# 共享缓冲区
|
||||||
|
self.undistorted_buffer = CameraFrameBuffer(self.names)
|
||||||
|
self.detection_buffer = DetectionResultBuffer(self.names)
|
||||||
|
self.shared_display_buffer = SharedFrameBuffer() # 用于 MJPEG
|
||||||
|
|
||||||
|
# 当前视图控制
|
||||||
|
self.current_view = "front"
|
||||||
|
self.current_view_lock = threading.Lock()
|
||||||
|
|
||||||
|
# 启动摄像头线程
|
||||||
|
self.camera_threads = []
|
||||||
|
for cap, model, name in zip(self.caps, self.camera_models, self.names):
|
||||||
|
thread = threading.Thread(target=self.camera_reader_thread, args=(cap, model, name), daemon=True)
|
||||||
|
thread.start()
|
||||||
|
self.camera_threads.append(thread)
|
||||||
|
|
||||||
|
# 启动 AI 检测线程
|
||||||
|
if self.yolo_model is not None:
|
||||||
|
self.ai_thread = threading.Thread(target=self.ai_detection_thread, daemon=True)
|
||||||
|
self.ai_thread.start()
|
||||||
|
else:
|
||||||
|
self.ai_thread = None
|
||||||
|
|
||||||
|
def _initialize_weights(self):
|
||||||
|
try:
|
||||||
|
images = [os.path.join(os.getcwd(), "images", name + ".png") for name in self.names]
|
||||||
|
static_frames = []
|
||||||
|
for img_path, cam_model in zip(images, self.camera_models):
|
||||||
|
img = cv2.imread(img_path)
|
||||||
|
if img is None:
|
||||||
|
img = np.zeros((1080, 1920, 3), dtype=np.uint8)
|
||||||
|
img = cam_model.undistort(img)
|
||||||
|
img = cam_model.project(img)
|
||||||
|
img = cam_model.flip(img)
|
||||||
|
static_frames.append(img)
|
||||||
|
if len(static_frames) == 4:
|
||||||
|
self.birdview.get_weights_and_masks(static_frames)
|
||||||
|
print("[INFO] 权重矩阵初始化成功")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] 权重初始化失败: {e}")
|
||||||
|
|
||||||
|
def camera_reader_thread(self, cap, model, name):
|
||||||
|
"""摄像头读取 + 去畸变线程"""
|
||||||
|
while self.running:
|
||||||
|
ret, frame = cap.read()
|
||||||
|
if ret:
|
||||||
|
undistorted = model.undistort(frame)
|
||||||
|
self.undistorted_buffer.update(name, undistorted)
|
||||||
|
else:
|
||||||
|
print(f"[WARN] {name} 摄像头读取失败")
|
||||||
|
break
|
||||||
|
|
||||||
|
def detect_persons(self, image):
|
||||||
|
if self.yolo_model is None:
|
||||||
|
return image, [], []
|
||||||
|
try:
|
||||||
|
orig_h, orig_w = image.shape[:2]
|
||||||
|
pad_color = (0, 0, 0)
|
||||||
|
img_preprocessed = self.co_helper.letter_box(
|
||||||
|
im=image.copy(),
|
||||||
|
new_shape=(IMG_SIZE[1], IMG_SIZE[0]),
|
||||||
|
pad_color=pad_color
|
||||||
|
)
|
||||||
|
outputs = self.yolo_model.run([np.expand_dims(img_preprocessed, 0)])
|
||||||
|
boxes, classes, scores = post_process(outputs, ANCHORS)
|
||||||
|
if boxes is not None:
|
||||||
|
real_boxes = self.co_helper.get_real_box(boxes)
|
||||||
|
person_boxes, person_scores = [], []
|
||||||
|
for i in range(len(real_boxes)):
|
||||||
|
if classes[i] < len(CLASSES) and CLASSES[classes[i]] == "person":
|
||||||
|
box = real_boxes[i].copy()
|
||||||
|
box = np.clip(box, [0, 0, 0, 0], [orig_w, orig_h, orig_w, orig_h])
|
||||||
|
person_boxes.append(box)
|
||||||
|
person_scores.append(scores[i])
|
||||||
|
if person_boxes:
|
||||||
|
image = draw_detections(image, np.array(person_boxes), np.array(person_scores), np.zeros(len(person_boxes), dtype=int))
|
||||||
|
return image, person_boxes, person_scores
|
||||||
|
else:
|
||||||
|
return image, [], []
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] YOLO检测失败: {e}")
|
||||||
|
return image, [], []
|
||||||
|
|
||||||
|
def ai_detection_thread(self):
|
||||||
|
detection_interval = 3
|
||||||
|
frame_count = 0
|
||||||
|
while self.running:
|
||||||
|
with self.current_view_lock:
|
||||||
|
view = self.current_view
|
||||||
|
success, frame = self.undistorted_buffer.get(view)
|
||||||
|
if success:
|
||||||
|
frame_count += 1
|
||||||
|
if frame_count % detection_interval == 0:
|
||||||
|
img_with_det, boxes, scores = self.detect_persons(frame)
|
||||||
|
self.detection_buffer.update(view, img_with_det, boxes, scores)
|
||||||
|
with self.alert_lock:
|
||||||
|
if boxes:
|
||||||
|
self.alerts[view] = True
|
||||||
|
for v in self.alerts:
|
||||||
|
if v != view:
|
||||||
|
self.alerts[v] = False
|
||||||
|
else:
|
||||||
|
for v in self.alerts:
|
||||||
|
self.alerts[v] = False
|
||||||
|
else:
|
||||||
|
self.detection_buffer.update(view, frame, [], [])
|
||||||
|
time.sleep(0.001)
|
||||||
|
|
||||||
|
def overlay_alert(self, birdview_img):
|
||||||
|
h, w = birdview_img.shape[:2]
|
||||||
|
overlay = birdview_img.copy()
|
||||||
|
alpha = 0.2
|
||||||
|
red = (0, 0, 200)
|
||||||
|
margin_f_b = int(min(h, w) * 0.07)
|
||||||
|
margin_l_r = int(min(h, w) * 0.15)
|
||||||
|
with self.alert_lock:
|
||||||
|
alerts = self.alerts.copy()
|
||||||
|
if alerts["front"]: cv2.rectangle(overlay, (0, 0), (w, margin_f_b), red, -1)
|
||||||
|
if alerts["back"]: cv2.rectangle(overlay, (0, h - margin_f_b), (w, h), red, -1)
|
||||||
|
if alerts["left"]: cv2.rectangle(overlay, (0, 0), (margin_l_r, h), red, -1)
|
||||||
|
if alerts["right"]: cv2.rectangle(overlay, (w - margin_l_r, 0), (w, h), red, -1)
|
||||||
|
return cv2.addWeighted(birdview_img, 1 - alpha, overlay, alpha, 0)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
h_display, w_display = 720, 1280
|
||||||
|
w_bird = w_display // 3
|
||||||
|
w_single = w_display - w_bird
|
||||||
|
|
||||||
|
while self.running:
|
||||||
|
# 1. 获取四路去畸变帧 → project → 拼接鸟瞰图
|
||||||
|
processed_frames = []
|
||||||
|
for name in self.names:
|
||||||
|
success, undist_frame = self.undistorted_buffer.get(name)
|
||||||
|
if success:
|
||||||
|
# 使用 UMat 加速 warpPerspective
|
||||||
|
uimg = cv2.UMat(undist_frame)
|
||||||
|
uresult = cv2.warpPerspective(
|
||||||
|
uimg,
|
||||||
|
self.camera_models[self.names.index(name)].project_matrix,
|
||||||
|
self.camera_models[self.names.index(name)].project_shape,
|
||||||
|
flags=cv2.INTER_LINEAR,
|
||||||
|
borderMode=cv2.BORDER_CONSTANT
|
||||||
|
)
|
||||||
|
p_frame = uresult.get()
|
||||||
|
p_frame = self.camera_models[self.names.index(name)].flip(p_frame)
|
||||||
|
processed_frames.append(p_frame)
|
||||||
|
else:
|
||||||
|
processed_frames.append(np.zeros((600, 800, 3), dtype=np.uint8))
|
||||||
|
|
||||||
|
self.birdview.update_frames(processed_frames)
|
||||||
|
self.birdview.stitch_all_parts()
|
||||||
|
self.birdview.make_white_balance()
|
||||||
|
self.birdview.copy_car_image()
|
||||||
|
|
||||||
|
# 2. 获取当前视图的 AI 检测结果
|
||||||
|
with self.current_view_lock:
|
||||||
|
view = self.current_view
|
||||||
|
det_img, _, _ = self.detection_buffer.get(view)
|
||||||
|
if det_img is None:
|
||||||
|
det_img = np.zeros((h_display, w_single, 3), dtype=np.uint8)
|
||||||
|
|
||||||
|
# 3. 拼接显示
|
||||||
|
bird_resized = cv2.resize(self.overlay_alert(self.birdview.image), (w_bird, h_display))
|
||||||
|
single_resized = cv2.resize(det_img, (w_single, h_display))
|
||||||
|
display = np.hstack((bird_resized, single_resized))
|
||||||
|
|
||||||
|
self.shared_display_buffer.update(display)
|
||||||
|
|
||||||
|
cv2.namedWindow('Video', cv2.WND_PROP_FULLSCREEN)
|
||||||
|
cv2.setWindowProperty('Video', cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
|
||||||
|
cv2.imshow("Video", display)
|
||||||
|
|
||||||
|
key = cv2.waitKey(1) & 0xFF
|
||||||
|
if key == ord('q'):
|
||||||
|
self.running = False
|
||||||
|
elif key == ord('1'):
|
||||||
|
with self.current_view_lock:
|
||||||
|
self.current_view = "front"
|
||||||
|
elif key == ord('2'):
|
||||||
|
with self.current_view_lock:
|
||||||
|
self.current_view = "back"
|
||||||
|
elif key == ord('3'):
|
||||||
|
with self.current_view_lock:
|
||||||
|
self.current_view = "left"
|
||||||
|
elif key == ord('4'):
|
||||||
|
with self.current_view_lock:
|
||||||
|
self.current_view = "right"
|
||||||
|
elif key == ord('0'):
|
||||||
|
with self.alert_lock:
|
||||||
|
for k in self.alerts:
|
||||||
|
self.alerts[k] = False
|
||||||
|
|
||||||
|
for cap in self.caps:
|
||||||
|
cap.release()
|
||||||
|
cv2.destroyAllWindows()
|
||||||
|
|
||||||
|
|
||||||
|
# ========== 辅助类 ==========
|
||||||
|
class SharedFrameBuffer:
|
||||||
|
def __init__(self):
|
||||||
|
self._frame = None
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
self._has_frame = False
|
||||||
|
|
||||||
|
def update(self, frame: np.ndarray):
|
||||||
|
with self._lock:
|
||||||
|
self._frame = frame.copy()
|
||||||
|
self._has_frame = True
|
||||||
|
|
||||||
|
def get_frame(self):
|
||||||
|
with self._lock:
|
||||||
|
if self._has_frame and self._frame is not None:
|
||||||
|
return True, self._frame.copy()
|
||||||
|
else:
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
|
||||||
|
# ========== 主函数 ==========
|
||||||
|
def main():
|
||||||
|
print("🚀 启动实时四路环视系统...")
|
||||||
|
multi_cam = MultiCameraBirdView()
|
||||||
|
if not multi_cam.running:
|
||||||
|
print("[ERROR] 摄像头初始化失败")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 启动 MJPEG 流
|
||||||
|
try:
|
||||||
|
mjpeg_server = MJPEGServer(multi_cam.shared_display_buffer, host="0.0.0.0", port=8080)
|
||||||
|
mjpeg_server.start()
|
||||||
|
print("[INFO] MJPEG 流已启动: http://<IP>:8080")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[WARN] MJPEG 流启动失败: {e}")
|
||||||
|
|
||||||
|
multi_cam.run()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
40
shared_buffer.py
Normal 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
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"carousel_interval": 4,
|
||||||
|
"brightness": 50,
|
||||||
|
"radar_unit": 0,
|
||||||
|
"show_radar": 0,
|
||||||
|
"radar_alarm_dist": 70,
|
||||||
|
"display_mode": 0
|
||||||
|
}
|
||||||
6
surround_view/__init__.py
Normal 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
|
||||||
BIN
surround_view/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
surround_view/__pycache__/base_thread.cpython-38.pyc
Normal file
BIN
surround_view/__pycache__/birdview.cpython-38.pyc
Normal file
BIN
surround_view/__pycache__/capture_thread.cpython-38.pyc
Normal file
BIN
surround_view/__pycache__/fisheye_camera.cpython-38.pyc
Normal file
BIN
surround_view/__pycache__/imagebuffer.cpython-38.pyc
Normal file
BIN
surround_view/__pycache__/param_settings.cpython-38.pyc
Normal file
BIN
surround_view/__pycache__/process_thread.cpython-38.pyc
Normal file
BIN
surround_view/__pycache__/simple_gui.cpython-38.pyc
Normal file
BIN
surround_view/__pycache__/structures.cpython-38.pyc
Normal file
BIN
surround_view/__pycache__/utils.cpython-38.pyc
Normal file
52
surround_view/base_thread.py
Normal 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
@@ -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)
|
||||||
113
surround_view/capture_thread.py
Normal 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()
|
||||||
128
surround_view/fisheye_camera.py
Normal 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()
|
||||||
161
surround_view/imagebuffer.py
Normal 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))
|
||||||
118
surround_view/param_settings.py
Normal 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)
|
||||||
|
|
||||||
81
surround_view/process_thread.py
Normal 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
@@ -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
|
||||||
12
surround_view/structures.py
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||||
925
web_back.py
Normal 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
|
After Width: | Height: | Size: 125 KiB |
37
yaml/back.yaml
Normal 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
@@ -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
@@ -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
@@ -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. ]
|
||||||