update wuxi demo
This commit is contained in:
+2
-2
@@ -21,11 +21,11 @@ def install_nuitka():
|
|||||||
def get_version() -> str:
|
def get_version() -> str:
|
||||||
init_file = Path("src/orisys/__init__.py")
|
init_file = Path("src/orisys/__init__.py")
|
||||||
if not init_file.exists():
|
if not init_file.exists():
|
||||||
return "0.3.1"
|
return "0.3.2"
|
||||||
for line in init_file.read_text(encoding="utf-8").splitlines():
|
for line in init_file.read_text(encoding="utf-8").splitlines():
|
||||||
if line.startswith("__version__"):
|
if line.startswith("__version__"):
|
||||||
return line.split("=", 1)[1].strip().strip('"').strip("'")
|
return line.split("=", 1)[1].strip().strip('"').strip("'")
|
||||||
return "0.3.1"
|
return "0.3.2"
|
||||||
|
|
||||||
|
|
||||||
def find_compiled_extension(output_dir: Path) -> Path:
|
def find_compiled_extension(output_dir: Path) -> Path:
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1014 KiB |
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "prototype",
|
||||||
|
"version": "1.0",
|
||||||
|
"camera": {
|
||||||
|
"resolution_raw": [1280, 720],
|
||||||
|
"fps": 15,
|
||||||
|
"units": 15,
|
||||||
|
"units_used": 9,
|
||||||
|
"resolution_stitch": [263, 263],
|
||||||
|
"stitch_res_list": [365,263,123,63]
|
||||||
|
},
|
||||||
|
"stitcher": {
|
||||||
|
"min_area_frac": 0.01,
|
||||||
|
"max_area_frac": 0.04,
|
||||||
|
"max_aspect_ratio": 25,
|
||||||
|
"index": [1,2,3,6,7,8,11,12,13],
|
||||||
|
"margin": 10,
|
||||||
|
"nominal_ov_pos": 67,
|
||||||
|
"n": [3,3],
|
||||||
|
"row_tol": 0.6,
|
||||||
|
"box_size": [180, 180]
|
||||||
|
},
|
||||||
|
"tracker": {
|
||||||
|
"adaptive": true,
|
||||||
|
"lower_threshold": 12.0,
|
||||||
|
"upper_threshold": 18.0,
|
||||||
|
"loop_closure_interval": 1,
|
||||||
|
"algorithm": "dis",
|
||||||
|
"dis_preset": "medium",
|
||||||
|
"dis_mode": 3,
|
||||||
|
"algorithm_list": ["raft", "dis"],
|
||||||
|
"hhd_downsample": 3,
|
||||||
|
"backend": "cpu"
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"min_contour_area": 3000,
|
||||||
|
"flow_threshold":1,
|
||||||
|
"div_threshold":0.02,
|
||||||
|
"expansion_threshold": 0.01,
|
||||||
|
"kernel_size": [5,5],
|
||||||
|
"denoise_sigma": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
+631
@@ -0,0 +1,631 @@
|
|||||||
|
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()
|
||||||
+5
-5
@@ -1,5 +1,5 @@
|
|||||||
叠动科技软件使用手册
|
叠动科技软件使用手册
|
||||||
版本: 0.3.1
|
版本: 0.3.2
|
||||||
更新日期: 2026年3月6日
|
更新日期: 2026年3月6日
|
||||||
|
|
||||||
# 简介
|
# 简介
|
||||||
@@ -57,9 +57,9 @@ orisys SDK 以 .whl 文件形式提供,您需要根据操作系统、Python
|
|||||||
## 下载正确的 .whl 文件
|
## 下载正确的 .whl 文件
|
||||||
从官方渠道获取 .whl 文件,文件名格式通常为:
|
从官方渠道获取 .whl 文件,文件名格式通常为:
|
||||||
orisys-{版本}-{Python版本}-{Python版本}-{操作系统}_{硬件架构}.whl
|
orisys-{版本}-{Python版本}-{Python版本}-{操作系统}_{硬件架构}.whl
|
||||||
例如:orisys-0.3.1-cp310-cp310-win_amd64.whl 表示:
|
例如:orisys-0.3.2-cp310-cp310-win_amd64.whl 表示:
|
||||||
|
|
||||||
版本:0.3.1
|
版本:0.3.2
|
||||||
Python 版本:3.10(cp310)
|
Python 版本:3.10(cp310)
|
||||||
操作系统:Windows(win)
|
操作系统:Windows(win)
|
||||||
硬件架构:64位(amd64)
|
硬件架构:64位(amd64)
|
||||||
@@ -71,7 +71,7 @@ Python 版本:3.10(cp310)
|
|||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
# 请将文件名替换为您实际下载的文件名
|
# 请将文件名替换为您实际下载的文件名
|
||||||
pip install orisys-0.3.1-cp310-cp310-win_amd64.whl
|
pip install orisys-0.3.2-cp310-cp310-win_amd64.whl
|
||||||
```
|
```
|
||||||
|
|
||||||
## 安装 CuPy(可选)
|
## 安装 CuPy(可选)
|
||||||
@@ -108,7 +108,7 @@ python
|
|||||||
import orisys
|
import orisys
|
||||||
print(orisys.__version__)
|
print(orisys.__version__)
|
||||||
```
|
```
|
||||||
如果输出版本号(例如 0.3.1),则说明安装成功。
|
如果输出版本号(例如 0.3.2),则说明安装成功。
|
||||||
|
|
||||||
# 使用方法
|
# 使用方法
|
||||||
## 运行示例程序
|
## 运行示例程序
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ opencv_python>=4.10.0.84
|
|||||||
scipy==1.13
|
scipy==1.13
|
||||||
setuptools>=45.2.0
|
setuptools>=45.2.0
|
||||||
av
|
av
|
||||||
|
numba
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ if configs_dir.exists():
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="orisys",
|
name="orisys",
|
||||||
version="0.3.1",
|
version="0.3.2",
|
||||||
description="Orisys - Optical Tactile Sensor SDK",
|
description="Orisys - Optical Tactile Sensor SDK",
|
||||||
author="Your Name",
|
author="Your Name",
|
||||||
package_dir={"": "src"},
|
package_dir={"": "src"},
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ A Python library for processing and analyzing optical tactile sensor data,
|
|||||||
including image stitching, optical flow tracking, and deformation decomposition.
|
including image stitching, optical flow tracking, and deformation decomposition.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "0.3.1"
|
__version__ = "0.3.2"
|
||||||
__author__ = "Orisys"
|
__author__ = "Orisys"
|
||||||
|
|
||||||
from .sensor import Sensor
|
from .sensor import Sensor
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ class Sensor:
|
|||||||
shape = (self.__config_cam["resolution_stitch"][1], self.__config_cam["resolution_stitch"][0])
|
shape = (self.__config_cam["resolution_stitch"][1], self.__config_cam["resolution_stitch"][0])
|
||||||
self.img_size_raw = None
|
self.img_size_raw = None
|
||||||
self.img_filtered = None
|
self.img_filtered = None
|
||||||
|
self.tail_frames_count = 0
|
||||||
|
|
||||||
# Optical Flow Data Variables (public)
|
# Optical Flow Data Variables (public)
|
||||||
self.flow = np.zeros(shape + (2,), dtype=np.float32)
|
self.flow = np.zeros(shape + (2,), dtype=np.float32)
|
||||||
@@ -268,7 +269,7 @@ class Sensor:
|
|||||||
# print(f"mag: {mag.max()}, {mag.min()}, {mag.mean()}")
|
# print(f"mag: {mag.max()}, {mag.min()}, {mag.mean()}")
|
||||||
return mag.mean() > threshold
|
return mag.mean() > threshold
|
||||||
|
|
||||||
def compute_deformation(self, decompose=True, check_motion=True, threshold=6):
|
def compute_deformation(self, decompose=True, check_motion=False, threshold=6, tail_frames = 5):
|
||||||
"""
|
"""
|
||||||
Compute optical flow and decompose into force components.
|
Compute optical flow and decompose into force components.
|
||||||
|
|
||||||
@@ -282,9 +283,14 @@ class Sensor:
|
|||||||
# self.compute_hhd()
|
# self.compute_hhd()
|
||||||
if check_motion:
|
if check_motion:
|
||||||
if self.detect_motion(threshold=threshold):
|
if self.detect_motion(threshold=threshold):
|
||||||
|
self.tail_frames_count = 0
|
||||||
self.compute_hhd()
|
self.compute_hhd()
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
|
if self.tail_frames_count <= tail_frames:
|
||||||
|
self.tail_frames_count +=1
|
||||||
|
self.compute_hhd()
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
if decompose:
|
if decompose:
|
||||||
|
|||||||
@@ -85,11 +85,11 @@ class Tracker:
|
|||||||
def track(self, frame):
|
def track(self, frame):
|
||||||
"""Main tracking function that dispatches to CUDA or CPU implementation"""
|
"""Main tracking function that dispatches to CUDA or CPU implementation"""
|
||||||
if self.__algorithm == "raft":
|
if self.__algorithm == "raft":
|
||||||
return self.track_raft(frame)
|
return self.track_raft(frame.copy())
|
||||||
elif self.__algorithm == "dis":
|
elif self.__algorithm == "dis":
|
||||||
if frame.ndim == 3:
|
if frame.ndim == 3:
|
||||||
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
||||||
return self.track_cpu(frame)
|
return self.track_cpu(frame.copy())
|
||||||
|
|
||||||
def track_cpu(self, frame):
|
def track_cpu(self, frame):
|
||||||
"""CPU-based optical flow tracking"""
|
"""CPU-based optical flow tracking"""
|
||||||
|
|||||||
Reference in New Issue
Block a user