拍视频版本
This commit is contained in:
474
2.py
Normal file
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()
|
||||
Reference in New Issue
Block a user