632 lines
23 KiB
Python
632 lines
23 KiB
Python
import os
|
|
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
|
|
import pyqtgraph as pg
|
|
from PyQt5.QtCore import Qt, QSize, QEvent
|
|
from PyQt5.QtGui import QImage, QFont, QPixmap, QKeySequence, QCursor
|
|
from PyQt5.QtWidgets import QLabel, QVBoxLayout, QWidget, QSizePolicy, QShortcut, QApplication
|
|
from pyqtgraph.Qt import QtGui
|
|
|
|
|
|
# Window aspect: width : total height = 936 : (936 + plot strip)
|
|
ASPECT_W = 936
|
|
ASPECT_H_TOTAL = 936 + 624
|
|
ARROW_STRETCH = 936
|
|
PLOT_STRETCH = 624
|
|
npoints = 120 # 图表数据长度
|
|
dispaly_angle = -90 # 箭头图显示角度
|
|
touch_threshold = 5 # 触碰反应阈值
|
|
frame_to_recover = 10 # 触碰后等待回复帧数
|
|
|
|
# Match arrow panel / Qt style (not pure black)
|
|
PLOT_THEME_BG = "#0B1016"
|
|
PLOT_THEME_FG = "#A0A0A0"
|
|
|
|
# OS title bar cannot be tinted to #0B1016; frameless removes it (use Alt+drag to move).
|
|
USE_FRAMELESS_WINDOW = True
|
|
|
|
|
|
class CombinedTactileWindow(QWidget):
|
|
"""
|
|
Resizable window that keeps width:height = ASPECT_W : ASPECT_H_TOTAL.
|
|
Dragging the width or height edge updates the other dimension accordingly.
|
|
"""
|
|
|
|
RESIZE_MARGIN = 8
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._aspect_hw = ASPECT_H_TOTAL / float(ASPECT_W)
|
|
self._resize_guard = False
|
|
self.user_closed = False
|
|
self._drag_anchor = None
|
|
self._frameless_resize_edge = None
|
|
self._frameless_resize_start_global = None
|
|
self._frameless_resize_start_geom = None
|
|
|
|
def _min_client_size(self):
|
|
w = max(320, self.minimumSizeHint().width())
|
|
h = int(round(w * self._aspect_hw))
|
|
return w, h
|
|
|
|
def _hit_test_resize(self, global_pos):
|
|
if not USE_FRAMELESS_WINDOW:
|
|
return None
|
|
p = self.mapFromGlobal(global_pos)
|
|
w, h = self.width(), self.height()
|
|
m = self.RESIZE_MARGIN
|
|
left = p.x() <= m
|
|
right = p.x() >= w - m
|
|
top = p.y() <= m
|
|
bottom = p.y() >= h - m
|
|
if top and left:
|
|
return "tl"
|
|
if top and right:
|
|
return "tr"
|
|
if bottom and left:
|
|
return "bl"
|
|
if bottom and right:
|
|
return "br"
|
|
if top:
|
|
return "t"
|
|
if bottom:
|
|
return "b"
|
|
if left:
|
|
return "l"
|
|
if right:
|
|
return "r"
|
|
return None
|
|
|
|
def _cursor_for_edge(self, edge):
|
|
return {
|
|
"tl": Qt.SizeFDiagCursor,
|
|
"br": Qt.SizeFDiagCursor,
|
|
"tr": Qt.SizeBDiagCursor,
|
|
"bl": Qt.SizeBDiagCursor,
|
|
"l": Qt.SizeHorCursor,
|
|
"r": Qt.SizeHorCursor,
|
|
"t": Qt.SizeVerCursor,
|
|
"b": Qt.SizeVerCursor,
|
|
}.get(edge, Qt.ArrowCursor)
|
|
|
|
def _apply_frameless_resize(self, dg):
|
|
edge = self._frameless_resize_edge
|
|
g0 = self._frameless_resize_start_geom
|
|
min_w, min_h = self._min_client_size()
|
|
dx, dy = dg.x(), dg.y()
|
|
x0, y0, w0, h0 = g0.x(), g0.y(), g0.width(), g0.height()
|
|
|
|
if edge == "br":
|
|
nw = max(min_w, w0 + dx)
|
|
nh = int(round(nw * self._aspect_hw))
|
|
self.setGeometry(x0, y0, nw, nh)
|
|
elif edge == "r":
|
|
nw = max(min_w, w0 + dx)
|
|
nh = int(round(nw * self._aspect_hw))
|
|
self.setGeometry(x0, y0, nw, nh)
|
|
elif edge == "b":
|
|
nh = max(min_h, h0 + dy)
|
|
nw = int(round(nh / self._aspect_hw))
|
|
self.setGeometry(x0, y0, nw, nh)
|
|
elif edge == "l":
|
|
nw = max(min_w, w0 - dx)
|
|
nh = int(round(nw * self._aspect_hw))
|
|
nx = g0.right() - nw
|
|
self.setGeometry(nx, y0, nw, nh)
|
|
elif edge == "t":
|
|
nh = max(min_h, h0 - dy)
|
|
nw = int(round(nh / self._aspect_hw))
|
|
ny = g0.bottom() - nh
|
|
self.setGeometry(x0, ny, nw, nh)
|
|
elif edge == "tr":
|
|
nw = max(min_w, w0 + dx)
|
|
nh = int(round(nw * self._aspect_hw))
|
|
ny = g0.bottom() - nh
|
|
self.setGeometry(x0, ny, nw, nh)
|
|
elif edge == "bl":
|
|
nw = max(min_w, w0 - dx)
|
|
nh = int(round(nw * self._aspect_hw))
|
|
nx = g0.right() - nw
|
|
self.setGeometry(nx, y0, nw, nh)
|
|
elif edge == "tl":
|
|
nw = max(min_w, w0 - dx)
|
|
nh = int(round(nw * self._aspect_hw))
|
|
nx = g0.right() - nw
|
|
ny = g0.bottom() - nh
|
|
self.setGeometry(nx, ny, nw, nh)
|
|
|
|
def install_drag_filters(self):
|
|
"""Alt+left-drag to move when frameless; grabs go to this widget."""
|
|
for w in self.findChildren(QWidget):
|
|
if w is not self:
|
|
w.installEventFilter(self)
|
|
|
|
def eventFilter(self, watched, event):
|
|
if not USE_FRAMELESS_WINDOW:
|
|
return super().eventFilter(watched, event)
|
|
et = event.type()
|
|
if et == QEvent.MouseButtonPress and event.button() == Qt.LeftButton:
|
|
edge = self._hit_test_resize(event.globalPos())
|
|
if edge is not None:
|
|
self._frameless_resize_edge = edge
|
|
self._frameless_resize_start_global = event.globalPos()
|
|
self._frameless_resize_start_geom = self.geometry()
|
|
self.grabMouse()
|
|
return True
|
|
if QApplication.keyboardModifiers() & Qt.AltModifier:
|
|
self._drag_anchor = event.globalPos() - self.frameGeometry().topLeft()
|
|
self.grabMouse()
|
|
return True
|
|
if et == QEvent.MouseMove and not event.buttons():
|
|
edge = self._hit_test_resize(event.globalPos())
|
|
watched.setCursor(QCursor(self._cursor_for_edge(edge)))
|
|
return super().eventFilter(watched, event)
|
|
|
|
def mouseMoveEvent(self, event):
|
|
if USE_FRAMELESS_WINDOW and self._frameless_resize_edge and event.buttons() & Qt.LeftButton:
|
|
dg = event.globalPos() - self._frameless_resize_start_global
|
|
self._apply_frameless_resize(dg)
|
|
event.accept()
|
|
return
|
|
if (
|
|
USE_FRAMELESS_WINDOW
|
|
and self._drag_anchor is not None
|
|
and event.buttons() & Qt.LeftButton
|
|
):
|
|
self.move(event.globalPos() - self._drag_anchor)
|
|
event.accept()
|
|
return
|
|
super().mouseMoveEvent(event)
|
|
|
|
def mouseReleaseEvent(self, event):
|
|
if USE_FRAMELESS_WINDOW and self._frameless_resize_edge and event.button() == Qt.LeftButton:
|
|
self.releaseMouse()
|
|
self._frameless_resize_edge = None
|
|
self._frameless_resize_start_global = None
|
|
self._frameless_resize_start_geom = None
|
|
event.accept()
|
|
return
|
|
if USE_FRAMELESS_WINDOW and self._drag_anchor is not None and event.button() == Qt.LeftButton:
|
|
self.releaseMouse()
|
|
self._drag_anchor = None
|
|
event.accept()
|
|
return
|
|
super().mouseReleaseEvent(event)
|
|
|
|
def closeEvent(self, event):
|
|
self.user_closed = True
|
|
event.accept()
|
|
|
|
def minimumSizeHint(self):
|
|
w = 320
|
|
return QSize(w, int(round(w * self._aspect_hw)))
|
|
|
|
def resizeEvent(self, event):
|
|
if self._resize_guard:
|
|
super().resizeEvent(event)
|
|
return
|
|
super().resizeEvent(event)
|
|
w, h = self.width(), self.height()
|
|
if w < 8 or h < 8:
|
|
return
|
|
old = event.oldSize()
|
|
self._resize_guard = True
|
|
try:
|
|
if old.isValid() and old.width() > 0 and old.height() > 0:
|
|
dw = abs(w - old.width())
|
|
dh = abs(h - old.height())
|
|
if dw >= dh:
|
|
new_w, new_h = w, int(round(w * self._aspect_hw))
|
|
else:
|
|
new_w, new_h = int(round(h / self._aspect_hw)), h
|
|
else:
|
|
new_w, new_h = w, int(round(w * self._aspect_hw))
|
|
if new_w != w or new_h != h:
|
|
self.resize(new_w, new_h)
|
|
finally:
|
|
self._resize_guard = False
|
|
|
|
|
|
def compute_arrow_canvas_side(win):
|
|
"""
|
|
Pixel size of the square arrow canvas from the window client area and the
|
|
fixed vertical split ARROW_STRETCH : PLOT_STRETCH (matches QVBoxLayout stretch).
|
|
"""
|
|
w = win.width()
|
|
h = win.height()
|
|
if w < 16 or h < 16:
|
|
return max(32, ASPECT_W)
|
|
h_arrow = int(round(h * ARROW_STRETCH / float(ARROW_STRETCH + PLOT_STRETCH)))
|
|
# With aspect ASPECT_W:ASPECT_H_TOTAL, h_arrow should equal w; min() absorbs rounding.
|
|
return max(32, min(w, h_arrow))
|
|
|
|
|
|
def draw_arrows_wuxi(
|
|
frame,
|
|
optical_flow,
|
|
fps=True,
|
|
threshold=5,
|
|
grid_spacing=40,
|
|
arrow_scale=1.0,
|
|
margin=0.8,
|
|
colormap=cv2.COLORMAP_PARULA,
|
|
below_threshold_color=(0, 0, 255),
|
|
edge_margin=15,
|
|
target_size=936,
|
|
):
|
|
"""
|
|
Draw colored optical flow arrows on the image by magnitude (JET colormap).
|
|
Resizes frame and flow to target_size x target_size; scales flow vectors and
|
|
grid_spacing / margins / threshold so arrow density matches the original resolution.
|
|
"""
|
|
image = frame.copy()
|
|
if len(image.shape) == 2 or image.shape[2] == 1:
|
|
image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
|
|
|
|
h0, w0 = image.shape[:2]
|
|
if h0 != target_size or w0 != target_size:
|
|
sx = target_size / float(w0)
|
|
sy = target_size / float(h0)
|
|
image = cv2.resize(image, (target_size, target_size), interpolation=cv2.INTER_LINEAR)
|
|
u = cv2.resize(optical_flow[:, :, 0], (target_size, target_size), interpolation=cv2.INTER_LINEAR) * sx
|
|
v = cv2.resize(optical_flow[:, :, 1], (target_size, target_size), interpolation=cv2.INTER_LINEAR) * sy
|
|
optical_flow = np.stack([u, v], axis=2)
|
|
scale = max(sx, sy)
|
|
grid_spacing = max(4, int(round(grid_spacing * scale)))
|
|
edge_margin = max(1, int(round(edge_margin * scale)))
|
|
threshold = float(threshold) * scale
|
|
|
|
h, w = image.shape[:2]
|
|
# Arrow grid in pixel coordinates (assume frame and flow same size)
|
|
rr = np.arange(grid_spacing // 2, h, grid_spacing)
|
|
cc = np.arange(grid_spacing // 2, w, grid_spacing)
|
|
rr, cc = np.meshgrid(rr, cc, indexing="ij")
|
|
rr, cc = rr.reshape(-1), cc.reshape(-1)
|
|
|
|
# Displacement at each grid point (flow stored as half-displacement)
|
|
disp = 2.0 * optical_flow[rr, cc, :]
|
|
mag = np.linalg.norm(disp, axis=1)
|
|
dx = disp[:, 0] / arrow_scale
|
|
dy = disp[:, 1] / arrow_scale
|
|
length = np.sqrt(dx * dx + dy * dy)
|
|
# Cap arrow length at grid_spacing
|
|
scale = np.where(length > grid_spacing*margin, grid_spacing / np.maximum(length, 1e-9), 1.0)*margin
|
|
dx, dy = dx * scale, dy * scale
|
|
end_x = (cc + dx).astype(np.int32)
|
|
end_y = (rr + dy).astype(np.int32)
|
|
|
|
valid = (
|
|
(cc >= edge_margin) & (cc < w - edge_margin) & (rr >= edge_margin) & (rr < h - edge_margin)
|
|
& (end_x >= 0) & (end_x < w) & (end_y >= 0) & (end_y < h)
|
|
)
|
|
mag_range = mag.max() - mag.min()
|
|
norm_mag = ((mag - mag.min()) / mag_range * 255).astype(np.uint8) if mag_range > 0 else np.zeros_like(mag, dtype=np.uint8)
|
|
norm_mag[mag < threshold] = 0
|
|
colors = cv2.applyColorMap(norm_mag.reshape(-1, 1), colormap)
|
|
|
|
for i in np.where(valid)[0]:
|
|
pt1, pt2 = (int(cc[i]), int(rr[i])), (int(end_x[i]), int(end_y[i]))
|
|
color = tuple(int(c) for c in colors[i, 0])
|
|
if mag[i] < threshold:
|
|
cv2.circle(image, pt1, radius=1, color=below_threshold_color, thickness=-1)
|
|
else:
|
|
# thickness = max(1, int(2 * mag[i] / mag.max())) if mag.max() > 0 else 2
|
|
thickness = 1
|
|
cv2.arrowedLine(image, pt1, pt2, color, thickness=thickness, tipLength=0.2)
|
|
return image
|
|
|
|
|
|
def set_arrow_label_bgr(label, bgr_image):
|
|
"""Show a BGR OpenCV image in a QLabel (RGB for Qt), centered with equal margins."""
|
|
rgb = cv2.cvtColor(bgr_image, cv2.COLOR_BGR2RGB)
|
|
rgb = np.ascontiguousarray(rgb)
|
|
h, w, ch = rgb.shape
|
|
qimg = QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888).copy()
|
|
pm = QPixmap.fromImage(qimg)
|
|
lw, lh = label.width(), label.height()
|
|
if lw > 0 and lh > 0 and (pm.width() != lw or pm.height() != lh):
|
|
# Keep square aspect; center in label (equal L/R or T/B bars if rect not square)
|
|
pm = pm.scaled(lw, lh, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
|
label.setPixmap(pm)
|
|
|
|
|
|
def make_gradient_brush(color_hex, max_alpha=150):
|
|
"""
|
|
创建一个从上到下渐变的笔刷 (兼容 PyQt5 和 PyQt6)
|
|
"""
|
|
c_top = QtGui.QColor(color_hex)
|
|
c_top.setAlpha(max_alpha) # 顶部半透明
|
|
|
|
c_bot = QtGui.QColor(color_hex)
|
|
c_bot.setAlpha(0) # 底部全透明
|
|
|
|
# 创建线性渐变:(0,0)到(0,1) 代表从控件顶部到底部
|
|
grad = QtGui.QLinearGradient(0, 0, 0, 1)
|
|
|
|
if hasattr(QtGui.QGradient, 'CoordinateMode'):
|
|
grad.setCoordinateMode(QtGui.QGradient.CoordinateMode.ObjectBoundingMode)
|
|
else:
|
|
grad.setCoordinateMode(QtGui.QGradient.ObjectBoundingMode)
|
|
# ==============================================
|
|
|
|
grad.setColorAt(0, c_top) # 0.0 (顶端) 颜色
|
|
grad.setColorAt(1, c_bot) # 1.0 (底端) 颜色
|
|
|
|
return QtGui.QBrush(grad)
|
|
|
|
def create_plot(plot_container):
|
|
pg.setConfigOption("background", PLOT_THEME_BG)
|
|
pg.setConfigOption("foreground", PLOT_THEME_FG)
|
|
pg.setConfigOptions(antialias=True) # 抗锯齿
|
|
theme_color = '#00E5FF' # '#00E5FF'
|
|
line_pen = pg.mkPen(color=theme_color, width=2)
|
|
grad_brush = make_gradient_brush(theme_color, max_alpha=120)
|
|
|
|
axis_font = QFont("Segoe UI", 11)
|
|
p1 = plot_container.addPlot()
|
|
p1.setLabel('left', 'N')
|
|
p1.getAxis('left').label.setFont(axis_font)
|
|
p1.getAxis('left').label.setRotation(dispaly_angle)
|
|
p1.showGrid(x=True, y=True, alpha=0.3)
|
|
plot_container.nextRow()
|
|
p2 = plot_container.addPlot()
|
|
p2.setLabel('left', 'X')
|
|
p2.getAxis('left').label.setFont(axis_font)
|
|
p2.getAxis('left').label.setRotation(dispaly_angle)
|
|
p2.showGrid(x=True, y=True, alpha=0.3)
|
|
plot_container.nextRow()
|
|
p3 = plot_container.addPlot()
|
|
p3.setLabel('left', 'Y')
|
|
|
|
# p3.setLabel('bottom', 'Time', units='s')
|
|
p3.getAxis('left').label.setFont(axis_font)
|
|
p3.getAxis('left').label.setRotation(dispaly_angle)
|
|
p3.getAxis('bottom').label.setFont(axis_font)
|
|
p3.showGrid(x=True, y=True, alpha=0.3)
|
|
_plot_bg = pg.mkColor(PLOT_THEME_BG)
|
|
for p in (p1, p2, p3):
|
|
p.getAxis("bottom").setStyle(showValues=False)
|
|
p.getAxis("left").setStyle(showValues=False)
|
|
# Keep title as "N" / "X" / "Y" — no PyQtGraph "(x0.001)" suffix on small ranges.
|
|
p.getAxis("left").enableAutoSIPrefix(False)
|
|
# PlotItem.setBackground exists only in newer pyqtgraph; ViewBox works on older installs.
|
|
p.vb.setBackgroundColor(_plot_bg)
|
|
plot_af = p1.plot(pen=line_pen, fillLevel=0, fillBrush=grad_brush)
|
|
plot_tfx = p2.plot(pen=line_pen, fillLevel=0, fillBrush=grad_brush)
|
|
plot_tfy = p3.plot(pen=line_pen, fillLevel=0, fillBrush=grad_brush)
|
|
return p1, p2, p3, plot_af, plot_tfx, plot_tfy
|
|
|
|
def _plot_y_range(y):
|
|
"""Avoid degenerate Y span; explicit x/y in setData so older PyQtGraph never plots time as y."""
|
|
mn = float(np.min(y))
|
|
mx = float(np.max(y))
|
|
if not (np.isfinite(mn) and np.isfinite(mx)):
|
|
return 0.0, 1.0
|
|
if mx - mn <= 1e-15:
|
|
mid = 0.5 * (mn + mx)
|
|
return mid - 0.5, mid + 0.5
|
|
pad = (mx - mn) * 0.05
|
|
return mn - pad, mx + pad
|
|
|
|
|
|
def update(plot_af, plot_tfx, plot_tfy, data, p1, p2, p3):
|
|
# global plot_af, plot_tfx, plot_tfy, data, p1, p2, p3
|
|
plot_af.setData(data[:, 0, 0], data[:, 1, 0])
|
|
p1.setYRange(*_plot_y_range(data[:, 1, 0]))
|
|
plot_tfx.setData(data[:, 0, 1], data[:, 1, 1])
|
|
p2.setYRange(*_plot_y_range(data[:, 1, 1]))
|
|
plot_tfy.setData(data[:, 0, 2], data[:, 1, 2])
|
|
p3.setYRange(*_plot_y_range(data[:, 1, 2]))
|
|
|
|
# 实时图表:单窗口,上为光流箭头,下为曲线
|
|
pg.setConfigOption("background", PLOT_THEME_BG)
|
|
pg.setConfigOption("foreground", PLOT_THEME_FG)
|
|
app = pg.mkQApp("Wuxi Demo")
|
|
|
|
combined_win = CombinedTactileWindow()
|
|
combined_win.setWindowTitle("Wuxi tactile — flow + forces")
|
|
main_layout = QVBoxLayout(combined_win)
|
|
main_layout.setContentsMargins(0, 0, 0, 0)
|
|
main_layout.setSpacing(0)
|
|
|
|
# Ignored: do not let pixmap/content drive QLabel size — layout stretch sets arrow vs plot heights.
|
|
_policy_layout = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
|
|
|
|
arrow_label = QLabel()
|
|
arrow_label.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
|
|
arrow_label.setStyleSheet(
|
|
"background-color: #0B1016; margin: 0; padding: 0; border: none;"
|
|
)
|
|
arrow_label.setSizePolicy(_policy_layout)
|
|
arrow_label.setMinimumSize(0, 0)
|
|
arrow_label.setScaledContents(False)
|
|
main_layout.addWidget(arrow_label, stretch=ARROW_STRETCH)
|
|
|
|
plot_widget = pg.GraphicsLayoutWidget()
|
|
plot_widget.setBackground(PLOT_THEME_BG)
|
|
plot_widget.setSizePolicy(_policy_layout)
|
|
plot_widget.setMinimumSize(0, 0)
|
|
main_layout.addWidget(plot_widget, stretch=PLOT_STRETCH)
|
|
|
|
if USE_FRAMELESS_WINDOW:
|
|
combined_win.setWindowFlags(Qt.Window | Qt.FramelessWindowHint)
|
|
# Client area matches theme (no bright border flash on frameless)
|
|
combined_win.setStyleSheet(f"background-color: {PLOT_THEME_BG};")
|
|
combined_win.resize(ASPECT_W, ASPECT_H_TOTAL)
|
|
combined_win.install_drag_filters()
|
|
combined_win.show()
|
|
|
|
p1, p2, p3, plot_af, plot_tfx, plot_tfy = create_plot(plot_widget)
|
|
data = np.zeros([npoints,2,3])
|
|
start_time = None # Will be set when sensor starts
|
|
fps = 30.0
|
|
for i in range(3):
|
|
data[:,0,i] = np.linspace(0.0, npoints / fps, npoints)
|
|
|
|
|
|
|
|
def main():
|
|
global start_time
|
|
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--video","-v", type=int, default=0, help="视频文件路径")
|
|
args = parser.parse_args()
|
|
|
|
session_start = datetime.now()
|
|
print(
|
|
f"[session] START {session_start:%Y-%m-%d %H:%M:%S} (long-run / crash log marker)",
|
|
flush=True,
|
|
)
|
|
if USE_FRAMELESS_WINDOW:
|
|
print(
|
|
"[window] Frameless: resize = drag window edges/corners; move = Alt+left-drag; quit = Q.",
|
|
flush=True,
|
|
)
|
|
|
|
sensor1 = None
|
|
try:
|
|
sensor1 = orisys.Sensor(
|
|
args.video,
|
|
config_name="./config/ddjx01_wuxi.json",
|
|
cal_path="./config/dd01a_0306.npy",
|
|
isstitch=True,
|
|
cuda=False,
|
|
verbose=False,
|
|
)
|
|
|
|
def _shortcut_reset_tracker():
|
|
sensor1.reset()
|
|
print("传感器已重置 (快捷键 R)", flush=True)
|
|
|
|
def _shortcut_quit():
|
|
combined_win.close()
|
|
|
|
_sc_r = QShortcut(QKeySequence(Qt.Key_R), combined_win)
|
|
_sc_r.activated.connect(_shortcut_reset_tracker)
|
|
_sc_q = QShortcut(QKeySequence(Qt.Key_Q), combined_win)
|
|
_sc_q.activated.connect(_shortcut_quit)
|
|
|
|
start_time = time.time()
|
|
is_save = False
|
|
frame_count = 0
|
|
writer = None
|
|
while True:
|
|
sensor1.get_img() # 提取并拼接复眼图像
|
|
|
|
sensor1.compute_deformation(check_motion = True, tail_frames = frame_to_recover, threshold = touch_threshold) # 计算形变和分解
|
|
# 提取信息:
|
|
# 向量场: VRAW, VNORMAL, VSHEAR: 原始,法向,切向
|
|
# 帧率,时间戳: FPS, TIMESTAMP
|
|
# 变形量总和: FNORMAL , FSHEARX, FSHEARY : 法向,切向(X),切向(Y)
|
|
fps, fn, fx, fy, flow = sensor1.read_info(
|
|
sensor1.info.FPS,
|
|
sensor1.info.FNORMAL,
|
|
sensor1.info.FSHEARX,
|
|
sensor1.info.FSHEARY,
|
|
sensor1.info.VRAW,
|
|
)
|
|
|
|
# print(fps,fn,fx,fy)
|
|
# 显示向量场 — BGR background; img may be 2D (H,W) or 3D (H,W,C)
|
|
_bg_bgr = (22, 16, 11)
|
|
_img = sensor1.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]
|
|
# Square side = min(label w,h) so pixmap matches centered KeepAspectRatio scaling
|
|
app.processEvents()
|
|
lw, lh = arrow_label.width(), arrow_label.height()
|
|
if lw > 16 and lh > 16:
|
|
side = max(32, min(lw, lh))
|
|
else:
|
|
side = compute_arrow_canvas_side(combined_win)
|
|
arrows = draw_arrows_wuxi(
|
|
black_img,
|
|
flow,
|
|
threshold=2,
|
|
grid_spacing=20,
|
|
arrow_scale=0.5,
|
|
below_threshold_color=(160, 160, 160),
|
|
target_size=int(side),
|
|
)
|
|
|
|
set_arrow_label_bgr(arrow_label, arrows)
|
|
|
|
# 显示图表 - 更新时间轴
|
|
current_time = time.time() - start_time
|
|
for i in range(3):
|
|
data[:-1, 0, i] = data[1:, 0, i] # shift time data left
|
|
data[:-1, 1, i] = data[1:, 1, i] # shift force data left
|
|
data[-1, 0, :] = frame_count / 15.0 # set current time for all plots
|
|
data[-1, 1, 0] = fn
|
|
data[-1, 1, 1] = abs(fx)
|
|
data[-1, 1, 2] = abs(fy)
|
|
update(plot_af, plot_tfx, plot_tfy, data, p1, p2, p3) # Update plot
|
|
frame_count += 1
|
|
app.processEvents()
|
|
|
|
if combined_win.user_closed or not combined_win.isVisible():
|
|
print("\n窗口已关闭,退出程序")
|
|
if writer is not None:
|
|
writer.release()
|
|
writer = None
|
|
break
|
|
|
|
# Convert combined window to image (arrows + curves)
|
|
pixmap = combined_win.grab()
|
|
# Convert QPixmap to QImage
|
|
qimg = pixmap.toImage()
|
|
# Convert QImage to numpy array (RGB format)
|
|
qimg = qimg.convertToFormat(QImage.Format_RGB888)
|
|
width = qimg.width()
|
|
height = qimg.height()
|
|
# Convert QImage to numpy array
|
|
ptr = qimg.bits()
|
|
ptr.setsize(height * width * 3)
|
|
arr = np.array(ptr, dtype=np.uint8).reshape((height, width, 3))
|
|
|
|
key = cv2.waitKey(1)
|
|
if key & 0xFF == ord("q"):
|
|
if writer is not None:
|
|
writer.release()
|
|
writer = None
|
|
print("\n 停止程序")
|
|
break
|
|
if key & 0xFF == ord("r"):
|
|
# 重置光流追踪器(当追踪漂移时使用);焦点需在 OpenCV 窗口上
|
|
sensor1.reset()
|
|
print("传感器已重置")
|
|
|
|
except BaseException as exc:
|
|
session_abnormal = datetime.now()
|
|
print(
|
|
f"[session] ABNORMAL at {session_abnormal:%Y-%m-%d %H:%M:%S}: "
|
|
f"{type(exc).__name__}: {exc}",
|
|
flush=True,
|
|
)
|
|
raise
|
|
finally:
|
|
session_end = datetime.now()
|
|
elapsed = session_end - session_start
|
|
print(
|
|
f"[session] END {session_end:%Y-%m-%d %H:%M:%S} "
|
|
f"(elapsed {elapsed})",
|
|
flush=True,
|
|
)
|
|
if sensor1 is not None:
|
|
try:
|
|
sensor1.disconnect()
|
|
except Exception as disconnect_err:
|
|
print(f"[session] disconnect error: {disconnect_err}", flush=True)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|