This commit is contained in:
cheng
2026-06-08 17:52:48 +08:00
commit 7a15bd8e14
189 changed files with 192687 additions and 0 deletions
+793
View File
@@ -0,0 +1,793 @@
"""
Orisys SDK 实时监控 — Qt 统一界面
将所有可视化和曲线整合到一个窗口中,适合销售演示使用。
SDK version: 0.3.1
"""
import os
import sys
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
import cv2
import orisys
import numpy as np
import time
import argparse
from datetime import datetime
try:
from scipy.signal import butter, filtfilt
except Exception:
butter = None
filtfilt = None
# 下面这些参数只影响右侧三条力曲线的显示,不改变传感器读数、光流图或幅值图数据。
FORCE_SAMPLE_RATE_HZ = 30.0
PLOT_BUFFER_POINTS = 120
PLOT_WINDOW_SEC = PLOT_BUFFER_POINTS / FORCE_SAMPLE_RATE_HZ
Y_AXIS_MIN_SPAN = 20000.0
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.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=1.5, 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):
"""和 qt_viewer_2.py 一样用于三条力曲线显示的低通滤波。"""
y = np.asarray(data, dtype=np.float64)
if cutoff_hz == 0 or y.size == 0:
return y.copy()
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}"
)
if butter is not None and filtfilt is not None:
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)
window = min(max(int(sample_rate_hz / max(cutoff_hz, 1e-6)), 3), y.size)
if window % 2 == 0:
window -= 1
if window <= 1:
return y.copy()
kernel = np.ones(window, dtype=np.float64) / window
pad = window // 2
forward = np.convolve(np.pad(y, (pad, pad), mode="edge"), kernel, mode="valid")
return np.convolve(np.pad(forward[::-1], (pad, pad), mode="edge"), kernel, mode="valid")[::-1]
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
# 曲线数据缓冲:只存原始 fn/fx/fy,滤波只在绘图副本上做。
self.npoints = PLOT_BUFFER_POINTS
self.lowpass_cutoff_hz = getattr(args, "cutoff", 10.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):
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)
# 曲线样式与 qt_viewer_2.py 对齐
curve = p.plot(pen=pg.mkPen(color=CURVE_THEME_COLOR, width=2), 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 _make_force_plot_data(self, data_plot):
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,
)
return data_plot
# ---- 帧处理 ----
def _on_timer(self):
if self.sensor is None:
return
sensor = self.sensor
sensor.get_img()
# 录像写入
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)
sensor.compute_deformation(check_motion=True, threshold=0)
fps, fn, fx, fy, flow, vnormal, vshear = sensor.read_info(
sensor.info.FPS, sensor.info.FNORMAL,
sensor.info.FSHEARX, sensor.info.FSHEARY,
sensor.info.VRAW, sensor.info.VNORMAL,
sensor.info.VSHEAR,
)
print(f"FPS={fps:.2f}, 法向力={fn:.4f}, 切向力X={fx:.4f}, 切向力Y={fy:.4f}")
# 图像
_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))
mag = draw_magnitude_map(flow)
self.mag_label.setPixmap(cv2_to_pixmap(mag))
# 曲线:只对右侧三条力曲线的显示数据做缓冲和低通滤波。
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._make_force_plot_data(self.data[:n].copy())
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
if ymax - ymin < Y_AXIS_MIN_SPAN:
mid = (ymin + ymax) / 2
ymin = mid - Y_AXIS_MIN_SPAN / 2
ymax = mid + Y_AXIS_MIN_SPAN / 2
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 _select_cal(self):
path, _ = QFileDialog.getOpenFileName(
self, "选择标定文件", "config", "NumPy (*.npy);;所有文件 (*)",
)
if path:
self.cal_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()
window._on_start()
sys.exit(app.exec_())
if __name__ == '__main__':
main()