diff --git a/DIS_scale_test.py b/DIS_scale_test.py deleted file mode 100644 index eba3992..0000000 --- a/DIS_scale_test.py +++ /dev/null @@ -1,106 +0,0 @@ -""" -示例 1: 基础用法 - 单传感器同步处理 - -本示例演示 orisys 的基本使用流程: -1. 初始化传感器(视频文件或摄像头) -2. 循环获取图像并计算形变和触碰点 -3. 读取各类传感器数据 -4. 可视化显示 - - -""" -import os -os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0" -import cv2 -import orisys -import numpy as np -import time -def main(): - - # 步骤1:创建传感器对象 (必要步骤),config_name为配置文件名(内置配置文件名)或路径(自定义配置文件路径) - # 方式1: 从视频文件读取 - sensor1 = orisys.Sensor("s1.mp4", config_name="./config/prototype_of1.json", cal_path= "./config/cal.npy", isstitch=True, cuda=True, verbose=True) - sensor2 = orisys.Sensor("s1.mp4", config_name="./config/prototype_of2.json", cal_path= "./config/cal.npy", isstitch=True, cuda=True, verbose=True) - - - # 方式2: 从摄像头读取(取消注释使用) - # sensor = orisys.Sensor(0, config_name="./config/lens_test.json", cal_path= "./config/cal.npy", isstitch=True, cuda=True, verbose=True) - - print("\n按 'q' 退出程序,按 'r' 重置光流追踪器") - - - while True: - # 步骤2:信息提取 - # 步骤 2.1: 获取并拼接图像 (必要步骤) - sensor1.get_img() - sensor2.get_img() - # if sensor.detect_motion(threshold=2): - # print("motion detected") - # 步骤 2.2: 计算形变场 (必要步骤) - sensor1.compute_deformation() - sensor2.compute_deformation() - - - #步骤 2.4: 读取信息 - fps, img, flow = sensor1.read_info( - sensor1.info.FPS, - sensor1.info.IMG, - sensor1.info.VRAW, - ) - fps2, img2, flow2 = sensor2.read_info( - sensor2.info.FPS, - sensor2.info.IMG, - sensor2.info.VRAW, - ) - - # flow_mag = np.linalg.norm(flow, axis=2) - # flow_mag2 = np.linalg.norm(flow2, axis=2) - # flow_diff = np.linalg.norm(flow - flow2, axis=2) - - # print(f"flow_mag: {flow_mag.mean()}, {flow_mag.min()}, flow_mag2: {flow_mag2.mean()}, {flow_mag2.min()}, flow_diff: {flow_diff.mean()}, {flow_diff.min()}") - # flow_diff_norm = cv2.applyColorMap((flow_diff*255).astype(np.uint8), cv2.COLORMAP_JET) - # cv2.imshow("flow_diff", flow_diff_norm) - - #步骤 3: 可视化显示 - # 步骤 3.1: 绘制形变场 - arrows = orisys.util.draw_arrows( - img, - flow, - threshold=5, - grid_spacing=10, - arrow_scale=1.0 - ) - arrows2 = orisys.util.draw_arrows( - img2, - flow2, - threshold=5, - grid_spacing=10, - arrow_scale=1.0 - ) - cv2.imshow("形变场 Deformation", arrows) - cv2.imshow("形变场 Deformation2", arrows2) - - - #步骤 3.4: 打印资料 - print(f"fps: {fps}, fps2: {fps2}") - - #步骤 3.5: 键盘控制 - key = cv2.waitKey(1) & 0xFF - if key == ord("q"): - print("\n退出程序...") - break - if key == ord("r"): - # 重置光流追踪器(当追踪漂移时使用) - sensor1.reset() - sensor2.reset() - print("传感器已重置") - - # ========================================================================= - # 4. 清理资源 - # ========================================================================= - sensor1.disconnect() # 释放视频源和相关资源 - sensor2.disconnect() # 释放视频源和相关资源 - - -if __name__ == '__main__': - main() diff --git a/camera_gui.py b/camera_gui.py deleted file mode 100644 index f29149d..0000000 --- a/camera_gui.py +++ /dev/null @@ -1,1147 +0,0 @@ -""" -Camera GUI using PyQt5 and OpenCV -Displays two video streams: deformation arrows and raw image -""" -import os -os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0" -import sys -import time -import cv2 -import numpy as np -import pyqtgraph as pg -from orisys import Sensor -from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, - QHBoxLayout, QPushButton, QLabel, QCheckBox, QLineEdit, QFileDialog) -from PyQt5.QtCore import Qt, QTimer -from PyQt5.QtGui import QImage, QPixmap, QIcon - - -def draw_arrows_fade(img, flow, depth, depth_scale=0.5, grid_spacing=20, arrow_scale=1.0, threshold=5.0): - """ - Draw optical flow arrows on image with transparency based on magnitude. - - Args: - img: Input image (BGR or grayscale) - flow: Optical flow field with shape (flow_height, flow_width, 2) - depth: Depth information - depth_scale: Scale factor for depth visualization - grid_spacing: Spacing between arrows in pixels (default: 20) - arrow_scale: Scale factor for arrow length (default: 1.0) - threshold: Minimum flow magnitude to display arrow (default: 5.0) - - Returns: - Image with arrows drawn, blended with original image - """ - - depth_img = cv2.applyColorMap(((depth+1)*depth_scale* 255.0).astype('uint8'), cv2.COLORMAP_HOT) - # Make a copy of the image - img_copy = depth_img.copy() - - - # Convert grayscale to BGR if needed - if len(img_copy.shape) == 2 or img_copy.shape[2] == 1: - img_copy = cv2.cvtColor(img_copy, cv2.COLOR_GRAY2BGR) - - # Get image and flow dimensions - img_height, img_width = img_copy.shape[:2] - flow_height, flow_width = flow.shape[:2] - - # Calculate scaling factors between image and flow dimensions - scale_x = img_width / flow_width - scale_y = img_height / flow_height - - # Create grid positions in flow coordinates - grid_y, grid_x = np.mgrid[grid_spacing//2:flow_height:grid_spacing, - grid_spacing//2:flow_width:grid_spacing] - - # Flatten grid positions - grid_y_flat = grid_y.flatten() - grid_x_flat = grid_x.flatten() - - # Get flow vectors at grid positions - flow_vectors = flow[grid_y_flat, grid_x_flat, :] # Shape: (n_points, 2) - - # Calculate flow magnitudes - magnitudes = np.linalg.norm(flow_vectors, axis=1) - - # Filter arrows by threshold: only show arrows above threshold - valid_mask = magnitudes >= threshold - - # Normalize magnitudes to 0-1 range for alpha blending - # Zero magnitude = transparent, higher magnitude = more opaque white - max_magnitude = magnitudes.max() if magnitudes.max() > 0 else 1.0 - alpha_values = magnitudes / max_magnitude - - # Scale grid positions to image coordinates - img_grid_x = (grid_x_flat * scale_x).astype(np.int32) - img_grid_y = (grid_y_flat * scale_y).astype(np.int32) - - # Scale flow vectors to image coordinates - scaled_flow_x = flow_vectors[:, 0] * scale_x / arrow_scale - scaled_flow_y = flow_vectors[:, 1] * scale_y / arrow_scale - - # Calculate arrow endpoints - end_x = (img_grid_x + scaled_flow_x).astype(np.int32) - end_y = (img_grid_y + scaled_flow_y).astype(np.int32) - - # Create overlay image with white background for arrows - overlay = np.zeros_like(img_copy) - - # Create weight map for blending based on arrow presence and magnitude - weight_map = np.zeros((img_height, img_width), dtype=np.float32) - - # Draw arrows on overlay with white color and build weight map - for i in range(len(img_grid_x)): - # Skip if magnitude is below threshold - if not valid_mask[i]: - continue - - # Check bounds - if (0 <= img_grid_x[i] < img_width and 0 <= img_grid_y[i] < img_height and - 0 <= end_x[i] < img_width and 0 <= end_y[i] < img_height): - - # Get alpha value (0 = transparent, 1 = opaque white) - alpha = alpha_values[i] - - # Skip if magnitude is zero (transparent) - if alpha <= 0: - continue - - # Use constant thickness for all arrows (magnitude affects opacity, not thickness) - thickness = 2 - - # White color for arrows - color = (255, 255, 255) - - # Draw arrow on overlay - cv2.arrowedLine(overlay, - (img_grid_x[i], img_grid_y[i]), - (end_x[i], end_y[i]), - color, - thickness=thickness, - tipLength=0.3) - - # Create a line mask for this arrow in the weight map - line_mask = np.zeros((img_height, img_width), dtype=np.float32) - cv2.line(line_mask, - (img_grid_x[i], img_grid_y[i]), - (end_x[i], end_y[i]), - 1.0, - thickness=thickness) - # Add arrow tip approximation - cv2.circle(line_mask, (end_x[i], end_y[i]), thickness, 1.0, -1) - - # Weight by alpha (magnitude) - use maximum for overlapping arrows - weight_map = np.maximum(weight_map, line_mask * alpha) - - # Expand weight map to 3 channels - weight_map_3ch = np.stack([weight_map] * 3, axis=2) - - # Blend: result = original * (1 - weight) + overlay * weight - result = (img_copy.astype(np.float32) * (1 - weight_map_3ch) + - overlay.astype(np.float32) * weight_map_3ch) - - return result.astype(np.uint8) - - -class CameraGUI(QMainWindow): - def __init__(self): - super().__init__() - self.setWindowTitle("Orisys Sensor Demo") - - # Set minimum window size and aspect ratio - self.setMinimumSize(1200, 800) - self.setGeometry(100, 100, 1366, 768) # Initial size - - # Set window icon (logo) - if os.path.exists("orisys_logo_notext.png"): - self.setWindowIcon(QIcon("orisys_logo_notext.png")) - - # Sensor variables - self.sensor = None - self.camera_running = False - self.timer = QTimer() - self.timer.timeout.connect(self.update_frame) - - # Recording variables - self.recording = False - self.video_writer_arrows = None - self.video_writer_raw = None - # Default to Windows Pictures folder - pictures_folder = os.path.join(os.path.expanduser("~"), "Pictures") - self.record_directory = os.path.join(pictures_folder, "OrisysRecordings") - self.record_filename = "recording" - - # Plotting variables - self.npoints = 100 # Chart data length - self.start_time = None # Will be set when sensor starts - self.data = None # Will be initialized in init_plots - self.max_magnitude = 0.0 # Track maximum magnitude for scaling - self.dynamic_scaling = True # Enable dynamic scaling by default - self.max_scale = 500000.0 # Default max scale value - - # Create GUI - self.init_ui() - - def init_ui(self): - # Central widget - central_widget = QWidget() - self.setCentralWidget(central_widget) - - # Main layout: horizontal split between left (controls/images) and right (plots) - main_layout = QHBoxLayout(central_widget) - main_layout.setSpacing(15) - - # Left side: Controls and images - left_panel = QWidget() - left_panel_layout = QVBoxLayout(left_panel) - left_panel_layout.setSpacing(10) - - # Title (left aligned) - title_label = QLabel("叠动视触觉传感器演示") - title_label.setStyleSheet(""" - font-size: 24px; - font-weight: bold; - padding: 10px; - color: #2c3e50; - font-family: 'Segoe UI', Arial, sans-serif; - background-color: #ecf0f1; - border-radius: 8px; - """) - title_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) - left_panel_layout.addWidget(title_label) - - # Button layout (top left) - button_layout = QHBoxLayout() - button_layout.setSpacing(10) - - # Start button - self.start_button = QPushButton("Start") - self.start_button.setStyleSheet(""" - QPushButton { - background-color: #27ae60; - color: white; - font-size: 14px; - font-weight: bold; - font-family: 'Segoe UI', Arial, sans-serif; - padding: 12px 35px; - border-radius: 6px; - border: none; - } - QPushButton:hover { - background-color: #229954; - } - QPushButton:pressed { - background-color: #1e8449; - } - QPushButton:disabled { - background-color: #95a5a6; - color: #ecf0f1; - } - """) - self.start_button.clicked.connect(self.start_camera) - button_layout.addWidget(self.start_button) - - # Stop button - self.stop_button = QPushButton("Stop") - self.stop_button.setStyleSheet(""" - QPushButton { - background-color: #e74c3c; - color: white; - font-size: 14px; - font-weight: bold; - font-family: 'Segoe UI', Arial, sans-serif; - padding: 12px 35px; - border-radius: 6px; - border: none; - } - QPushButton:hover { - background-color: #c0392b; - } - QPushButton:pressed { - background-color: #a93226; - } - QPushButton:disabled { - background-color: #95a5a6; - color: #ecf0f1; - } - """) - self.stop_button.setEnabled(False) - self.stop_button.clicked.connect(self.stop_camera) - button_layout.addWidget(self.stop_button) - - # Reset button - self.reset_button = QPushButton("Reset") - self.reset_button.setStyleSheet(""" - QPushButton { - background-color: #f39c12; - color: white; - font-size: 14px; - font-weight: bold; - font-family: 'Segoe UI', Arial, sans-serif; - padding: 12px 35px; - border-radius: 6px; - border: none; - } - QPushButton:hover { - background-color: #e67e22; - } - QPushButton:pressed { - background-color: #d35400; - } - QPushButton:disabled { - background-color: #95a5a6; - color: #ecf0f1; - } - """) - self.reset_button.setEnabled(False) # Disabled until camera starts - self.reset_button.clicked.connect(self.reset_sensor) - button_layout.addWidget(self.reset_button) - - # Exit button - exit_button = QPushButton("Exit") - exit_button.setStyleSheet(""" - QPushButton { - background-color: #7f8c8d; - color: white; - font-size: 14px; - font-weight: bold; - font-family: 'Segoe UI', Arial, sans-serif; - padding: 12px 35px; - border-radius: 6px; - border: none; - } - QPushButton:hover { - background-color: #5d6d7e; - } - QPushButton:pressed { - background-color: #566573; - } - """) - exit_button.clicked.connect(self.close) - button_layout.addWidget(exit_button) - - button_layout.addStretch() # Push buttons to the left - left_panel_layout.addLayout(button_layout) - - # Recording controls - recording_layout = QVBoxLayout() - recording_label = QLabel("录制设置") - recording_label.setStyleSheet(""" - font-size: 14px; - font-weight: bold; - font-family: 'Segoe UI', Arial, sans-serif; - color: #2c3e50; - padding: 5px; - """) - recording_layout.addWidget(recording_label) - - # Directory input - dir_layout = QHBoxLayout() - dir_label = QLabel("目录:") - dir_label.setStyleSheet("font-size: 12px; font-family: 'Segoe UI', Arial, sans-serif;") - dir_layout.addWidget(dir_label) - self.record_dir_textbox = QLineEdit() - # Set default to Windows Pictures folder - pictures_folder = os.path.join(os.path.expanduser("~"), "Pictures") - default_dir = os.path.join(pictures_folder, "OrisysRecordings") - self.record_dir_textbox.setText(default_dir) - self.record_dir_textbox.setStyleSheet(""" - font-size: 12px; - font-family: 'Segoe UI', Arial, sans-serif; - padding: 4px; - border: 1px solid #bdc3c7; - border-radius: 4px; - """) - self.record_dir_textbox.textChanged.connect(self.on_record_dir_changed) - dir_layout.addWidget(self.record_dir_textbox) - - # Browse button - browse_button = QPushButton("浏览") - browse_button.setStyleSheet(""" - QPushButton { - background-color: #95a5a6; - color: white; - font-size: 11px; - font-weight: bold; - font-family: 'Segoe UI', Arial, sans-serif; - padding: 6px 15px; - border-radius: 4px; - border: none; - } - QPushButton:hover { - background-color: #7f8c8d; - } - QPushButton:pressed { - background-color: #5d6d7e; - } - """) - browse_button.clicked.connect(self.browse_directory) - dir_layout.addWidget(browse_button) - - recording_layout.addLayout(dir_layout) - - # Filename input - filename_layout = QHBoxLayout() - filename_label = QLabel("文件名:") - filename_label.setStyleSheet("font-size: 12px; font-family: 'Segoe UI', Arial, sans-serif;") - filename_layout.addWidget(filename_label) - self.record_filename_textbox = QLineEdit() - self.record_filename_textbox.setText("recording") - self.record_filename_textbox.setStyleSheet(""" - font-size: 12px; - font-family: 'Segoe UI', Arial, sans-serif; - padding: 4px; - border: 1px solid #bdc3c7; - border-radius: 4px; - """) - self.record_filename_textbox.textChanged.connect(self.on_record_filename_changed) - filename_layout.addWidget(self.record_filename_textbox) - recording_layout.addLayout(filename_layout) - - # Record button - self.record_button = QPushButton("开始录制") - self.record_button.setStyleSheet(""" - QPushButton { - background-color: #3498db; - color: white; - font-size: 13px; - font-weight: bold; - font-family: 'Segoe UI', Arial, sans-serif; - padding: 10px 25px; - border-radius: 6px; - border: none; - } - QPushButton:hover { - background-color: #2980b9; - } - QPushButton:pressed { - background-color: #21618c; - } - QPushButton:disabled { - background-color: #95a5a6; - color: #ecf0f1; - } - """) - self.record_button.setEnabled(False) # Disabled until camera starts - self.record_button.clicked.connect(self.toggle_recording) - recording_layout.addWidget(self.record_button) - - recording_widget = QWidget() - recording_widget.setLayout(recording_layout) - recording_widget.setStyleSheet(""" - QWidget { - border: 1px solid #bdc3c7; - border-radius: 5px; - background-color: #f8f9fa; - padding: 8px; - } - """) - left_panel_layout.addWidget(recording_widget) - - # Left side: Image displays (side by side) - images_layout = QHBoxLayout() - - # Arrows (deformation) display - arrows_label = QLabel("变形") - arrows_label.setStyleSheet(""" - font-size: 15px; - font-weight: bold; - padding: 8px; - color: #2c3e50; - font-family: 'Segoe UI', Arial, sans-serif; - background-color: #ecf0f1; - border-radius: 5px; - """) - arrows_label.setAlignment(Qt.AlignCenter) - - self.arrows_image_label = QLabel() - self.arrows_image_label.setMinimumSize(400, 400) # Fixed initial size - self.arrows_image_label.setMaximumSize(400, 400) # Fixed size - self.arrows_image_label.setStyleSheet("background-color: black; border: 2px solid #4a90e2; border-radius: 5px;") - self.arrows_image_label.setAlignment(Qt.AlignCenter) - self.arrows_image_label.setScaledContents(True) # Allow scaling within fixed size - - arrows_container = QWidget() - arrows_container_layout = QVBoxLayout(arrows_container) - arrows_container_layout.addWidget(arrows_label) - arrows_container_layout.addWidget(self.arrows_image_label) - images_layout.addWidget(arrows_container) - - # Raw image display - raw_label = QLabel("原始图像") - raw_label.setStyleSheet(""" - font-size: 15px; - font-weight: bold; - padding: 8px; - color: #2c3e50; - font-family: 'Segoe UI', Arial, sans-serif; - background-color: #ecf0f1; - border-radius: 5px; - """) - raw_label.setAlignment(Qt.AlignCenter) - - self.raw_image_label = QLabel() - self.raw_image_label.setMinimumSize(400, 400) # Fixed initial size - self.raw_image_label.setMaximumSize(400, 400) # Fixed size - self.raw_image_label.setStyleSheet("background-color: black; border: 2px solid #4a90e2; border-radius: 5px;") - self.raw_image_label.setAlignment(Qt.AlignCenter) - self.raw_image_label.setScaledContents(True) # Allow scaling within fixed size - - raw_container = QWidget() - raw_container_layout = QVBoxLayout(raw_container) - raw_container_layout.addWidget(raw_label) - raw_container_layout.addWidget(self.raw_image_label) - images_layout.addWidget(raw_container) - - # FPS/Status label (left side, not spanning to plots) - self.status_label = QLabel("Status: Stopped") - self.status_label.setStyleSheet(""" - font-size: 13px; - font-weight: bold; - font-family: 'Segoe UI', Arial, sans-serif; - color: #e74c3c; - padding: 8px; - background-color: #fadbd8; - border-radius: 5px; - """) - self.status_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) - left_panel_layout.addWidget(self.status_label) - - # Images layout - images_widget = QWidget() - images_widget.setLayout(images_layout) - left_panel_layout.addWidget(images_widget) - - left_panel_layout.addStretch() # Push everything to top - - # Add left panel to main layout - main_layout.addWidget(left_panel) - - # Right side: Plots with controls (all in one bordered container) - plots_container = QWidget() - plots_container.setStyleSheet(""" - QWidget { - border: 2px solid #4a90e2; - border-radius: 8px; - background-color: #ffffff; - padding: 10px; - } - """) - plots_container_layout = QVBoxLayout(plots_container) - plots_container_layout.setContentsMargins(10, 10, 10, 10) - plots_container_layout.setSpacing(10) - - # Create controls container (no border, just for layout) - controls_container = QWidget() - controls_layout = QVBoxLayout(controls_container) - controls_layout.setContentsMargins(0, 0, 0, 0) - - # Title label - controls_title = QLabel("力信息") - controls_title.setStyleSheet(""" - font-size: 18px; - font-weight: bold; - padding: 8px; - color: #2c3e50; - font-family: 'Segoe UI', Arial, sans-serif; - """) - controls_title.setAlignment(Qt.AlignCenter) - controls_layout.addWidget(controls_title) - - # Scaling controls layout - scaling_layout = QHBoxLayout() - - # Dynamic scaling checkbox - self.dynamic_scaling_checkbox = QCheckBox("动态缩放") - self.dynamic_scaling_checkbox.setChecked(True) - self.dynamic_scaling_checkbox.setStyleSheet(""" - font-size: 12px; - font-family: 'Segoe UI', Arial, sans-serif; - color: #34495e; - """) - self.dynamic_scaling_checkbox.toggled.connect(self.on_dynamic_scaling_changed) - scaling_layout.addWidget(self.dynamic_scaling_checkbox) - - # Max scale label and textbox - max_scale_label = QLabel("最大刻度:") - max_scale_label.setStyleSheet(""" - font-size: 12px; - font-family: 'Segoe UI', Arial, sans-serif; - color: #34495e; - """) - scaling_layout.addWidget(max_scale_label) - - self.max_scale_textbox = QLineEdit() - self.max_scale_textbox.setText("500000.0") - self.max_scale_textbox.setEnabled(False) # Disabled when dynamic scaling is on - self.max_scale_textbox.setMaximumWidth(100) - self.max_scale_textbox.setStyleSheet(""" - font-size: 12px; - font-family: 'Segoe UI', Arial, sans-serif; - padding: 4px; - border: 1px solid #bdc3c7; - border-radius: 4px; - """) - self.max_scale_textbox.textChanged.connect(self.on_max_scale_changed) - scaling_layout.addWidget(self.max_scale_textbox) - - scaling_layout.addStretch() # Push controls to the left - controls_layout.addLayout(scaling_layout) - - plots_container_layout.addWidget(controls_container) - - # Initialize plots - self.init_plots() - plots_container_layout.addWidget(self.plot_widget) - - # Add plots container to main layout (right side) - main_layout.addWidget(plots_container) - - def on_dynamic_scaling_changed(self, checked): - """Handle dynamic scaling checkbox state change""" - self.dynamic_scaling = checked - self.max_scale_textbox.setEnabled(not self.dynamic_scaling) - - def on_max_scale_changed(self, text): - """Handle max scale textbox change""" - try: - self.max_scale = float(text) - if self.max_scale <= 0: - self.max_scale = 1.0 - except ValueError: - # Invalid input, keep previous value - pass - - def on_record_dir_changed(self, text): - """Handle record directory change""" - if text: - self.record_directory = text - else: - # Default to Windows Pictures folder if empty - pictures_folder = os.path.join(os.path.expanduser("~"), "Pictures") - self.record_directory = os.path.join(pictures_folder, "OrisysRecordings") - - def on_record_filename_changed(self, text): - """Handle record filename change""" - self.record_filename = text if text else "recording" - - def browse_directory(self): - """Open directory browser dialog""" - directory = QFileDialog.getExistingDirectory( - self, - "选择保存目录", - self.record_directory if os.path.exists(self.record_directory) else "." - ) - if directory: - self.record_dir_textbox.setText(directory) - self.record_directory = directory - - def toggle_recording(self): - """Toggle recording on/off""" - if not self.recording: - # Start recording - if not os.path.exists(self.record_directory): - os.makedirs(self.record_directory) - - # Get frame size from sensor images (original size, not scaled) - if self.sensor and hasattr(self.sensor, 'img'): - h_raw, w_raw = self.sensor.img.shape[:2] - # Arrows image has same dimensions as raw (after cropping) - h_arr = h_raw - 30 if h_raw > 30 else h_raw - w_arr = w_raw - 30 if w_raw > 30 else w_raw - - # Create video writers - fourcc = cv2.VideoWriter_fourcc(*'mp4v') - fps = 30.0 - - arrows_path = os.path.join(self.record_directory, f"{self.record_filename}_arrows.mp4") - raw_path = os.path.join(self.record_directory, f"{self.record_filename}_raw.mp4") - - self.video_writer_arrows = cv2.VideoWriter( - arrows_path, fourcc, fps, - (w_arr, h_arr) - ) - self.video_writer_raw = cv2.VideoWriter( - raw_path, fourcc, fps, - (w_arr, h_arr) - ) - - if self.video_writer_arrows.isOpened() and self.video_writer_raw.isOpened(): - self.recording = True - self.record_button.setText("停止录制") - self.record_button.setStyleSheet(""" - QPushButton { - background-color: #e74c3c; - color: white; - font-size: 13px; - font-weight: bold; - font-family: 'Segoe UI', Arial, sans-serif; - padding: 10px 25px; - border-radius: 6px; - border: none; - } - QPushButton:hover { - background-color: #c0392b; - } - QPushButton:pressed { - background-color: #a93226; - } - """) - else: - # Stop recording - self.recording = False - if self.video_writer_arrows: - self.video_writer_arrows.release() - self.video_writer_arrows = None - if self.video_writer_raw: - self.video_writer_raw.release() - self.video_writer_raw = None - - self.record_button.setText("开始录制") - self.record_button.setStyleSheet(""" - QPushButton { - background-color: #3498db; - color: white; - font-size: 13px; - font-weight: bold; - font-family: 'Segoe UI', Arial, sans-serif; - padding: 10px 25px; - border-radius: 6px; - border: none; - } - QPushButton:hover { - background-color: #2980b9; - } - QPushButton:pressed { - background-color: #21618c; - } - """) - - def init_plots(self): - """Initialize pyqtgraph plots for force visualization""" - # Configure pyqtgraph with modern color theme - pg.setConfigOption('background', '#f8f9fa') # Light gray background - pg.setConfigOption('foreground', '#2c3e50') # Dark blue-gray text - - # Create GraphicsLayoutWidget (will scale with window) - self.plot_widget = pg.GraphicsLayoutWidget() - self.plot_widget.setMinimumSize(400, 600) # Minimum size, will scale - self.plot_widget.setBackground('#f8f9fa') # Light gray background - - # Modern color palette - colors = { - 'normal': '#3498db', # Blue - 'shear_x': '#e74c3c', # Red - 'shear_y': '#2ecc71', # Green - 'magnitude': '#9b59b6' # Purple - } - - # Create plots with modern styling - p1 = self.plot_widget.addPlot(title="法相力") - p1.setLabel('bottom', '数据点', units='', color='#34495e') - p1.setLabel('left', '力', color='#34495e') - p1.showGrid(x=True, y=True, alpha=0.3) - p1.getAxis('bottom').setPen(pg.mkPen(color='#7f8c8d', width=1)) - p1.getAxis('left').setPen(pg.mkPen(color='#7f8c8d', width=1)) - p1.getAxis('bottom').setTextPen(pg.mkPen(color='#34495e')) - p1.getAxis('left').setTextPen(pg.mkPen(color='#34495e')) - self.plot_widget.nextRow() - - p2 = self.plot_widget.addPlot(title="切向力 X") - p2.setLabel('bottom', '数据点', units='', color='#34495e') - p2.setLabel('left', '力', color='#34495e') - p2.showGrid(x=True, y=True, alpha=0.3) - p2.getAxis('bottom').setPen(pg.mkPen(color='#7f8c8d', width=1)) - p2.getAxis('left').setPen(pg.mkPen(color='#7f8c8d', width=1)) - p2.getAxis('bottom').setTextPen(pg.mkPen(color='#34495e')) - p2.getAxis('left').setTextPen(pg.mkPen(color='#34495e')) - self.plot_widget.nextRow() - - p3 = self.plot_widget.addPlot(title="切向力 Y") - p3.setLabel('bottom', '数据点', units='', color='#34495e') - p3.setLabel('left', '力', color='#34495e') - p3.showGrid(x=True, y=True, alpha=0.3) - p3.getAxis('bottom').setPen(pg.mkPen(color='#7f8c8d', width=1)) - p3.getAxis('left').setPen(pg.mkPen(color='#7f8c8d', width=1)) - p3.getAxis('bottom').setTextPen(pg.mkPen(color='#34495e')) - p3.getAxis('left').setTextPen(pg.mkPen(color='#34495e')) - self.plot_widget.nextRow() - - p4 = self.plot_widget.addPlot(title="合力大小") - p4.setLabel('bottom', '数据点', units='', color='#34495e') - p4.setLabel('left', '力', color='#34495e') - p4.showGrid(x=True, y=True, alpha=0.3) - p4.getAxis('bottom').setPen(pg.mkPen(color='#7f8c8d', width=1)) - p4.getAxis('left').setPen(pg.mkPen(color='#7f8c8d', width=1)) - p4.getAxis('bottom').setTextPen(pg.mkPen(color='#34495e')) - p4.getAxis('left').setTextPen(pg.mkPen(color='#34495e')) - - # Create plot curves with modern colors and styling - self.plot_af = p1.plot(pen=pg.mkPen(color=colors['normal'], width=2)) - self.plot_tfx = p2.plot(pen=pg.mkPen(color=colors['shear_x'], width=2)) - self.plot_tfy = p3.plot(pen=pg.mkPen(color=colors['shear_y'], width=2)) - self.plot_magnitude = p4.plot(pen=pg.mkPen(color=colors['magnitude'], width=2)) - - # Store plot references - self.p1 = p1 - self.p2 = p2 - self.p3 = p3 - self.p4 = p4 - - # Initialize data array: [npoints, 2, 4] where: - # - First dimension: data points - # - Second dimension: [time, force_value] - # - Third dimension: [normal, shear_x, shear_y, magnitude] - # Use relative time (0 to npoints) so x-axis always shows 0 to npoints - self.data = np.zeros([self.npoints, 2, 4]) - self.data_count = 0 # Track how many data points we've collected - for i in range(4): - self.data[:, 0, i] = np.arange(self.npoints) # Start with 0 to npoints-1 - - def update_plots(self, fn, fx, fy, current_time): - """Update the plots with new force data""" - if self.data is None: - return - - # Calculate magnitude: sqrt(fn^2 + fx^2 + fy^2) - magnitude = np.sqrt(fn**2 + fx**2 + fy**2) - - # Update maximum magnitude for scaling - self.max_magnitude = max(self.max_magnitude, magnitude) - - # Update data count - self.data_count += 1 - - if self.data_count <= self.npoints: - # Before we have npoints, just append to the array - idx = self.data_count - 1 # 0-indexed - self.data[idx, 0, :] = idx # X-axis: 0, 1, 2, ..., data_count-1 - self.data[idx, 1, 0] = fn - self.data[idx, 1, 1] = fx - self.data[idx, 1, 2] = fy - self.data[idx, 1, 3] = magnitude - # Show only the data we have so far - show_indices = slice(0, self.data_count) - # X-axis range: 0 to data_count-1 - x_min, x_max = 0, max(0, self.data_count - 1) - else: - # After we have npoints, shift force data left (rolling window) - # Shift force values left - for i in range(4): - self.data[:-1, 1, i] = self.data[1:, 1, i] # shift force data left only - - # Set new data at the end - self.data[-1, 1, 0] = fn - self.data[-1, 1, 1] = fx - self.data[-1, 1, 2] = fy - self.data[-1, 1, 3] = magnitude - - # Update x-axis values to show sliding window of actual data point indices - # X-axis shows the actual data point numbers (data_count - npoints + 1 to data_count) - start_idx = self.data_count - self.npoints - for i in range(4): - self.data[:, 0, i] = np.arange(start_idx, start_idx + self.npoints) - - # Show all npoints - show_indices = slice(None) - # X-axis range: start_idx to start_idx + npoints - 1 - x_min, x_max = start_idx, start_idx + self.npoints - 1 - - # Set x-axis range to show the sliding window - self.p1.setXRange(x_min, x_max) - self.p2.setXRange(x_min, x_max) - self.p3.setXRange(x_min, x_max) - self.p4.setXRange(x_min, x_max) - - # Calculate scaling based on dynamic scaling setting - if self.dynamic_scaling: - # Dynamic scaling: use current maximum force sum magnitude from visible data * 1.05 - # Calculate max from current visible data (magnitude is in index 3) - if len(self.data[show_indices, 1, 3]) > 0: - current_max_magnitude = max(self.data[show_indices, 1, 3]) - if current_max_magnitude > 0: - scale_max = current_max_magnitude * 1.05 - else: - scale_max = 1.0 - else: - scale_max = 1.0 - else: - # Fixed scaling: use value from textbox - scale_max = self.max_scale - - # Update plot data with scaled Y ranges - self.plot_af.setData(self.data[show_indices, 0, 0], self.data[show_indices, 1, 0]) - y_min_0, y_max_0 = min(self.data[show_indices, 1, 0]), max(self.data[show_indices, 1, 0]) - if y_max_0 > y_min_0: - # Normal force: start from 0 (no negative part) - self.p1.setYRange(0, scale_max) - else: - self.p1.setYRange(0, max(1.0, y_max_0 + 1)) - - self.plot_tfx.setData(self.data[show_indices, 0, 1], self.data[show_indices, 1, 1]) - y_min_1, y_max_1 = min(self.data[show_indices, 1, 1]), max(self.data[show_indices, 1, 1]) - if y_max_1 > y_min_1: - self.p2.setYRange(-scale_max, scale_max) - else: - self.p2.setYRange(y_min_1 - 1, y_max_1 + 1) - - self.plot_tfy.setData(self.data[show_indices, 0, 2], self.data[show_indices, 1, 2]) - y_min_2, y_max_2 = min(self.data[show_indices, 1, 2]), max(self.data[show_indices, 1, 2]) - if y_max_2 > y_min_2: - self.p3.setYRange(-scale_max, scale_max) - else: - self.p3.setYRange(y_min_2 - 1, y_max_2 + 1) - - # Update magnitude plot - self.plot_magnitude.setData(self.data[show_indices, 0, 3], self.data[show_indices, 1, 3]) - y_min_mag, y_max_mag = min(self.data[show_indices, 1, 3]), max(self.data[show_indices, 1, 3]) - if y_max_mag > y_min_mag: - self.p4.setYRange(0, scale_max) # Magnitude is always positive - else: - self.p4.setYRange(-1, 1) - - def start_camera(self): - if not self.camera_running: - try: - # Open sensor (0 for camera, or use video file path) - sensor1 = Sensor("./s1.mp4",isstitch=True,cuda = True,verbose = True) - # self.sensor = Sensor(0, isstitch=True, cuda=True, verbose=True) - - # Initialize start time for plots - self.start_time = time.time() - self.data_count = 0 # Reset data count when starting - self.max_magnitude = 0.0 # Reset max magnitude - - # Reset plot data - self.data = np.zeros([self.npoints, 2, 4]) - for i in range(4): - self.data[:, 0, i] = np.arange(self.npoints) - - self.camera_running = True - self.timer.start(30) # ~30 FPS - - self.status_label.setText("Status: Running") - self.status_label.setStyleSheet(""" - font-size: 14px; - font-weight: bold; - font-family: 'Segoe UI', Arial, sans-serif; - color: #27ae60; - padding: 10px; - background-color: #d5f4e6; - border-radius: 5px; - """) - self.start_button.setEnabled(False) - self.stop_button.setEnabled(True) - self.reset_button.setEnabled(True) # Enable reset when camera starts - self.record_button.setEnabled(True) # Enable recording when camera starts - except Exception as e: - self.status_label.setText(f"Status: Sensor Error - {str(e)}") - self.status_label.setStyleSheet(""" - font-size: 14px; - font-weight: bold; - font-family: 'Segoe UI', Arial, sans-serif; - color: #f39c12; - padding: 10px; - background-color: #fef5e7; - border-radius: 5px; - """) - self.sensor = None - - def stop_camera(self): - if self.camera_running: - self.camera_running = False - self.timer.stop() - - if self.sensor is not None: - try: - self.sensor.disconnect() - except: - pass - self.sensor = None - - self.start_time = None - self.data_count = 0 # Reset data count - self.max_magnitude = 0.0 # Reset max magnitude - - self.status_label.setText("Status: Stopped") - self.status_label.setStyleSheet(""" - font-size: 14px; - font-weight: bold; - font-family: 'Segoe UI', Arial, sans-serif; - color: #e74c3c; - padding: 10px; - background-color: #fadbd8; - border-radius: 5px; - """) - self.start_button.setEnabled(True) - self.stop_button.setEnabled(False) - self.reset_button.setEnabled(False) # Disable reset when camera stops - self.record_button.setEnabled(False) # Disable recording when camera stops - - # Stop recording if active - if self.recording: - self.toggle_recording() - - # Clear images - self.arrows_image_label.clear() - self.arrows_image_label.setStyleSheet("background-color: black; border: 2px solid #333;") - self.raw_image_label.clear() - self.raw_image_label.setStyleSheet("background-color: black; border: 2px solid #333;") - - def reset_sensor(self): - """Reset the sensor""" - if self.sensor is not None and self.camera_running: - try: - self.sensor.reset() - # Reset plot data - self.data_count = 0 - self.max_magnitude = 0.0 - self.data = np.zeros([self.npoints, 2, 4]) - for i in range(4): - self.data[:, 0, i] = np.arange(self.npoints) - self.status_label.setText("Status: Sensor Reset") - self.status_label.setStyleSheet(""" - font-size: 13px; - font-weight: bold; - font-family: 'Segoe UI', Arial, sans-serif; - color: #f39c12; - padding: 8px; - background-color: #fef5e7; - border-radius: 5px; - """) - except Exception as e: - self.status_label.setText(f"Status: Reset Error - {str(e)}") - self.status_label.setStyleSheet(""" - font-size: 13px; - font-weight: bold; - font-family: 'Segoe UI', Arial, sans-serif; - color: #e74c3c; - padding: 8px; - background-color: #fadbd8; - border-radius: 5px; - """) - - def update_frame(self): - if self.camera_running and self.sensor is not None: - try: - # Extract and stitch compound eye image - self.sensor.get_img() - # Compute deformation and decomposition - self.sensor.compute_deformation() - - # Extract information: - # Vector fields: VRAW, VNORMAL, VSHEAR: raw, normal, tangential - # Frame rate, timestamp: FPS, TIMESTAMP - # Total deformation: FNORMAL, FSHEARX, FSHEARY: normal, tangential (X), tangential (Y) - fps, fn, fx, fy, flow, depth = self.sensor.read_info( - self.sensor.info.FPS, - self.sensor.info.FNORMAL, - self.sensor.info.FSHEARX, - self.sensor.info.FSHEARY, - self.sensor.info.VRAW, - self.sensor.info.DEPTH - ) - - # Generate arrows visualization - arrows = draw_arrows_fade(self.sensor.img, flow, depth) - - # Crop edges (15 pixels from each side) - only if image is large enough - h, w = arrows.shape[:2] - if h > 30 and w > 30: - arrows_cropped = arrows[15:-15, 15:-15, :] - else: - arrows_cropped = arrows - - # Crop raw image edges if large enough - h_raw, w_raw = self.sensor.img.shape[:2] - if h_raw > 30 and w_raw > 30: - raw_cropped = self.sensor.img[15:-15, 15:-15] - else: - raw_cropped = self.sensor.img - - # Convert arrows image BGR to RGB - arrows_rgb = cv2.cvtColor(arrows_cropped, cv2.COLOR_BGR2RGB) - h_arr, w_arr, ch_arr = arrows_rgb.shape - bytes_per_line_arr = ch_arr * w_arr - qt_image_arr = QImage(arrows_rgb.data, w_arr, h_arr, bytes_per_line_arr, QImage.Format_RGB888) - - # Scale to fixed 400x400 size (maintains aspect ratio) - scaled_pixmap_arr = QPixmap.fromImage(qt_image_arr).scaled( - 400, 400, - Qt.KeepAspectRatio, - Qt.SmoothTransformation - ) - self.arrows_image_label.setPixmap(scaled_pixmap_arr) - - # Record arrows frame if recording - if self.recording and self.video_writer_arrows: - # Convert back to BGR for video writer - arrows_bgr = cv2.cvtColor(arrows_cropped, cv2.COLOR_RGB2BGR) - self.video_writer_arrows.write(arrows_bgr) - - # Convert raw image BGR to RGB - raw_rgb = cv2.cvtColor(raw_cropped, cv2.COLOR_BGR2RGB) - h_raw_img, w_raw_img, ch_raw = raw_rgb.shape - bytes_per_line_raw = ch_raw * w_raw_img - qt_image_raw = QImage(raw_rgb.data, w_raw_img, h_raw_img, bytes_per_line_raw, QImage.Format_RGB888) - - # Scale to fixed 400x400 size (maintains aspect ratio) - scaled_pixmap_raw = QPixmap.fromImage(qt_image_raw).scaled( - 400, 400, - Qt.KeepAspectRatio, - Qt.SmoothTransformation - ) - self.raw_image_label.setPixmap(scaled_pixmap_raw) - - # Record raw frame if recording - if self.recording and self.video_writer_raw: - # Convert back to BGR for video writer - raw_bgr = cv2.cvtColor(raw_cropped, cv2.COLOR_RGB2BGR) - self.video_writer_raw.write(raw_bgr) - - # Update status with FPS info - self.status_label.setText(f"Status: Running - FPS: {fps:.1f}, FNormal: {fn:.2f}, FShearX: {fx:.2f}, FShearY: {fy:.2f}") - self.status_label.setStyleSheet(""" - font-size: 13px; - font-weight: bold; - font-family: 'Segoe UI', Arial, sans-serif; - color: #27ae60; - padding: 8px; - background-color: #d5f4e6; - border-radius: 5px; - """) - - # Update plots with force data - if self.start_time is not None: - current_time = time.time() - self.start_time - self.update_plots(fn, fx, fy, current_time) - - except Exception as e: - # Sensor read failed - self.stop_camera() - self.status_label.setText(f"Status: Sensor Error - {str(e)}") - self.status_label.setStyleSheet(""" - font-size: 14px; - font-weight: bold; - font-family: 'Segoe UI', Arial, sans-serif; - color: #f39c12; - padding: 10px; - background-color: #fef5e7; - border-radius: 5px; - """) - - def closeEvent(self, event): - self.stop_camera() - event.accept() - - -def main(): - app = QApplication(sys.argv) - window = CameraGUI() - window.show() - sys.exit(app.exec_()) - - -if __name__ == '__main__': - main() diff --git a/demo.py b/demo.py deleted file mode 100644 index 02a1d7a..0000000 --- a/demo.py +++ /dev/null @@ -1,183 +0,0 @@ -""" -示例 1: 基础用法 - 单传感器同步处理 - -本示例演示 orisys 的基本使用流程: -1. 初始化传感器(视频文件或摄像头) -2. 循环获取图像并计算形变和触碰点 -3. 读取各类传感器数据 -4. 可视化显示 - - -""" -import os -os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0" -import cv2 -import orisys -import numpy as np -from scipy.interpolate import griddata - -def draw_arrows(image, optical_flow,fps=True, threshold=5, grid_spacing=40, arrow_scale=1.0): - """ - Draw colored optical flow arrows on sensor image based on flow magnitude. - - Args: - threshold: Minimum flow magnitude to display arrow (default: 10.0) - grid_spacing: Spacing between arrows in pixels (default: 20) - arrow_scale: Scale factor for arrow length (default: 3.0) - - Returns: - np.ndarray: 400x400 BGR image with colored arrows (JET colormap) - - """ - # Create working copies to avoid modifying originals - - # Convert grayscale to BGR to enable colored arrow drawing - if len(image.shape) == 2 or image.shape[2] == 1: - image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) - - # Calculate scaling factors between image and flow dimensions - img_height, img_width = image.shape[:2] - flow_height, flow_width = optical_flow.shape[:2] - scale_x, scale_y = img_width / flow_width, img_height / flow_height - - # Create sampling grid for flow field (used for interpolation source points) - sample_step = 19 # Spacing between sample points - flow_start = np.stack(np.meshgrid(range(0, flow_width, sample_step), range(0, flow_height, sample_step)), 2) - flow_end = (optical_flow[flow_start[:, :, 1], flow_start[:, :, 0], :] * 2.0 + flow_start).astype(np.int32) - - # Create regular grid for arrow placement in image coordinates - grid_x, grid_y = np.mgrid[grid_spacing//2:flow_height:grid_spacing, grid_spacing//2:flow_width:grid_spacing] - x_axis = (grid_y.reshape(-1) * scale_x).astype(np.int64) # Scale to image width - y_axis = (grid_x.reshape(-1) * scale_y).astype(np.int64) # Scale to image height - - # Interpolate flow vectors from sample grid to regular arrow grid using cubic interpolation - grid_vector = griddata( - flow_start.reshape(-1, 2), - (flow_end - flow_start).reshape(-1, 2), # Flow from start to end (original to new position) - (grid_y, grid_x), method='cubic', fill_value=0 - ) - grid_vector[np.isnan(grid_vector)] = 0 # Replace NaN with zero for stability - grid_flat_scaled = grid_vector.reshape(-1, 2) * np.array([scale_x, scale_y]) - - # Compute arrow endpoints and ensure they stay within image bounds - end_x = (grid_flat_scaled[:, 0] / arrow_scale + x_axis).astype(np.int64) - end_y = (grid_flat_scaled[:, 1] / arrow_scale + y_axis).astype(np.int64) - valid_mask = (y_axis >= 0) & (y_axis < img_height) & (x_axis >= 0) & (x_axis < img_width) - valid_mask &= (end_x >= 0) & (end_x < img_width) & (end_y >= 0) & (end_y < img_height) - - # Filter arrows by magnitude threshold and compute colors using JET colormap - magnitudes = np.linalg.norm(grid_flat_scaled, axis=1) - # Only show arrows where image is non-zero - # Check if image pixels at arrow positions are non-zero (sum across channels > 0) - # Ensure indices are within bounds before indexing - safe_y = np.clip(y_axis, 0, img_height - 1) - safe_x = np.clip(x_axis, 0, img_width - 1) - # Get pixel values at arrow positions and check if non-zero - pixel_values = image[safe_y, safe_x] - # When indexing 3D image with 1D arrays, result is 2D (N, channels) - # When indexing 2D image with 1D arrays, result is 1D (N,) - if pixel_values.ndim == 2: # BGR image (shape: N x 3) - pixel_sums = np.sum(pixel_values, axis=1) - else: # Grayscale image (shape: N,) - pixel_sums = pixel_values - valid_mask &= (pixel_sums > 0) - valid_indices = np.where(valid_mask)[0] - mag_range = magnitudes.max() - magnitudes.min() - # Normalize magnitudes to 0-255 range for colormap application, but set below-threshold values to 0 (blue) - norm_magnitudes = ((magnitudes - magnitudes.min()) / mag_range * 255.0).astype(np.uint8) if mag_range > 0 else np.zeros_like(magnitudes, dtype=np.uint8) - norm_magnitudes[magnitudes < threshold] = 0 # Keep lowest color (blue) for below-threshold magnitudes - colors_array = cv2.applyColorMap(norm_magnitudes.reshape(-1, 1), cv2.COLORMAP_BONE) # JET: blue (low) -> red (high) - - # Draw arrows at each valid grid point with magnitude-based coloring - for i in valid_indices: - color = tuple(map(int, colors_array[i, 0])) - # Scale arrow length and thickness based on threshold - if magnitudes[i] < threshold: - # Below threshold: draw as small dot (zero-length arrow with larger tip) - cv2.circle(image, (x_axis[i], y_axis[i]), radius=1, color=color, thickness=-1) - else: - # Above threshold: draw full arrow with thickness proportional to magnitude - # Arrow rooted at original position (x_axis, y_axis), head points to new position (end_x, end_y) - thickness = max(1, int(2 * magnitudes[i] / magnitudes.max())) if magnitudes.max() > 0 else 2 - cv2.arrowedLine(image, (x_axis[i], y_axis[i]), (end_x[i], end_y[i]), color, thickness=thickness, tipLength=0.3) - return image - -def main(): - - # 步骤1:创建传感器对象 (必要步骤),config_name为配置文件名(内置配置文件名)或路径(自定义配置文件路径) - - # 方式1: 从视频文件读取 - # sensor = orisys.Sensor("./s1.mp4", isstitch=True, cuda=True, verbose=True,config_name="prototype") - - # 方式2: 从摄像头读取(取消注释使用) - sensor = orisys.Sensor(0, cuda=True, verbose=True, config_name="./config/demo.json") - sensor.export_config(path="./config/exported_config.json") # 导出配置文件供查看和修改 - - print("\n按 'q' 退出程序,按 'r' 重置光流追踪器") - - - while True: - # 步骤2:信息提取 - # 步骤 2.1: 获取并拼接图像 (必要步骤) - sensor.get_img() - - # 步骤 2.2: 计算形变场 (必要步骤) - sensor.compute_deformation() - # 步骤 2.3: 计算触碰点 - sensor.compute_contact() - - #步骤 2.4: 读取信息 - fps, flow, vnormal, img, contour, centroid, depth_map = sensor.read_info( - sensor.info.FPS, - sensor.info.VRAW, - sensor.info.VNORMAL, - sensor.info.IMG, - sensor.info.CONTOUR, - sensor.info.CENTROID, - sensor.info.DEPTH - ) - - #步骤 3: 可视化显示 - # 步骤 3.3: 绘制深度图 - # 归一化散度值到0-255范围(使用绝对值,然后归一化) - div_abs = depth_map - if div_abs.max() > div_abs.min(): - div_normalized = ((div_abs - div_abs.min()) / (div_abs.max() - div_abs.min()) * 255).astype(np.uint8) - else: - div_normalized = np.zeros_like(div_abs, dtype=np.uint8) - - # 应用颜色映射(使用JET:蓝色=低散度,红色=高散度) - img_depth = cv2.applyColorMap(div_normalized, cv2.COLORMAP_HOT) - - arrows = draw_arrows( - img_depth, - flow, - threshold=1, - grid_spacing=10, - arrow_scale=0.5 - ) - cv2.imshow("形变场 Deformation", arrows) - - - - #步骤 3.4: 打印资料 - print(f"fps: {fps}") - - #步骤 3.5: 键盘控制 - key = cv2.waitKey(1) & 0xFF - if key == ord("q"): - print("\n退出程序...") - break - if key == ord("r"): - # 重置光流追踪器(当追踪漂移时使用) - sensor.reset() - print("光流追踪器已重置") - - # ========================================================================= - # 4. 清理资源 - # ========================================================================= - sensor.disconnect() # 释放视频源和相关资源 - - -if __name__ == '__main__': - main() diff --git a/src/orisys/Stitcher.py b/src/orisys/Stitcher.py index e506a1e..b63434f 100644 --- a/src/orisys/Stitcher.py +++ b/src/orisys/Stitcher.py @@ -29,6 +29,8 @@ class Stitcher: self.bound=self.nominal_ov_pos - self.margin self.row_tol = config_stitcher["row_tol"] self.box_size = config_stitcher["box_size"] + + # stich related (BF-matching) self._blend_coef_cache = {} self._feature_stitch_cache = { "ref_idx": None, @@ -77,58 +79,58 @@ class Stitcher: if feature_stitched is not None: return feature_stitched - tile_list = self.tile_list - stitch_calibration = self.stitch_calibration - ncol = self.ncol - nrow = self.nrow + # tile_list = self.tile_list + # stitch_calibration = self.stitch_calibration + # ncol = self.ncol + # nrow = self.nrow - img_stitched_height = None + # img_stitched_height = None - for i in range(nrow): - # Start with first tile of the row - img_stitched_width = tile_list[i * ncol] + # for i in range(nrow): + # # Start with first tile of the row + # img_stitched_width = tile_list[i * ncol] - # Stitch horizontally within the row - for j in range(1, ncol): - img_1 = img_stitched_width - img_2 = tile_list[i * ncol + j] + # # Stitch horizontally within the row + # for j in range(1, ncol): + # img_1 = img_stitched_width + # img_2 = tile_list[i * ncol + j] - ov_pos = int(stitch_calibration[i * ncol + j - 1][1]) - ov_pos, ov1, ov2 = self.__find_ov(img_1, img_2, ov_pos, dim=1) + # ov_pos = int(stitch_calibration[i * ncol + j - 1][1]) + # ov_pos, ov1, ov2 = self.__find_ov(img_1, img_2, ov_pos, dim=1) - img_stitched_width = self.__stitch_optimized( - img_1, img_2, ov_pos, ov1, ov2, dim=1, blend=blend - ) + # img_stitched_width = self.__stitch_optimized( + # img_1, img_2, ov_pos, ov1, ov2, dim=1, blend=blend + # ) - # Stitch rows vertically - if i == 0: - img_stitched_height = img_stitched_width - else: - width_h = img_stitched_height.shape[1] - width_w = img_stitched_width.shape[1] - wdiff = width_h - width_w + # # Stitch rows vertically + # if i == 0: + # img_stitched_height = img_stitched_width + # else: + # width_h = img_stitched_height.shape[1] + # width_w = img_stitched_width.shape[1] + # wdiff = width_h - width_w - if wdiff != 0: - if wdiff > 0: - padding = ( - ((0, 0), (0, wdiff), (0, 0)) - if len(img_stitched_width.shape) == 3 - else ((0, 0), (0, wdiff)) - ) - img_stitched_width = np.pad(img_stitched_width, padding, mode='edge') - else: - img_stitched_width = img_stitched_width[:, :width_h] + # if wdiff != 0: + # if wdiff > 0: + # padding = ( + # ((0, 0), (0, wdiff), (0, 0)) + # if len(img_stitched_width.shape) == 3 + # else ((0, 0), (0, wdiff)) + # ) + # img_stitched_width = np.pad(img_stitched_width, padding, mode='edge') + # else: + # img_stitched_width = img_stitched_width[:, :width_h] - ov_pos_h = int(stitch_calibration[(i + 1) * nrow - 1][1]) - ov_pos_h, ov_h1, ov_h2 = self.__find_ov( - img_stitched_height, img_stitched_width, ov_pos_h, dim=0 - ) + # ov_pos_h = int(stitch_calibration[(i + 1) * nrow - 1][1]) + # ov_pos_h, ov_h1, ov_h2 = self.__find_ov( + # img_stitched_height, img_stitched_width, ov_pos_h, dim=0 + # ) - img_stitched_height = self.__stitch_optimized( - img_stitched_height, img_stitched_width, ov_pos_h, ov_h1, ov_h2, dim=0, blend=blend - ) + # img_stitched_height = self.__stitch_optimized( + # img_stitched_height, img_stitched_width, ov_pos_h, ov_h1, ov_h2, dim=0, blend=blend + # ) - return img_stitched_height + # return img_stitched_height def stitch_feature_optimized(self, blend=True, match_brightness=False, pad=5, print_every=30): """ @@ -140,10 +142,10 @@ class Stitcher: if plan is None: return None - ref_idx, t_ref, moving_indices, moving_tiles = plan + ref_idx, t_ref, moving_indices, moving_tiles, selected_positions = plan self._feature_stitch_frame_idx += 1 frame_idx = self._feature_stitch_frame_idx - tile_shapes = tuple(tile.shape[:2] for tile in self.tile_list[: self.nrow * self.ncol]) + tile_shapes = tuple(self.tile_list[pos].shape[:2] for pos in selected_positions) cache = self._feature_stitch_cache cache_changed = ( cache["ref_idx"] != ref_idx @@ -157,6 +159,11 @@ class Stitcher: cache["tile_shapes"] = tile_shapes cache["transforms"] = None cache["canvas"] = {} + if self.verbose: + print( + "Feature stitch tile selection: " + f"ref_idx={ref_idx}, moving_indices={list(moving_indices)}" + ) H_use_list = cache["transforms"] if H_use_list is None: @@ -187,46 +194,58 @@ class Stitcher: blend=blend, ) - def __find_ov(self, img1, img2, ov_pos, dim=1): + # def __find_ov(self, img1, img2, ov_pos, dim=1): - _dim = len(img1.shape) + # _dim = len(img1.shape) - if dim == 1: - if _dim == 3: - # RGB case - ov_1 = img1[:, -ov_pos:, :] - ov_2 = img2[:, :ov_pos, :] - else: - # gray scale - ov_1 = img1[:, -ov_pos:] - ov_2 = img2[:, :ov_pos] - elif dim == 0: - if _dim == 3: - ov_1 = img1[-ov_pos:, :, :] - ov_2 = img2[:ov_pos, :, :] - else: - # gray scale - ov_1 = img1[-ov_pos:, :] - ov_2 = img2[:ov_pos, :] + # if dim == 1: + # if _dim == 3: + # # RGB case + # ov_1 = img1[:, -ov_pos:, :] + # ov_2 = img2[:, :ov_pos, :] + # else: + # # gray scale + # ov_1 = img1[:, -ov_pos:] + # ov_2 = img2[:, :ov_pos] + # elif dim == 0: + # if _dim == 3: + # ov_1 = img1[-ov_pos:, :, :] + # ov_2 = img2[:ov_pos, :, :] + # else: + # # gray scale + # ov_1 = img1[-ov_pos:, :] + # ov_2 = img2[:ov_pos, :] - return ov_pos, ov_1, ov_2 + # return ov_pos, ov_1, ov_2 def _get_feature_stitch_plan(self): if not self.isstitch: return None - total_tiles = self.nrow * self.ncol - if total_tiles <= 1 or len(self.tile_list) < total_tiles: + if isinstance(self.index, int): + configured_indices = (self.index,) + elif isinstance(self.index, np.ndarray): + configured_indices = tuple(int(i) for i in self.index.tolist()) + else: + configured_indices = tuple(int(i) for i in self.index) + + total_tiles = len(configured_indices) + expected_tiles = self.nrow * self.ncol + if total_tiles <= 1 or total_tiles != expected_tiles or len(self.tile_list) < total_tiles: return None - # Use the center tile as the reference frame for a stable translation-only canvas. - ref_idx = (self.nrow // 2) * self.ncol + (self.ncol // 2) - if ref_idx >= total_tiles: + # Use the configured grid order from self.index and keep the same reference tile + # location as the original stitch setup. + ref_pos = (self.nrow // 2) * self.ncol + (self.ncol // 2) + if ref_pos >= total_tiles: return None - moving_indices = tuple(i for i in range(total_tiles) if i != ref_idx) - moving_tiles = [self.tile_list[i] for i in moving_indices] - return ref_idx, self.tile_list[ref_idx], moving_indices, moving_tiles + selected_positions = tuple(range(total_tiles)) + ref_idx = configured_indices[ref_pos] + moving_positions = tuple(pos for pos in selected_positions if pos != ref_pos) + moving_indices = tuple(configured_indices[pos] for pos in moving_positions) + moving_tiles = [self.tile_list[pos] for pos in moving_positions] + return ref_idx, self.tile_list[ref_pos], moving_indices, moving_tiles, selected_positions def _tile_to_bgr(self, img): if img.ndim == 2: @@ -701,173 +720,173 @@ class Stitcher: return tile_list - def stitch_cal(self): + # def stitch_cal(self): - # init - tile_list = self.tile_list - img_stitched_height = None - stitch_calibration = [] - # Flip col and row for porting - nrow, ncol = self.ncol, self.nrow - if not self.isstitch: - return self.tile_list[0], None + # # init + # tile_list = self.tile_list + # img_stitched_height = None + # stitch_calibration = [] + # # Flip col and row for porting + # nrow, ncol = self.ncol, self.nrow + # if not self.isstitch: + # return self.tile_list[0], None - for i in range(ncol): - img_stitched_width = None - for j in range(nrow): - if j == 0: - img_j0 = tile_list[i*ncol] - img_stitched_width = img_j0 - else: - img_1 = tile_list[i*ncol + j-1] - img_2 = tile_list[i*ncol + j] + # for i in range(ncol): + # img_stitched_width = None + # for j in range(nrow): + # if j == 0: + # img_j0 = tile_list[i*ncol] + # img_stitched_width = img_j0 + # else: + # img_1 = tile_list[i*ncol + j-1] + # img_2 = tile_list[i*ncol + j] - ov_pos, ov1, ov2, c_corr = self.__get_ov(img_1.astype(np.float32), img_2.astype(np.float32), dim=1) + # ov_pos, ov1, ov2, c_corr = self.__get_ov(img_1.astype(np.float32), img_2.astype(np.float32), dim=1) - stitch_calibration.append(["hor {}".format(j-1),ov_pos]) + # stitch_calibration.append(["hor {}".format(j-1),ov_pos]) - # stitch images with stacking - img_stitched_width = self.__stitch_optimized(img_stitched_width, img_2, ov_pos, ov1, ov2, blend=True, dim=1) + # # stitch images with stacking + # img_stitched_width = self.__stitch_optimized(img_stitched_width, img_2, ov_pos, ov1, ov2, blend=True, dim=1) - if i == 0: - img_stitched_height = img_stitched_width - stitch_calibration.append(["ver {}".format(i),0]) - pass - else: - width = img_stitched_height.shape[1] - width_new = img_stitched_width.shape[1] - wdiff = width - width_new - if wdiff > 0: - img_stitched_width = np.hstack((img_stitched_width, img_stitched_width[:, -wdiff:])) - elif wdiff < 0: - img_stitched_width = img_stitched_width[:, :wdiff] + # if i == 0: + # img_stitched_height = img_stitched_width + # stitch_calibration.append(["ver {}".format(i),0]) + # pass + # else: + # width = img_stitched_height.shape[1] + # width_new = img_stitched_width.shape[1] + # wdiff = width - width_new + # if wdiff > 0: + # img_stitched_width = np.hstack((img_stitched_width, img_stitched_width[:, -wdiff:])) + # elif wdiff < 0: + # img_stitched_width = img_stitched_width[:, :wdiff] - ov_pos_h, ov_h1, ov_h2, c_corr = self.__get_ov(img_stitched_height, img_stitched_width, dim=0) + # ov_pos_h, ov_h1, ov_h2, c_corr = self.__get_ov(img_stitched_height, img_stitched_width, dim=0) - stitch_calibration.append(["ver {}".format(i),ov_pos_h]) + # stitch_calibration.append(["ver {}".format(i),ov_pos_h]) - img_stitched_height = self.__stitch_optimized(img_stitched_height, img_stitched_width, ov_pos_h, ov_h1, ov_h2, blend=True, dim=0) + # img_stitched_height = self.__stitch_optimized(img_stitched_height, img_stitched_width, ov_pos_h, ov_h1, ov_h2, blend=True, dim=0) - self.stitch_calibration = stitch_calibration + # self.stitch_calibration = stitch_calibration - return img_stitched_height, stitch_calibration + # return img_stitched_height, stitch_calibration - def __stitch_optimized(self, img1, img2, ov_pos, ov1, ov2, blend=True, dim=1): - _dim = len(img1.shape) + # def __stitch_optimized(self, img1, img2, ov_pos, ov1, ov2, blend=True, dim=1): + # _dim = len(img1.shape) - # Clamp overlap for safety - if dim == 1: - ov_pos = int(max(0, min(ov_pos, img1.shape[1], img2.shape[1]))) - else: - ov_pos = int(max(0, min(ov_pos, img1.shape[0], img2.shape[0]))) + # # Clamp overlap for safety + # if dim == 1: + # ov_pos = int(max(0, min(ov_pos, img1.shape[1], img2.shape[1]))) + # else: + # ov_pos = int(max(0, min(ov_pos, img1.shape[0], img2.shape[0]))) - # Fast blend path with cached coefficient tensors - if not blend or ov_pos <= 1: - ov = ov1 - else: - blend_coef = self._get_blend_coef(ov_pos, dim, _dim) - ov = ( - ov1.astype(np.float32, copy=False) * blend_coef - + ov2.astype(np.float32, copy=False) * (1.0 - blend_coef) - ).astype(np.uint8) + # # Fast blend path with cached coefficient tensors + # if not blend or ov_pos <= 1: + # ov = ov1 + # else: + # blend_coef = self._get_blend_coef(ov_pos, dim, _dim) + # ov = ( + # ov1.astype(np.float32, copy=False) * blend_coef + # + ov2.astype(np.float32, copy=False) * (1.0 - blend_coef) + # ).astype(np.uint8) - # Build stitched output with one target allocation and slice writes - if dim == 1: - left_w = img1.shape[1] - ov_pos - right_w = img2.shape[1] - ov_pos - out_w = left_w + ov_pos + right_w - if _dim == 3: - stitched_img = np.empty((img1.shape[0], out_w, img1.shape[2]), dtype=img1.dtype) - if left_w > 0: - stitched_img[:, :left_w, :] = img1[:, :left_w, :] - if ov_pos > 0: - stitched_img[:, left_w:left_w + ov_pos, :] = ov - if right_w > 0: - stitched_img[:, left_w + ov_pos:, :] = img2[:, ov_pos:, :] - else: - stitched_img = np.empty((img1.shape[0], out_w), dtype=img1.dtype) - if left_w > 0: - stitched_img[:, :left_w] = img1[:, :left_w] - if ov_pos > 0: - stitched_img[:, left_w:left_w + ov_pos] = ov - if right_w > 0: - stitched_img[:, left_w + ov_pos:] = img2[:, ov_pos:] - else: - top_h = img1.shape[0] - ov_pos - bottom_h = img2.shape[0] - ov_pos - out_h = top_h + ov_pos + bottom_h - if _dim == 3: - stitched_img = np.empty((out_h, img1.shape[1], img1.shape[2]), dtype=img1.dtype) - if top_h > 0: - stitched_img[:top_h, :, :] = img1[:top_h, :, :] - if ov_pos > 0: - stitched_img[top_h:top_h + ov_pos, :, :] = ov - if bottom_h > 0: - stitched_img[top_h + ov_pos:, :, :] = img2[ov_pos:, :, :] - else: - stitched_img = np.empty((out_h, img1.shape[1]), dtype=img1.dtype) - if top_h > 0: - stitched_img[:top_h, :] = img1[:top_h, :] - if ov_pos > 0: - stitched_img[top_h:top_h + ov_pos, :] = ov - if bottom_h > 0: - stitched_img[top_h + ov_pos:, :] = img2[ov_pos:, :] + # # Build stitched output with one target allocation and slice writes + # if dim == 1: + # left_w = img1.shape[1] - ov_pos + # right_w = img2.shape[1] - ov_pos + # out_w = left_w + ov_pos + right_w + # if _dim == 3: + # stitched_img = np.empty((img1.shape[0], out_w, img1.shape[2]), dtype=img1.dtype) + # if left_w > 0: + # stitched_img[:, :left_w, :] = img1[:, :left_w, :] + # if ov_pos > 0: + # stitched_img[:, left_w:left_w + ov_pos, :] = ov + # if right_w > 0: + # stitched_img[:, left_w + ov_pos:, :] = img2[:, ov_pos:, :] + # else: + # stitched_img = np.empty((img1.shape[0], out_w), dtype=img1.dtype) + # if left_w > 0: + # stitched_img[:, :left_w] = img1[:, :left_w] + # if ov_pos > 0: + # stitched_img[:, left_w:left_w + ov_pos] = ov + # if right_w > 0: + # stitched_img[:, left_w + ov_pos:] = img2[:, ov_pos:] + # else: + # top_h = img1.shape[0] - ov_pos + # bottom_h = img2.shape[0] - ov_pos + # out_h = top_h + ov_pos + bottom_h + # if _dim == 3: + # stitched_img = np.empty((out_h, img1.shape[1], img1.shape[2]), dtype=img1.dtype) + # if top_h > 0: + # stitched_img[:top_h, :, :] = img1[:top_h, :, :] + # if ov_pos > 0: + # stitched_img[top_h:top_h + ov_pos, :, :] = ov + # if bottom_h > 0: + # stitched_img[top_h + ov_pos:, :, :] = img2[ov_pos:, :, :] + # else: + # stitched_img = np.empty((out_h, img1.shape[1]), dtype=img1.dtype) + # if top_h > 0: + # stitched_img[:top_h, :] = img1[:top_h, :] + # if ov_pos > 0: + # stitched_img[top_h:top_h + ov_pos, :] = ov + # if bottom_h > 0: + # stitched_img[top_h + ov_pos:, :] = img2[ov_pos:, :] - return stitched_img + # return stitched_img - def __get_ov(self, img1, img2, dim=1): - ''' - default dimension is along axis 1 (width of the images) - ''' - # pass parameters - nominal_ov = self.nominal_ov_pos - bound_ov = self.bound + # def __get_ov(self, img1, img2, dim=1): + # ''' + # default dimension is along axis 1 (width of the images) + # ''' + # # pass parameters + # nominal_ov = self.nominal_ov_pos + # bound_ov = self.bound - cross_corr = [] - h1, w1 = img1.shape[:2] - h2, w2 = img2.shape[:2] - # clamp tested overlap to valid range (1..min dimension) - max_possible = w1 if dim == 1 else h1 - max_possible = min(max_possible, w2 if dim == 1 else h2) - lo = max(1, nominal_ov - bound_ov) - hi = min(max_possible, nominal_ov + bound_ov) - ov_space = range(lo, hi + 1) + # cross_corr = [] + # h1, w1 = img1.shape[:2] + # h2, w2 = img2.shape[:2] + # # clamp tested overlap to valid range (1..min dimension) + # max_possible = w1 if dim == 1 else h1 + # max_possible = min(max_possible, w2 if dim == 1 else h2) + # lo = max(1, nominal_ov - bound_ov) + # hi = min(max_possible, nominal_ov + bound_ov) + # ov_space = range(lo, hi + 1) - for ov in ov_space: - if dim == 1: - ov_area1 = img1[:, max(0, w1 - ov):, :] - ov_area2 = img2[:, :min(ov, w2), :] - elif dim == 0: - ov_area1 = img1[max(0, h1 - ov):, :, :] - ov_area2 = img2[:min(ov, h2), :, :] + # for ov in ov_space: + # if dim == 1: + # ov_area1 = img1[:, max(0, w1 - ov):, :] + # ov_area2 = img2[:, :min(ov, w2), :] + # elif dim == 0: + # ov_area1 = img1[max(0, h1 - ov):, :, :] + # ov_area2 = img2[:min(ov, h2), :, :] - # require exact shape match (detect_nonblack_regions guarantees uniform tile size) - if ov_area1.size == 0 or ov_area2.size == 0 or ov_area1.shape != ov_area2.shape: - cross_corr.append(-1.0) - continue + # # require exact shape match (detect_nonblack_regions guarantees uniform tile size) + # if ov_area1.size == 0 or ov_area2.size == 0 or ov_area1.shape != ov_area2.shape: + # cross_corr.append(-1.0) + # continue - # multi-channel cross correlation (average over channels) - corr = 0.0 - for i in range(min(3, ov_area1.shape[2])): - a = ov_area1[:, :, i].astype(np.float32) - b = ov_area2[:, :, i].astype(np.float32) - denom = np.sqrt(np.sum(a * a) * np.sum(b * b)) + 1e-12 - corr += float(np.sum(a * b) / denom) - corr = corr / float(min(3, ov_area1.shape[2])) - cross_corr.append(corr) + # # multi-channel cross correlation (average over channels) + # corr = 0.0 + # for i in range(min(3, ov_area1.shape[2])): + # a = ov_area1[:, :, i].astype(np.float32) + # b = ov_area2[:, :, i].astype(np.float32) + # denom = np.sqrt(np.sum(a * a) * np.sum(b * b)) + 1e-12 + # corr += float(np.sum(a * b) / denom) + # corr = corr / float(min(3, ov_area1.shape[2])) + # cross_corr.append(corr) - # pick best overlap (ignore invalid entries) - best_idx = int(np.argmax(cross_corr)) - ov_pos = list(ov_space)[best_idx] + # # pick best overlap (ignore invalid entries) + # best_idx = int(np.argmax(cross_corr)) + # ov_pos = list(ov_space)[best_idx] - # extract the overlap regions (assume shapes match) - if dim == 1: - ov_1 = img1[:, max(0, w1 - ov_pos):, :].copy() - ov_2 = img2[:, :min(ov_pos, w2), :].copy() - else: - ov_1 = img1[max(0, h1 - ov_pos):, :, :].copy() - ov_2 = img2[:min(ov_pos, h2), :, :].copy() - return ov_pos, ov_1, ov_2, cross_corr + # # extract the overlap regions (assume shapes match) + # if dim == 1: + # ov_1 = img1[:, max(0, w1 - ov_pos):, :].copy() + # ov_2 = img2[:, :min(ov_pos, w2), :].copy() + # else: + # ov_1 = img1[max(0, h1 - ov_pos):, :, :].copy() + # ov_2 = img2[:min(ov_pos, h2), :, :].copy() + # return ov_pos, ov_1, ov_2, cross_corr # Helper functions for stitching diff --git a/src/orisys/sensor.py b/src/orisys/sensor.py index 4453098..8f415f6 100644 --- a/src/orisys/sensor.py +++ b/src/orisys/sensor.py @@ -108,7 +108,7 @@ class Sensor: self.compute_contact_valid = False # Image Data Variables (public) - self.stitch_calibration = [] + # self.stitch_calibration = [] self.frame = self.img = None self.initial_img = None shape = (self.__config_cam["resolution_stitch"][1], self.__config_cam["resolution_stitch"][0]) @@ -402,7 +402,9 @@ class Sensor: def reset(self): """Reset optical flow tracker.""" self.__tracker.reset() - self.initial_img = self.img.copy() + if self.img is not None: + self.initial_img = self.img.copy() + def disconnect(self): """Release resources and stop async threads.""" @@ -438,31 +440,31 @@ class Sensor: - def stitch_cal(self): - """ - Load or create stitch calibration. + # def stitch_cal(self): + # """ + # Load or create stitch calibration. - Returns: - np.ndarray: Stitched calibration image - """ - if self.cal_path is None: - self.cal_path = "./config/cal.npy" + # Returns: + # np.ndarray: Stitched calibration image + # """ + # if self.cal_path is None: + # self.cal_path = "./config/cal.npy" - if os.path.exists(self.cal_path): - if self.verbose: - print("Loading calibration from", self.cal_path) - self.stitch_calibration = np.load(self.cal_path, allow_pickle=True) - self.stitcher.stitch_calibration = self.stitch_calibration - return self.stitcher.stitch_rt_optimized() - else: - if self.verbose: - print("Creating new calibration...") - img, self.stitch_calibration = self.stitcher.stitch_cal() - os.makedirs("./config", exist_ok=True) - np.save(self.cal_path, self.stitch_calibration) - if self.verbose: - print("Calibration saved to", self.cal_path) - return img + # if os.path.exists(self.cal_path): + # if self.verbose: + # print("Loading calibration from", self.cal_path) + # self.stitch_calibration = np.load(self.cal_path, allow_pickle=True) + # self.stitcher.stitch_calibration = self.stitch_calibration + # return self.stitcher.stitch_rt_optimized() + # else: + # if self.verbose: + # print("Creating new calibration...") + # img, self.stitch_calibration = self.stitcher.stitch_cal() + # os.makedirs("./config", exist_ok=True) + # np.save(self.cal_path, self.stitch_calibration) + # if self.verbose: + # print("Calibration saved to", self.cal_path) + # return img # ========================================================================= # Private - Initialization Helpers @@ -575,7 +577,11 @@ class Sensor: else: self._find_stitch_boxes(frame) self._extract_tiles(frame) - img = self.stitch_cal() + # Feature-based stitching no longer needs eager overlap calibration here. + # Keep stitch_cal() as a fallback so older overlap-based stitching still works. + img = self.stitcher.stitch_rt_optimized() + # if img is None: + # img = self.stitch_cal() img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) self.resolution_stitch = img.shape self.initial_img = cv2.resize(img, tuple(self._config_resolution_stitch), interpolation=cv2.INTER_CUBIC) diff --git a/src/orisys/tracker.py b/src/orisys/tracker.py index 1df704a..82475cb 100644 --- a/src/orisys/tracker.py +++ b/src/orisys/tracker.py @@ -60,6 +60,10 @@ class Tracker: # Aggressive optimization for CPU self.__tracking_inst.setFinestScale(config_tracker["dis_mode"]) # Skip finest pyramid level (2x faster) + # self.__tracking_inst.setUseSpatialPropagation(False) + # self.__tracking_inst.setUseMeanNormalization(False) + # self.__tracking_inst.setPatchSize(4) # 8 + # self.__tracking_inst.setPatchStride(2) # 3 # else: # # CUDA flow method with stream support