init
This commit is contained in:
@@ -0,0 +1,825 @@
|
||||
"""
|
||||
Orisys SDK 实时监控 — Qt 统一界面
|
||||
|
||||
将所有可视化和曲线整合到一个窗口中,适合销售演示使用。
|
||||
SDK version: 0.3.1
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||
import cv2
|
||||
def _fix_qt_plugin_path():
|
||||
"""OpenCV redirects Qt to cv2/qt/plugins; use PyQt5's plugins instead."""
|
||||
try:
|
||||
from PyQt5.QtCore import QLibraryInfo
|
||||
os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = QLibraryInfo.location(
|
||||
QLibraryInfo.PluginsPath
|
||||
)
|
||||
except Exception:
|
||||
path = os.environ.get("QT_QPA_PLATFORM_PLUGIN_PATH", "")
|
||||
if "cv2" in path.replace("\\", "/"):
|
||||
os.environ.pop("QT_QPA_PLATFORM_PLUGIN_PATH", None)
|
||||
_fix_qt_plugin_path()
|
||||
import orisys
|
||||
import numpy as np
|
||||
import time
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
from scipy.signal import butter, filtfilt
|
||||
|
||||
# Force curve sampling rate (Hz), matches typical sensor loop ~30 FPS.
|
||||
FORCE_SAMPLE_RATE_HZ = 30.0
|
||||
PLOT_BUFFER_POINTS = 120
|
||||
PLOT_WINDOW_SEC = PLOT_BUFFER_POINTS / FORCE_SAMPLE_RATE_HZ # 4 s window
|
||||
# Minimum y-axis span so autoscale does not over-zoom on small fluctuations.
|
||||
Y_AXIS_MIN_SPAN = 20000
|
||||
CURVE_THEME_COLOR = '#00E5FF'
|
||||
|
||||
from PyQt5.QtWidgets import (
|
||||
QMainWindow, QWidget, QHBoxLayout, QVBoxLayout,
|
||||
QLabel, QApplication, QSplitter, QPlainTextEdit,
|
||||
QPushButton, QFileDialog, QLineEdit,
|
||||
)
|
||||
from PyQt5.QtCore import Qt, QTimer
|
||||
from PyQt5 import QtGui
|
||||
from PyQt5.QtGui import QImage, QPixmap, QFont, QIcon
|
||||
|
||||
try:
|
||||
import pyqtgraph as pg
|
||||
except Exception:
|
||||
print("PyQtGraph is not installed. Please install it using 'pip install pyqt5 pyqtgraph'.")
|
||||
sys.exit(1)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 工具
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def cv2_to_pixmap(img_bgr: np.ndarray) -> QPixmap:
|
||||
rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
|
||||
h, w, ch = rgb.shape
|
||||
qimg = QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888)
|
||||
return QPixmap.fromImage(qimg)
|
||||
|
||||
def draw_magnitude_map(vfield, threshold=3, colormap=cv2.COLORMAP_JET) -> np.ndarray:
|
||||
flow_field = vfield
|
||||
magnitude = np.sqrt(flow_field[:, :, 0] ** 2 + flow_field[:, :, 1] ** 2)
|
||||
|
||||
downsample_factor = 10
|
||||
h, w = magnitude.shape
|
||||
magnitude_downsampled = magnitude[::downsample_factor, ::downsample_factor]
|
||||
magnitude_smoothed = cv2.resize(magnitude_downsampled, (w, h), interpolation=cv2.INTER_CUBIC)
|
||||
|
||||
magnitude_thresholded = magnitude_smoothed.copy()
|
||||
magnitude_thresholded[magnitude_smoothed < threshold] = 0
|
||||
|
||||
mag_min, mag_max = magnitude_thresholded.min(), magnitude_thresholded.max()
|
||||
if mag_max > mag_min:
|
||||
magnitude_norm = ((magnitude_thresholded - mag_min) / (mag_max - mag_min) * 255).astype(np.uint8)
|
||||
else:
|
||||
magnitude_norm = np.zeros_like(magnitude_thresholded, dtype=np.uint8)
|
||||
|
||||
magnitude_colored = cv2.applyColorMap(magnitude_norm, colormap)
|
||||
out_img = cv2.resize(magnitude_colored, (400, 400), interpolation=cv2.INTER_CUBIC)
|
||||
return out_img
|
||||
|
||||
|
||||
def lowpass_filter(data, cutoff_hz, sample_rate_hz=FORCE_SAMPLE_RATE_HZ, order=2):
|
||||
"""
|
||||
Zero-phase Butterworth low-pass filter for uniformly sampled 1-D data.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : array-like
|
||||
Input samples acquired at `sample_rate_hz` (default 30 Hz).
|
||||
cutoff_hz : float
|
||||
-3 dB cutoff frequency in Hz; use 0 to skip filtering (return raw data).
|
||||
Otherwise must satisfy 0 < cutoff_hz < sample_rate_hz / 2.
|
||||
sample_rate_hz : float
|
||||
Sampling rate of `data` in Hz.
|
||||
order : int
|
||||
Butterworth filter order.
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.ndarray
|
||||
Filtered 1-D array with the same length as `data`.
|
||||
"""
|
||||
y = np.asarray(data, dtype=np.float64)
|
||||
if cutoff_hz == 0:
|
||||
return y.copy()
|
||||
if y.size == 0:
|
||||
return y
|
||||
|
||||
nyquist = 0.5 * sample_rate_hz
|
||||
if cutoff_hz < 0 or cutoff_hz >= nyquist:
|
||||
raise ValueError(
|
||||
f"cutoff_hz must be in (0, {nyquist}) for sample_rate_hz={sample_rate_hz}"
|
||||
)
|
||||
|
||||
b, a = butter(order, cutoff_hz / nyquist, btype="low")
|
||||
padlen = 3 * (max(len(b), len(a)) - 1)
|
||||
if y.size <= padlen:
|
||||
return y.copy()
|
||||
|
||||
return filtfilt(b, a, y)
|
||||
|
||||
|
||||
class TeeStream:
|
||||
"""同时写入多个流"""
|
||||
def __init__(self, *streams):
|
||||
self.streams = streams
|
||||
|
||||
def write(self, text):
|
||||
for s in self.streams:
|
||||
s.write(text)
|
||||
|
||||
def flush(self):
|
||||
for s in self.streams:
|
||||
s.flush()
|
||||
|
||||
|
||||
class WidgetLogHandler:
|
||||
"""将 print 输出实时显示到 QPlainTextEdit"""
|
||||
def __init__(self, widget: QPlainTextEdit):
|
||||
self.widget = widget
|
||||
self.buf = ""
|
||||
|
||||
def write(self, text: str):
|
||||
self.buf += text
|
||||
while "\n" in self.buf:
|
||||
line, self.buf = self.buf.split("\n", 1)
|
||||
if line:
|
||||
self.widget.appendPlainText(line)
|
||||
|
||||
def flush(self):
|
||||
if self.buf:
|
||||
self.widget.appendPlainText(self.buf)
|
||||
self.buf = ""
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 样式表
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
STYLE = """
|
||||
QMainWindow {
|
||||
background-color: #000000;
|
||||
}
|
||||
|
||||
/* ---- 图像面板 ---- */
|
||||
QLabel#arrowsLabel, QLabel#magLabel {
|
||||
border: 1px solid #262626;
|
||||
border-radius: 4px;
|
||||
background-color: #0b0b0b;
|
||||
}
|
||||
|
||||
/* ---- 顶栏按钮 & 输入 - — */
|
||||
QPushButton {
|
||||
background-color: #1a1a1a;
|
||||
color: #999999;
|
||||
border: 1px solid #333333;
|
||||
border-radius: 4px;
|
||||
padding: 6px 16px;
|
||||
font-family: "SimHei";
|
||||
font-size: 13px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #262626;
|
||||
color: #cccccc;
|
||||
border: 1px solid #555555;
|
||||
}
|
||||
QPushButton#btnStart {
|
||||
background-color: #0d2818;
|
||||
color: #4caf84;
|
||||
border: 1px solid #1a4d30;
|
||||
}
|
||||
QPushButton#btnStart:hover {
|
||||
background-color: #143d24;
|
||||
color: #6fd4a0;
|
||||
}
|
||||
QPushButton#btnStart:disabled {
|
||||
background-color: #0f0f0f;
|
||||
color: #333333;
|
||||
border: 1px solid #222222;
|
||||
}
|
||||
QPushButton#btnStop {
|
||||
background-color: #2b1010;
|
||||
color: #e05555;
|
||||
border: 1px solid #4d1a1a;
|
||||
}
|
||||
QPushButton#btnStop:hover {
|
||||
background-color: #3d1818;
|
||||
color: #f07070;
|
||||
}
|
||||
QPushButton#btnStop:disabled {
|
||||
background-color: #0f0f0f;
|
||||
color: #333333;
|
||||
border: 1px solid #222222;
|
||||
}
|
||||
QLineEdit {
|
||||
background-color: #0d0d0d;
|
||||
color: #999999;
|
||||
border: 1px solid #333333;
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ---- 力曲线容器 ---- */
|
||||
GraphicsLayoutWidget {
|
||||
border: 1px solid #262626;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* ---- 日志面板 ---- */
|
||||
QPlainTextEdit {
|
||||
background-color: #0b0b0b;
|
||||
color: #777777;
|
||||
border: 1px solid #262626;
|
||||
border-radius: 4px;
|
||||
font-family: "Cascadia Code", "Consolas", "Courier New", monospace;
|
||||
font-size: 13px;
|
||||
padding: 8px;
|
||||
selection-background-color: #1a3a4a;
|
||||
selection-color: #cccccc;
|
||||
}
|
||||
|
||||
QPlainTextEdit QScrollBar:vertical {
|
||||
background-color: #0b0b0b;
|
||||
width: 6px;
|
||||
margin: 1px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
QPlainTextEdit QScrollBar::handle:vertical {
|
||||
background-color: #262626;
|
||||
border-radius: 3px;
|
||||
min-height: 30px;
|
||||
}
|
||||
QPlainTextEdit QScrollBar::handle:vertical:hover {
|
||||
background-color: #3a3a3a;
|
||||
}
|
||||
QPlainTextEdit QScrollBar::add-line:vertical,
|
||||
QPlainTextEdit QScrollBar::sub-line:vertical {
|
||||
height: 0px;
|
||||
}
|
||||
QPlainTextEdit QScrollBar::add-page:vertical,
|
||||
QPlainTextEdit QScrollBar::sub-page:vertical {
|
||||
background: none;
|
||||
}
|
||||
|
||||
QSplitter::handle {
|
||||
background-color: #1a1a1a;
|
||||
}
|
||||
QSplitter::handle:vertical {
|
||||
height: 1px;
|
||||
}
|
||||
QSplitter::handle:horizontal {
|
||||
width: 1px;
|
||||
}
|
||||
"""
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 主窗口
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
def __init__(self, args, log_file):
|
||||
super().__init__()
|
||||
self.setWindowTitle("Orisys SDK")
|
||||
self.setStyleSheet(STYLE)
|
||||
|
||||
logo_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "logo.png")
|
||||
if os.path.exists(logo_path):
|
||||
self.setWindowIcon(QIcon(logo_path))
|
||||
|
||||
self.sensor = None
|
||||
self.start_time = None
|
||||
self.args = args
|
||||
|
||||
# 录像状态
|
||||
self.is_save = False
|
||||
self.frame_count = 0
|
||||
self.writer = None
|
||||
self.save_path = None
|
||||
|
||||
# 曲线数据缓冲(先填满 npoints 再滚动;120 点 @ 30 Hz = 4 s)
|
||||
self.npoints = PLOT_BUFFER_POINTS
|
||||
self.lowpass_cutoff_hz = getattr(args, "cutoff", 5.0)
|
||||
self._plot_sample_count = 0
|
||||
self._init_plot_buffer()
|
||||
|
||||
self._setup_ui()
|
||||
self._create_plots()
|
||||
|
||||
# 同步参数到 UI
|
||||
self.camera_edit.setText(args.video)
|
||||
self.config_edit.setText(args.config)
|
||||
self.cal_edit.setText(args.cal)
|
||||
|
||||
# stdout → 日志文件 + 界面日志面板
|
||||
self.log_handler = WidgetLogHandler(self.log_widget)
|
||||
sys.stdout = TeeStream(log_file, self.log_handler)
|
||||
sys.stderr = sys.stdout
|
||||
|
||||
self._y_range_alpha = 0.5
|
||||
|
||||
self.timer = QTimer()
|
||||
self.timer.timeout.connect(self._on_timer)
|
||||
self._running = False
|
||||
|
||||
def _init_plot_buffer(self):
|
||||
"""Empty buffer; x-axis grows until npoints real samples, then scrolls."""
|
||||
self.data = np.full((self.npoints, 2, 3), np.nan, dtype=np.float64)
|
||||
self._plot_sample_count = 0
|
||||
self._y_range_smooth = [None, None, None]
|
||||
|
||||
# ---- UI 构建 ----
|
||||
|
||||
def _setup_ui(self):
|
||||
central = QWidget()
|
||||
self.setCentralWidget(central)
|
||||
root_layout = QVBoxLayout(central)
|
||||
root_layout.setContentsMargins(6, 6, 6, 6)
|
||||
root_layout.setSpacing(6)
|
||||
|
||||
# ---- 顶栏 ----
|
||||
bar = QHBoxLayout()
|
||||
bar.setSpacing(8)
|
||||
|
||||
self.btn_camera = QPushButton("摄像机")
|
||||
self.btn_camera.setObjectName("btnCamera")
|
||||
self.btn_camera.clicked.connect(self._select_camera)
|
||||
bar.addWidget(self.btn_camera)
|
||||
self.camera_edit = QLineEdit("1")
|
||||
self.camera_edit.setPlaceholderText("摄像头编号 或 视频路径")
|
||||
bar.addWidget(self.camera_edit, 1)
|
||||
|
||||
self.btn_config = QPushButton("配置")
|
||||
self.btn_config.setObjectName("btnConfig")
|
||||
self.btn_config.clicked.connect(self._select_config)
|
||||
bar.addWidget(self.btn_config)
|
||||
self.config_edit = QLineEdit("./config/ddjx01.json")
|
||||
bar.addWidget(self.config_edit, 1)
|
||||
|
||||
self.btn_reset = QPushButton("重置")
|
||||
self.btn_reset.setObjectName("btnReset")
|
||||
self.btn_reset.setEnabled(False)
|
||||
self.btn_reset.clicked.connect(self._on_reset)
|
||||
bar.addWidget(self.btn_reset)
|
||||
self.cal_edit = QLineEdit("./config/ddjx01.npy")
|
||||
bar.addWidget(self.cal_edit, 1)
|
||||
|
||||
self.btn_start = QPushButton("启动")
|
||||
self.btn_start.setObjectName("btnStart")
|
||||
self.btn_start.clicked.connect(self._on_start)
|
||||
bar.addWidget(self.btn_start)
|
||||
|
||||
self.btn_stop = QPushButton("暂停")
|
||||
self.btn_stop.setObjectName("btnStop")
|
||||
self.btn_stop.setEnabled(False)
|
||||
self.btn_stop.clicked.connect(self._on_stop)
|
||||
bar.addWidget(self.btn_stop)
|
||||
|
||||
root_layout.addLayout(bar)
|
||||
|
||||
# ---- 外层纵向 splitter:主区域 / 日志 ----
|
||||
v_splitter = QSplitter(Qt.Vertical)
|
||||
|
||||
# ---- 主区域:图像 (左) | 曲线 (右) ----
|
||||
h_splitter = QSplitter(Qt.Horizontal)
|
||||
|
||||
# 左侧图像面板
|
||||
left = QWidget()
|
||||
left_layout = QVBoxLayout(left)
|
||||
left_layout.setContentsMargins(0, 0, 0, 0)
|
||||
left_layout.setSpacing(6)
|
||||
|
||||
self.arrows_label = QLabel()
|
||||
self.arrows_label.setObjectName("arrowsLabel")
|
||||
self.arrows_label.setMinimumSize(160, 160)
|
||||
self.arrows_label.setScaledContents(True)
|
||||
self.arrows_label.setAlignment(Qt.AlignCenter)
|
||||
left_layout.addWidget(self.arrows_label)
|
||||
|
||||
self.mag_label = QLabel()
|
||||
self.mag_label.setObjectName("magLabel")
|
||||
self.mag_label.setMinimumSize(160, 160)
|
||||
self.mag_label.setScaledContents(True)
|
||||
self.mag_label.setAlignment(Qt.AlignCenter)
|
||||
left_layout.addWidget(self.mag_label)
|
||||
|
||||
h_splitter.addWidget(left)
|
||||
|
||||
# 右侧力曲线
|
||||
pg.setConfigOption('background', '#000000')
|
||||
pg.setConfigOption('foreground', '#777777')
|
||||
pg.setConfigOptions(antialias=True)
|
||||
self.graph_widget = pg.GraphicsLayoutWidget()
|
||||
self.graph_widget.setMinimumWidth(300)
|
||||
h_splitter.addWidget(self.graph_widget)
|
||||
|
||||
self.h_splitter = h_splitter
|
||||
|
||||
v_splitter.addWidget(h_splitter)
|
||||
|
||||
# ---- 底部日志面板 ----
|
||||
self.log_widget = QPlainTextEdit()
|
||||
self.log_widget.setReadOnly(True)
|
||||
self.log_widget.setMaximumBlockCount(2000)
|
||||
font = QFont("Consolas", 12)
|
||||
font.setStyleHint(QFont.Monospace)
|
||||
self.log_widget.setFont(font)
|
||||
v_splitter.addWidget(self.log_widget)
|
||||
|
||||
v_splitter.setStretchFactor(0, 4)
|
||||
v_splitter.setStretchFactor(1, 1)
|
||||
|
||||
root_layout.addWidget(v_splitter)
|
||||
|
||||
def _create_plots(self):
|
||||
titles = ["法向力", "切向力 X", "切向力 Y"]
|
||||
self.plots = []
|
||||
self.curves = []
|
||||
self.dot_glows = []
|
||||
self.dot_cores = []
|
||||
|
||||
axis_pen = pg.mkPen(color=(38, 38, 38), width=1) # #262626
|
||||
label_font = QFont("SimHei", 9)
|
||||
|
||||
for i, title in enumerate(titles):
|
||||
p = self.graph_widget.addPlot(title=None)
|
||||
|
||||
# 标题:黑体
|
||||
p.setTitle(title, color='#999999', size='14pt')
|
||||
p.titleLabel.setFont(QFont("SimHei", 14))
|
||||
|
||||
# X 轴标签
|
||||
p.setLabel('bottom', '时间', units='s')
|
||||
p.getAxis('bottom').label.setFont(label_font)
|
||||
|
||||
# 仅保留左 + 下轴线,隐藏右 + 上边框
|
||||
p.showAxis('top', False)
|
||||
p.showAxis('right', False)
|
||||
p.getAxis('left').setPen(axis_pen)
|
||||
p.getAxis('bottom').setPen(axis_pen)
|
||||
|
||||
# 仅横向网格,极淡
|
||||
p.showGrid(x=False, y=True, alpha=0.12)
|
||||
|
||||
# 等宽刻度字体
|
||||
tick_font = QFont("Consolas", 11)
|
||||
tick_font.setStyleHint(QFont.Monospace)
|
||||
p.getAxis('left').setTickFont(tick_font)
|
||||
p.getAxis('bottom').setTickFont(tick_font)
|
||||
|
||||
# 曲线: 青色描边 + 向下渐变填充(与 demo_wuxi 一致)
|
||||
line_pen = pg.mkPen(color=CURVE_THEME_COLOR, width=2)
|
||||
# grad_brush = make_gradient_brush(CURVE_THEME_COLOR, max_alpha=120)
|
||||
curve = p.plot(pen=line_pen, fillLevel=0)
|
||||
|
||||
# 终点光晕: #00e5ff 30% 透明,半径 8
|
||||
dot_glow = p.plot([], [], pen=None, symbol='o', symbolSize=8,
|
||||
symbolPen=None, symbolBrush=(0, 229, 255, 76))
|
||||
# 终点核心: 纯白,半径 4
|
||||
dot_core = p.plot([], [], pen=None, symbol='o', symbolSize=4,
|
||||
symbolPen=None, symbolBrush=(255, 255, 255, 255))
|
||||
|
||||
p.setXRange(0, PLOT_WINDOW_SEC, padding=0)
|
||||
p.enableAutoRange(x=False, y=False)
|
||||
|
||||
self.plots.append(p)
|
||||
self.curves.append(curve)
|
||||
self.dot_glows.append(dot_glow)
|
||||
self.dot_cores.append(dot_core)
|
||||
|
||||
if i < 2:
|
||||
self.graph_widget.nextRow()
|
||||
|
||||
def adjust_square_images(self):
|
||||
"""根据窗口高度调整左侧面板宽度,使两张图保持正方形"""
|
||||
top_bar_h = 48
|
||||
log_h = self.log_widget.height()
|
||||
spacing = 20
|
||||
available = self.height() - top_bar_h - log_h - spacing
|
||||
each_h = max(available // 2, 160)
|
||||
self.h_splitter.setSizes([each_h, self.width() - each_h])
|
||||
|
||||
# ---- 帧处理 ----
|
||||
|
||||
def _on_timer(self):
|
||||
if self.sensor is None:
|
||||
return
|
||||
sensor = self.sensor
|
||||
|
||||
isimg = sensor.get_img()
|
||||
if isimg is None:
|
||||
print("Failed to get image")
|
||||
return
|
||||
|
||||
# 录像写入
|
||||
if self.is_save and self.writer is not None and sensor.frame is not None:
|
||||
self.frame_count += 1
|
||||
save_img = sensor.frame
|
||||
if len(save_img.shape) == 2 or (len(save_img.shape) == 3 and save_img.shape[2] == 1):
|
||||
if len(save_img.shape) == 3:
|
||||
save_img = save_img.squeeze(2)
|
||||
save_img = cv2.cvtColor(save_img, cv2.COLOR_GRAY2BGR)
|
||||
self.writer.write(save_img)
|
||||
|
||||
is_touched = sensor.compute_deformation(check_motion=True, threshold=0)
|
||||
sensor.compute_contact()
|
||||
|
||||
fps, fn, fx, fy, flow, vnormal, vshear, depth_map = sensor.read_info(
|
||||
sensor.info.FPS, sensor.info.FNORMAL,
|
||||
sensor.info.FSHEARX, sensor.info.FSHEARY,
|
||||
sensor.info.VRAW, sensor.info.VNORMAL,
|
||||
sensor.info.VSHEAR,
|
||||
sensor.info.DEPTH
|
||||
)
|
||||
|
||||
|
||||
print(f"FPS={fps:.2f}, 法向力={fn:.4f}, 切向力X={fx:.4f}, 切向力Y={fy:.4f}")
|
||||
# print(f"CUPY Available: {sensor.CUPY_AVAILABLE}")
|
||||
|
||||
# 图像
|
||||
_bg_bgr = (22, 16, 11)
|
||||
_img = sensor.img
|
||||
_h, _w = _img.shape[:2]
|
||||
black_img = np.empty((_h, _w, 3), dtype=_img.dtype)
|
||||
black_img[:, :, 0] = _bg_bgr[0]
|
||||
black_img[:, :, 1] = _bg_bgr[1]
|
||||
black_img[:, :, 2] = _bg_bgr[2]
|
||||
|
||||
arrows = orisys.util.draw_arrows(
|
||||
black_img, flow,
|
||||
threshold=2, grid_spacing=20,
|
||||
arrow_scale=0.5,
|
||||
below_threshold_color=(160, 160, 160),
|
||||
)
|
||||
arrows = cv2.resize(arrows, (400, 400), interpolation=cv2.INTER_CUBIC)
|
||||
self.arrows_label.setPixmap(cv2_to_pixmap(arrows))
|
||||
|
||||
# 显示深度图
|
||||
div_abs = depth_map
|
||||
if div_abs.max() > div_abs.min():
|
||||
div_normalized = ((div_abs - div_abs.min()) / (div_abs.max() - div_abs.min()) * 255).astype(np.uint8)
|
||||
else:
|
||||
div_normalized = np.zeros_like(div_abs, dtype=np.uint8)
|
||||
|
||||
# 应用颜色映射(使用JET:蓝色=低散度,红色=高散度)
|
||||
depth_colored = cv2.applyColorMap(div_normalized, cv2.COLORMAP_JET)
|
||||
|
||||
# mag = draw_magnitude_map(flow)
|
||||
self.mag_label.setPixmap(cv2_to_pixmap(depth_colored))
|
||||
|
||||
# 曲线:先顺序写入 npoints 个真实样本,满后再左移滚动
|
||||
current_time = time.time() - self.start_time
|
||||
if self._plot_sample_count < self.npoints:
|
||||
idx = self._plot_sample_count
|
||||
self.data[idx, 0, :] = current_time
|
||||
self.data[idx, 1, 0] = fn
|
||||
self.data[idx, 1, 1] = fx
|
||||
self.data[idx, 1, 2] = fy
|
||||
self._plot_sample_count += 1
|
||||
else:
|
||||
for i in range(3):
|
||||
self.data[:-1, 0, i] = self.data[1:, 0, i]
|
||||
self.data[:-1, 1, i] = self.data[1:, 1, i]
|
||||
self.data[-1, 0, :] = current_time
|
||||
self.data[-1, 1, 0] = fn
|
||||
self.data[-1, 1, 1] = fx
|
||||
self.data[-1, 1, 2] = fy
|
||||
|
||||
n = self._plot_sample_count
|
||||
if n == 0:
|
||||
return
|
||||
|
||||
data_plot = self.data[:n].copy()
|
||||
for i in range(3):
|
||||
data_plot[:, 1, i] = lowpass_filter(
|
||||
data_plot[:, 1, i],
|
||||
self.lowpass_cutoff_hz,
|
||||
sample_rate_hz=FORCE_SAMPLE_RATE_HZ,
|
||||
)
|
||||
|
||||
for i, curve in enumerate(self.curves):
|
||||
x = data_plot[:, 0, i]
|
||||
y = data_plot[:, 1, i]
|
||||
curve.setData(x, y)
|
||||
ymin, ymax = y.min(), y.max()
|
||||
if self._y_range_smooth[i] is None:
|
||||
self._y_range_smooth[i] = (ymin, ymax)
|
||||
else:
|
||||
prev_min, prev_max = self._y_range_smooth[i]
|
||||
alpha = self._y_range_alpha
|
||||
ymin = alpha * ymin + (1 - alpha) * prev_min
|
||||
ymax = alpha * ymax + (1 - alpha) * prev_max
|
||||
span = ymax - ymin
|
||||
if span < Y_AXIS_MIN_SPAN:
|
||||
mid = (ymin + ymax) / 2
|
||||
half = Y_AXIS_MIN_SPAN / 2
|
||||
ymin = mid - half
|
||||
ymax = mid + half
|
||||
self._y_range_smooth[i] = (ymin, ymax)
|
||||
pad = max((ymax - ymin) * 0.12, 0.01)
|
||||
self.plots[i].setYRange(ymin - pad, ymax + pad)
|
||||
last_x = x[-1:]
|
||||
last_y = y[-1:]
|
||||
self.dot_glows[i].setData(last_x, last_y)
|
||||
self.dot_cores[i].setData(last_x, last_y)
|
||||
|
||||
if n < self.npoints:
|
||||
for p in self.plots:
|
||||
p.setXRange(0, PLOT_WINDOW_SEC, padding=0)
|
||||
else:
|
||||
x0 = float(data_plot[0, 0, 0])
|
||||
x1 = float(data_plot[-1, 0, 0])
|
||||
if x1 <= x0:
|
||||
x1 = x0 + PLOT_WINDOW_SEC
|
||||
for p in self.plots:
|
||||
p.setXRange(x0, x1, padding=0)
|
||||
|
||||
# ---- 键盘 & 录像 ----
|
||||
|
||||
def _on_start(self):
|
||||
if self._running:
|
||||
return
|
||||
self.btn_start.setText("启动中...")
|
||||
self.btn_start.setEnabled(False)
|
||||
QApplication.processEvents()
|
||||
|
||||
try:
|
||||
src = self.camera_edit.text()
|
||||
cfg = self.config_edit.text()
|
||||
cal = self.cal_edit.text()
|
||||
verbose = getattr(self.args, 'verbose', True)
|
||||
try:
|
||||
src = int(src)
|
||||
except ValueError:
|
||||
pass
|
||||
self.sensor = orisys.Sensor(src, config_name=cfg, cal_path=cal, verbose=verbose)
|
||||
except Exception as e:
|
||||
self.btn_start.setText("启动")
|
||||
self.btn_start.setEnabled(True)
|
||||
print(f"启动失败: {e}")
|
||||
return
|
||||
|
||||
self.start_time = time.time()
|
||||
self._init_plot_buffer()
|
||||
self._running = True
|
||||
self.timer.start(10)
|
||||
self.btn_start.setText("启动")
|
||||
self.btn_stop.setEnabled(True)
|
||||
self.btn_reset.setEnabled(True)
|
||||
print(f"▶ 启动 — 摄像机={src}, 配置={cfg}, 标定={cal}")
|
||||
|
||||
def _on_stop(self):
|
||||
if not self._running:
|
||||
return
|
||||
self._running = False
|
||||
self.timer.stop()
|
||||
self.btn_stop.setText("停止中...")
|
||||
self.btn_stop.setEnabled(False)
|
||||
QApplication.processEvents()
|
||||
|
||||
if self.sensor is not None:
|
||||
self.sensor.disconnect()
|
||||
self.sensor = None
|
||||
|
||||
self.btn_start.setEnabled(True)
|
||||
self.btn_stop.setText("暂停")
|
||||
self.btn_reset.setEnabled(False)
|
||||
print("⏸ 已暂停")
|
||||
|
||||
def _select_camera(self):
|
||||
text = self.camera_edit.text()
|
||||
path, _ = QFileDialog.getOpenFileName(
|
||||
self, "选择视频文件", "data", "视频 (*.mp4 *.avi *.mov);;所有文件 (*)",
|
||||
)
|
||||
if path:
|
||||
self.camera_edit.setText(path)
|
||||
|
||||
def _select_config(self):
|
||||
path, _ = QFileDialog.getOpenFileName(
|
||||
self, "选择配置文件", "config", "JSON (*.json);;所有文件 (*)",
|
||||
)
|
||||
if path:
|
||||
self.config_edit.setText(path)
|
||||
|
||||
def _on_reset(self):
|
||||
"""Reset optical flow tracker when drift occurs (same as basic_pipeline 'r' key)."""
|
||||
if self.sensor is None or not self._running:
|
||||
print("请先启动传感器后再重置追踪器。")
|
||||
return
|
||||
self.sensor.reset()
|
||||
print("追踪器已重置。")
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
key = event.key()
|
||||
if key == Qt.Key_Escape:
|
||||
if self.isFullScreen():
|
||||
self.showNormal()
|
||||
else:
|
||||
self.showFullScreen()
|
||||
elif key == Qt.Key_Q:
|
||||
print("\n正在退出示例程序...")
|
||||
self.close()
|
||||
elif key == Qt.Key_S:
|
||||
self._start_recording()
|
||||
elif key == Qt.Key_E:
|
||||
self._stop_recording()
|
||||
elif key == Qt.Key_R:
|
||||
self._on_reset()
|
||||
else:
|
||||
super().keyPressEvent(event)
|
||||
|
||||
def _start_recording(self):
|
||||
if self.sensor is None:
|
||||
print("请先启动采集")
|
||||
return
|
||||
self.save_path = "./data/pulse_test.mp4"
|
||||
os.makedirs(os.path.dirname(self.save_path), exist_ok=True)
|
||||
|
||||
save_img = self.sensor.frame
|
||||
if save_img is None:
|
||||
print("当前没有可用帧,无法确定录像尺寸。")
|
||||
return
|
||||
|
||||
temp_img = save_img.copy()
|
||||
if len(temp_img.shape) == 2 or (len(temp_img.shape) == 3 and temp_img.shape[2] == 1):
|
||||
if len(temp_img.shape) == 3:
|
||||
temp_img = temp_img.squeeze(2)
|
||||
temp_img = cv2.cvtColor(temp_img, cv2.COLOR_GRAY2BGR)
|
||||
|
||||
fh, fw = temp_img.shape[:2]
|
||||
self.writer = cv2.VideoWriter(
|
||||
self.save_path, cv2.VideoWriter_fourcc(*'mp4v'), 30, (fw, fh),
|
||||
)
|
||||
if self.writer.isOpened():
|
||||
self.is_save = True
|
||||
print(f"开始录像:{self.save_path}")
|
||||
else:
|
||||
print(f"无法创建录像文件:{self.save_path}")
|
||||
self.writer = None
|
||||
|
||||
def _stop_recording(self):
|
||||
self.is_save = False
|
||||
if self.writer is not None:
|
||||
self.writer.release()
|
||||
self.writer = None
|
||||
if self.save_path is not None:
|
||||
print(f"录像结束:{self.save_path},总帧数:{self.frame_count}")
|
||||
else:
|
||||
print(f"录像结束,总帧数:{self.frame_count}")
|
||||
self.frame_count = 0
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.timer.stop()
|
||||
if self.writer is not None:
|
||||
self.writer.release()
|
||||
if self.sensor is not None:
|
||||
self.sensor.disconnect()
|
||||
event.accept()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 入口
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
log_dir = os.path.join(script_dir, "logs")
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
log_path = os.path.join(log_dir, f"qt_viewer_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log")
|
||||
log_file = open(log_path, 'w', encoding='utf-8')
|
||||
# stdout 到文件的临时重定向(创建 MainWindow 后会加上日志面板)
|
||||
sys.stdout = log_file
|
||||
sys.stderr = sys.stdout
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--video", "-v", type=str, default="0")
|
||||
parser.add_argument("--config", "-c", type=str, default="./config/ddjx01.json")
|
||||
parser.add_argument("--cal", "-cal", type=str, default="./config/ddjx01.npy")
|
||||
parser.add_argument("--verbose", "-verbose", type=bool, default=True)
|
||||
parser.add_argument(
|
||||
"--cutoff",
|
||||
type=float,
|
||||
default=10.0,
|
||||
help="Low-pass cutoff for force curves (Hz), sample rate 30 Hz",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
print("\n按键说明:Q 退出 | R 重置追踪器 | S 开始录像 | E 结束录像 | ESC 全屏")
|
||||
|
||||
# 高 DPI 适配(笔记本高分屏)
|
||||
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
|
||||
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
|
||||
app = QApplication(sys.argv)
|
||||
window = MainWindow(args, log_file)
|
||||
window.showFullScreen()
|
||||
QApplication.processEvents()
|
||||
window.adjust_square_images()
|
||||
|
||||
sys.exit(app.exec_())
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user