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