import os os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0" # Force pyqtgraph to bind to PyQt5 when multiple Qt bindings are installed. os.environ.setdefault("PYQTGRAPH_QT_LIB", "PyQt5") 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, QHBoxLayout, QVBoxLayout, QWidget, QSizePolicy, QShortcut, QApplication from pyqtgraph.Qt import QtGui # Final desired appearance on the mirrored 1872x1560 (6:5) display. # The desktop is first shown on a 1920x1080 (16:9) screen, then stretched by # the OS onto the 6:5 mirror. We therefore pre-distort the source layout so # the mirrored output looks correct. TARGET_SINGLE_PANEL_W = 936 TARGET_ASPECT_W = TARGET_SINGLE_PANEL_W * 2 TARGET_VIDEO_FRAME_H = 1053 TARGET_BOTTOM_PANEL_H = 507 TARGET_ASPECT_H_TOTAL = TARGET_VIDEO_FRAME_H + TARGET_BOTTOM_PANEL_H TARGET_MIRROR_W = 1872 TARGET_MIRROR_H = 1560 SINGLE_PANEL_W = TARGET_SINGLE_PANEL_W ASPECT_W = TARGET_ASPECT_W BOTTOM_PANEL_H = TARGET_BOTTOM_PANEL_H VIDEO_FRAME_W = TARGET_ASPECT_W VIDEO_FRAME_H = TARGET_VIDEO_FRAME_H ASPECT_H_TOTAL = TARGET_ASPECT_H_TOTAL UI_SCALE = 1.0 ARROW_STRETCH = 507 PLOT_STRETCH = 429 VIDEO_LOOP_PATH = "./叠动机械爪_成片.m4v" npoints = 120 # 图表数据长度 dispaly_angle = -90 # 箭头图显示角度 touch_threshold = 5 # 触碰反应阈值 frame_to_recover = 10 # 触碰后等待回复帧数 # Opneing Hour for keep reading image open_hour = 9 close_hour = 17 manual_open_enable = True # 控制是否可以手动切换营业状态 (按键G) is_open = True # 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 = False def scale_ui_px(px, floor=1): return max(floor, int(round(px * UI_SCALE))) def compute_source_layout(screen_w, screen_h): """Pre-distort the app on the source screen so the mirrored monitor is correct.""" target_video_ratio = TARGET_VIDEO_FRAME_H / float(TARGET_ASPECT_H_TOTAL) target_bottom_ratio = TARGET_BOTTOM_PANEL_H / float(TARGET_ASPECT_H_TOTAL) return { "window_w": max(320, int(screen_w)), "window_h": max(240, int(screen_h)), "video_w": max(320, int(screen_w)), "video_h": max(1, int(round(screen_h * target_video_ratio))), "bottom_h": max(1, int(round(screen_h * target_bottom_ratio))), "single_panel_w": max(1, int(round(screen_w / 2.0))), } 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(panel_widget): """ Pixel size of the square arrow canvas from the window client area and the fixed horizontal split ARROW_STRETCH : PLOT_STRETCH (matches QHBoxLayout stretch). """ w = panel_widget.width() h = panel_widget.height() if w < 16 or h < 16: return max(32, ASPECT_H_TOTAL) w_arrow = int(round(w * ARROW_STRETCH / float(ARROW_STRETCH + PLOT_STRETCH))) # Keep the rendered arrow image square inside the left panel. return max(32, min(h, w_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 using source-screen pre-distortion.""" 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): pm = pm.scaled(lw, lh, Qt.IgnoreAspectRatio, Qt.SmoothTransformation) label.setPixmap(pm) def set_video_label_bgr(label, bgr_image): """Show loop video in the top frame using the fitted display size.""" 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): pm = pm.scaled(lw, lh, Qt.IgnoreAspectRatio, Qt.FastTransformation) label.setPixmap(pm) def read_loop_video_frame_realtime(video_cap, video_clock): """Read loop-video frame paced to wall clock; skip frames when main loop is slow.""" if video_cap is None or not video_cap.isOpened(): return False, None now = time.perf_counter() fps = max(1.0, float(video_clock.get("fps", 30.0))) frame_dt = 1.0 / fps next_ts = video_clock.get("next_ts") if next_ts is None: next_ts = now frames_due = 1 if now > next_ts + frame_dt: frames_due += int((now - next_ts) / frame_dt) # Cap skip burst to avoid long catch-up loops after pauses. frames_due = min(frames_due, int(max(2.0, fps))) for _ in range(frames_due - 1): if not video_cap.grab(): video_cap.set(cv2.CAP_PROP_POS_FRAMES, 0) if not video_cap.grab(): return False, None vret, vframe = video_cap.read() if not vret: video_cap.set(cv2.CAP_PROP_POS_FRAMES, 0) vret, vframe = video_cap.read() if not vret: return False, None next_ts = next_ts + frames_due * frame_dt if next_ts < now - 2.0 * frame_dt: next_ts = now video_clock["next_ts"] = next_ts return True, vframe 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", scale_ui_px(11, floor=8)) 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])) def build_sensor_panel(parent_layout): panel = QWidget() panel_layout = QHBoxLayout(panel) panel_layout.setContentsMargins(0, 0, 0, 0) panel_layout.setSpacing(0) # Ignored: do not let pixmap/content drive QLabel size — layout stretch sets arrow vs plot widths. _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) panel_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) panel_layout.addWidget(plot_widget, stretch=PLOT_STRETCH) disconnected_label = QLabel("已断开") disconnected_label.setAlignment(Qt.AlignCenter) disconnected_label.setStyleSheet( f"background-color: #0B1016; color: #FF4D4D; font-size: {scale_ui_px(54, floor=24)}px; " "font-weight: bold; letter-spacing: 2px; margin: 0; padding: 0; border: none;" ) disconnected_label.hide() panel_layout.addWidget(disconnected_label, stretch=SINGLE_PANEL_W) parent_layout.addWidget(panel, stretch=1) p1, p2, p3, plot_af, plot_tfx, plot_tfy = create_plot(plot_widget) data = np.zeros([npoints, 2, 3]) for i in range(3): data[:, 0, i] = np.linspace(0.0, npoints / 30.0, npoints) return { "panel": panel, "arrow_label": arrow_label, "plot_widget": plot_widget, "disconnected_label": disconnected_label, "p1": p1, "p2": p2, "p3": p3, "plot_af": plot_af, "plot_tfx": plot_tfx, "plot_tfy": plot_tfy, "data": data, } # 实时图表:单窗口,两套并排(左 sensor1,右 sensor2) pg.setConfigOption("background", PLOT_THEME_BG) pg.setConfigOption("foreground", PLOT_THEME_FG) app = pg.mkQApp("Wuxi Demo") screen = app.primaryScreen() screen_geometry = screen.availableGeometry() if screen is not None else None screen_w = screen_geometry.width() if screen_geometry is not None else 1920 screen_h = screen_geometry.height() if screen_geometry is not None else 1080 source_layout = compute_source_layout(screen_w, screen_h) ASPECT_W = source_layout["window_w"] ASPECT_H_TOTAL = source_layout["window_h"] VIDEO_FRAME_W = source_layout["video_w"] VIDEO_FRAME_H = source_layout["video_h"] BOTTOM_PANEL_H = source_layout["bottom_h"] SINGLE_PANEL_W = source_layout["single_panel_w"] UI_SCALE = max(0.1, screen_h / float(TARGET_ASPECT_H_TOTAL)) combined_win = CombinedTactileWindow() combined_win.setWindowTitle("Wuxi tactile — dual flow + forces") main_layout = QVBoxLayout(combined_win) main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setSpacing(0) video_label = QLabel() video_label.setAlignment(Qt.AlignCenter) video_label.setStyleSheet("background-color: #000000; margin: 0; padding: 0; border: none;") video_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) video_label.setMinimumSize(0, 0) main_layout.addWidget(video_label, stretch=VIDEO_FRAME_H) bottom_container = QWidget() bottom_layout = QHBoxLayout(bottom_container) bottom_layout.setContentsMargins(0, 0, 0, 0) bottom_layout.setSpacing(0) main_layout.addWidget(bottom_container, stretch=BOTTOM_PANEL_H) panel1_ui = build_sensor_panel(bottom_layout) panel2_ui = build_sensor_panel(bottom_layout) 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.showFullScreen() closed_hour_overlay = QLabel(combined_win) closed_hour_overlay.setAlignment(Qt.AlignCenter) closed_hour_overlay.setStyleSheet( f"background-color: {PLOT_THEME_BG}; color: #A0A0A0; " f"font-size: {scale_ui_px(72, floor=32)}px; font-weight: bold; " "margin: 0; padding: 0; border: none;" ) closed_hour_overlay.setText("Closed hour\n非营业时间") closed_hour_overlay.hide() start_time = None # Will be set when sensor starts fps = 30.0 def outside_opening_hours(now): return now.hour < open_hour or now.hour >= close_hour def main(): global start_time parser = argparse.ArgumentParser() parser.add_argument("--video1","-v1", type=str, default="0", help="视频文件路径") parser.add_argument("--video2","-v2", type=str, 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 sensor2 = None video_cap = None try: input1 = int(args.video1) input2 = int(args.video2) is_camera = True except: is_camera = False try: if is_camera: sensor1 = orisys.Sensor( input1, config_name="./config/ddjx01_wuxi.json", cal_path="./config/dd01a_0306.npy", isstitch=True, cuda=False, verbose=False, ) sensor2 = orisys.Sensor( input2, config_name="./config/ddjx01_wuxi.json", cal_path="./config/dd01a_0306.npy", isstitch=True, cuda=False, verbose=False, ) else: sensor1 = orisys.Sensor( args.video1, config_name="./config/ddjx01_wuxi.json", cal_path="./config/dd01a_0306.npy", isstitch=True, cuda=False, verbose=False, ) sensor2 = orisys.Sensor( args.video2, config_name="./config/ddjx01_wuxi.json", cal_path="./config/dd01a_0306.npy", isstitch=True, cuda=False, verbose=False, ) def _shortcut_reset_tracker(): sensor1.reset() sensor2.reset() print("传感器已重置 (快捷键 R)", flush=True) def _shortcut_quit(): combined_win.close() def _shortcut_toggle_open(): global is_open is_open = not is_open print(f"营业状态: {'营业' if is_open else '非营业'}", flush=True) _sc_r = QShortcut(QKeySequence(Qt.Key_R), combined_win) _sc_r.activated.connect(_shortcut_reset_tracker) if manual_open_enable: _sc_g = QShortcut(QKeySequence(Qt.Key_G), combined_win) _sc_g.activated.connect(_shortcut_toggle_open) _sc_q = QShortcut(QKeySequence(Qt.Key_Q), combined_win) _sc_q.activated.connect(_shortcut_quit) video_cap = cv2.VideoCapture(VIDEO_LOOP_PATH) video_clock = {"fps": 30.0, "next_ts": None} if not video_cap.isOpened(): print(f"[video] Failed to open loop video: {VIDEO_LOOP_PATH}", flush=True) else: src_fps = float(video_cap.get(cv2.CAP_PROP_FPS) or 0.0) if np.isfinite(src_fps) and src_fps >= 1.0: video_clock["fps"] = src_fps start_time = time.time() is_save = False frame_count = 0 writer = None sensor1_active = True sensor2_active = True # Local to main(): assignments inside the loop make this local; must exist # before the first iteration that skips the closed branch. start_open = False while True: now = datetime.now() if outside_opening_hours(now) or not is_open: start_open = False closed_hour_overlay.setGeometry(0, 0, combined_win.width(), combined_win.height()) closed_hour_overlay.raise_() closed_hour_overlay.show() app.processEvents() if combined_win.user_closed or not combined_win.isVisible(): print("\n窗口已关闭,退出程序") if writer is not None: writer.release() writer = None break time.sleep(1) continue if not start_open: start_open = True sensor1.reset() sensor2.reset() print(f"传感器已重置: {datetime.now():%Y-%m-%d %H:%M:%S}", flush=True) closed_hour_overlay.hide() if video_cap is not None and video_cap.isOpened(): vret, vframe = read_loop_video_frame_realtime(video_cap, video_clock) if vret: if vframe.shape[1] != VIDEO_FRAME_W or vframe.shape[0] != VIDEO_FRAME_H: vframe = cv2.resize(vframe, (VIDEO_FRAME_W, VIDEO_FRAME_H), interpolation=cv2.INTER_LINEAR) set_video_label_bgr(video_label, vframe) if sensor1_active: img1_valid = sensor1.get_img() if not img1_valid: panel1_ui["arrow_label"].hide() panel1_ui["plot_widget"].hide() panel1_ui["disconnected_label"].show() sensor1_active = False print(f"Sensor1 disconnected. Showing DISCONNECTED screen. {datetime.now():%Y-%m-%d %H:%M:%S}", flush=True) else: sensor1.compute_deformation(check_motion=True, tail_frames=frame_to_recover, threshold=touch_threshold) _, fn, fx, fy, flow = sensor1.read_info( sensor1.info.FPS, sensor1.info.FNORMAL, sensor1.info.FSHEARX, sensor1.info.FSHEARY, sensor1.info.VRAW, ) _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] lw, lh = panel1_ui["arrow_label"].width(), panel1_ui["arrow_label"].height() if lw > 16 and lh > 16: side = max(32, min(lw, lh)) else: side = compute_arrow_canvas_side(panel1_ui["panel"]) 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(panel1_ui["arrow_label"], arrows) data1 = panel1_ui["data"] for i in range(3): data1[:-1, 0, i] = data1[1:, 0, i] data1[:-1, 1, i] = data1[1:, 1, i] data1[-1, 0, :] = frame_count / 15.0 data1[-1, 1, 0] = fn data1[-1, 1, 1] = abs(fx) data1[-1, 1, 2] = abs(fy) update( panel1_ui["plot_af"], panel1_ui["plot_tfx"], panel1_ui["plot_tfy"], data1, panel1_ui["p1"], panel1_ui["p2"], panel1_ui["p3"], ) if sensor2_active: img2_valid = sensor2.get_img() if not img2_valid: panel2_ui["arrow_label"].hide() panel2_ui["plot_widget"].hide() panel2_ui["disconnected_label"].show() sensor2_active = False print(f"Sensor2 disconnected. Showing DISCONNECTED screen. {datetime.now():%Y-%m-%d %H:%M:%S}", flush=True) else: sensor2.compute_deformation(check_motion=True, tail_frames=frame_to_recover, threshold=touch_threshold) _, fn2, fx2, fy2, flow2 = sensor2.read_info( sensor2.info.FPS, sensor2.info.FNORMAL, sensor2.info.FSHEARX, sensor2.info.FSHEARY, sensor2.info.VRAW, ) _bg_bgr2 = (22, 16, 11) _img2 = sensor2.img _h2, _w2 = _img2.shape[:2] black_img2 = np.empty((_h2, _w2, 3), dtype=_img2.dtype) black_img2[:, :, 0] = _bg_bgr2[0] black_img2[:, :, 1] = _bg_bgr2[1] black_img2[:, :, 2] = _bg_bgr2[2] lw2, lh2 = panel2_ui["arrow_label"].width(), panel2_ui["arrow_label"].height() if lw2 > 16 and lh2 > 16: side2 = max(32, min(lw2, lh2)) else: side2 = compute_arrow_canvas_side(panel2_ui["panel"]) arrows2 = draw_arrows_wuxi( black_img2, flow2, threshold=2, grid_spacing=20, arrow_scale=0.5, below_threshold_color=(160, 160, 160), target_size=int(side2), ) set_arrow_label_bgr(panel2_ui["arrow_label"], arrows2) data2 = panel2_ui["data"] for i in range(3): data2[:-1, 0, i] = data2[1:, 0, i] data2[:-1, 1, i] = data2[1:, 1, i] data2[-1, 0, :] = frame_count / 15.0 data2[-1, 1, 0] = fn2 data2[-1, 1, 1] = abs(fx2) data2[-1, 1, 2] = abs(fy2) update( panel2_ui["plot_af"], panel2_ui["plot_tfx"], panel2_ui["plot_tfy"], data2, panel2_ui["p1"], panel2_ui["p2"], panel2_ui["p3"], ) app.processEvents() now = datetime.now() if now.hour == 9 and now.minute == 0 and now.second == 0: if sensor1_active: sensor1.reset() if sensor2_active: sensor2.reset() print( f"[session] 传感器已重置: {now:%Y-%m-%d %H:%M:%S} ", flush=True, ) if not sensor1_active and not sensor2_active: while not combined_win.user_closed and combined_win.isVisible(): app.processEvents() time.sleep(0.05) break 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)) 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} {datetime.now():%Y-%m-%d %H:%M:%S}", flush=True) if 'sensor2' in locals() and sensor2 is not None: try: sensor2.disconnect() except Exception as disconnect_err: print(f"[session] disconnect error (sensor2): {disconnect_err} {datetime.now():%Y-%m-%d %H:%M:%S}", flush=True) if video_cap is not None: video_cap.release() if __name__ == '__main__': main()