diff --git a/.gitignore b/.gitignore index fba7829..b3e3cc5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ /dist_obfuscated/ /release/ __pycache__/ -orisys.egg-info/ \ No newline at end of file +orisys.egg-info/ +/.cursor/ \ No newline at end of file diff --git a/BUILD_INSTRUCTIONS.md b/BUILD_INSTRUCTIONS.md new file mode 100644 index 0000000..287e1f8 --- /dev/null +++ b/BUILD_INSTRUCTIONS.md @@ -0,0 +1,119 @@ +# Building Orisys Sensor Demo Executable and Installer + +This guide explains how to package `camera_gui.py` into a standalone executable (.exe) and create an installer. + +## Prerequisites + +1. **Python 3.8+** installed +2. **All dependencies** installed: + ```bash + pip install -r requirements.txt + pip install PyQt5 pyqtgraph PyInstaller + ``` + +3. **For creating installer (.exe installer)**: + - Download and install [Inno Setup](https://jrsoftware.org/isdl.php) (free, open-source) + +## Method 1: Using PyInstaller (Simple) + +### Option A: Using the spec file (Recommended) + +1. Build the executable: + ```bash + pyinstaller camera_gui.spec + ``` + +2. The executable will be in the `dist` folder: `dist\camera_gui.exe` + +### Option B: Using the build script + +1. Run the build script: + ```bash + python build_exe.py + ``` + +2. The executable will be in the `dist` folder: `dist\camera_gui.exe` + +### Option C: Manual PyInstaller command + +```bash +pyinstaller --name=camera_gui --onefile --windowed --icon=orisys_logo_notext.png --add-data="orisys_logo_notext.png;." camera_gui.py +``` + +## Method 2: Creating an Installer (.exe installer) + +### Using Inno Setup + +1. **Build the executable first** using Method 1 above + +2. **Open Inno Setup Compiler** + +3. **Open the script file**: `build_installer.iss` + +4. **Compile the installer**: + - Click "Build" → "Compile" + - Or press F9 + +5. **The installer will be created** in the `installer` folder: `installer\OrisysSensorDemo_Setup.exe` + +### Customizing the Installer + +Edit `build_installer.iss` to customize: +- App name and version +- Installation directory +- Start menu shortcuts +- Desktop icons +- License file +- Additional files to include + +## Troubleshooting + +### Common Issues + +1. **"Module not found" errors**: + - Add missing modules to `hiddenimports` in `camera_gui.spec` + - Or use `--hidden-import=module_name` in PyInstaller command + +2. **Large executable size**: + - This is normal - PyInstaller bundles Python interpreter and all dependencies + - Consider using `--onedir` instead of `--onefile` for faster startup + +3. **Missing DLL files**: + - Ensure Visual C++ Redistributable is installed on target machines + - Include required DLLs using `--add-binary` option + +4. **OpenCV/CUDA issues**: + - If using CUDA, ensure CUDA DLLs are available + - May need to add CUDA paths to `pathex` in spec file + +### Testing the Executable + +1. Test on a clean Windows machine (without Python installed) +2. Ensure all required DLLs are included +3. Test camera functionality +4. Test file saving functionality + +## File Structure + +After building, you should have: + +``` +dist/ + └── camera_gui.exe # Standalone executable + +installer/ + └── OrisysSensorDemo_Setup.exe # Installer (after compiling with Inno Setup) +``` + +## Distribution + +- **Standalone executable**: Just distribute `dist\camera_gui.exe` +- **Installer**: Distribute `installer\OrisysSensorDemo_Setup.exe` for easier installation + +## Notes + +- The executable is large (~100-200MB) because it includes Python interpreter and all dependencies +- First launch may be slower as files are extracted +- Antivirus software may flag PyInstaller executables - this is a false positive +- Consider code signing for production releases + diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..c85c78c --- /dev/null +++ b/build.bat @@ -0,0 +1,65 @@ +@echo off +REM Build script for Windows +REM Creates executable and optionally installer + +echo ======================================== +echo Building Orisys Sensor Demo +echo ======================================== +echo. + +REM Check if Python is available +python --version >nul 2>&1 +if errorlevel 1 ( + echo ERROR: Python is not installed or not in PATH + pause + exit /b 1 +) + +REM Install/upgrade PyInstaller if needed +echo Installing/upgrading PyInstaller... +pip install --upgrade PyInstaller + +echo. +echo Building executable... +echo. + +REM Build using spec file +pyinstaller camera_gui.spec + +if errorlevel 1 ( + echo. + echo ERROR: Build failed! + pause + exit /b 1 +) + +echo. +echo ======================================== +echo Build successful! +echo Executable: dist\camera_gui.exe +echo ======================================== +echo. + +REM Ask if user wants to create installer +set /p CREATE_INSTALLER="Create installer? (y/n): " +if /i "%CREATE_INSTALLER%"=="y" ( + echo. + echo Creating installer... + echo Please ensure Inno Setup Compiler is installed. + echo Opening Inno Setup script... + + REM Try to find Inno Setup Compiler + if exist "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" ( + "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" build_installer.iss + ) else if exist "C:\Program Files\Inno Setup 6\ISCC.exe" ( + "C:\Program Files\Inno Setup 6\ISCC.exe" build_installer.iss + ) else ( + echo Inno Setup Compiler not found in default locations. + echo Please compile build_installer.iss manually using Inno Setup Compiler. + ) +) + +echo. +echo Done! +pause + diff --git a/build_exe.py b/build_exe.py new file mode 100644 index 0000000..09196f9 --- /dev/null +++ b/build_exe.py @@ -0,0 +1,52 @@ +""" +Build script for creating camera_gui.exe using PyInstaller +""" +import PyInstaller.__main__ +import os +import sys + +# Add PyQt5 and pyqtgraph to requirements if not present +requirements = [ + 'PyInstaller>=5.0', + 'PyQt5>=5.15.0', + 'pyqtgraph>=0.12.0', +] + +def build_exe(): + """Build the executable""" + + # PyInstaller arguments + args = [ + 'camera_gui.py', + '--name=camera_gui', + '--onefile', # Create a single executable file + '--windowed', # No console window (GUI app) + '--icon=orisys_logo_notext.png', # Use logo as icon + '--add-data=orisys_logo_notext.png;.', # Include logo file + '--hidden-import=PyQt5.QtCore', + '--hidden-import=PyQt5.QtGui', + '--hidden-import=PyQt5.QtWidgets', + '--hidden-import=pyqtgraph', + '--hidden-import=orisys', + '--hidden-import=cv2', + '--hidden-import=numpy', + '--collect-all=pyqtgraph', # Collect all pyqtgraph data + '--collect-all=PyQt5', # Collect all PyQt5 data + '--collect-all=cv2', # Collect all OpenCV data + ] + + # Add orisys package if it's a local package + if os.path.exists('src/orisys'): + args.append('--paths=src') + + print("Building executable...") + print("Command: PyInstaller " + " ".join(args)) + + PyInstaller.__main__.run(args) + + print("\nBuild complete! Executable is in the 'dist' folder.") + print("You can now create an installer using Inno Setup or NSIS.") + +if __name__ == '__main__': + build_exe() + diff --git a/build_installer.iss b/build_installer.iss new file mode 100644 index 0000000..b9667f4 --- /dev/null +++ b/build_installer.iss @@ -0,0 +1,52 @@ +; Inno Setup Script for camera_gui installer +; Compile this script using Inno Setup Compiler +; Download from: https://jrsoftware.org/isdl.php + +[Setup] +AppName=Orisys Sensor Demo +AppVersion=1.0.0 +AppPublisher=Orisys +AppPublisherURL=https://orisys.com +DefaultDirName={autopf}\Orisys\SensorDemo +DefaultGroupName=Orisys Sensor Demo +AllowNoIcons=yes +LicenseFile= +OutputDir=installer +OutputBaseFilename=OrisysSensorDemo_Setup +Compression=lzma +SolidCompression=yes +WizardStyle=modern +PrivilegesRequired=admin +ArchitecturesInstallIn64BitMode=x64 + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" +Name: "chinese"; MessagesFile: "compiler:Languages\ChineseSimplified.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked +Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 6.1; Check: not IsAdminInstallMode + +[Files] +Source: "dist\camera_gui.exe"; DestDir: "{app}"; Flags: ignoreversion +Source: "orisys_logo_notext.png"; DestDir: "{app}"; Flags: ignoreversion +; Add any other required files here +; Source: "config\*"; DestDir: "{app}\config"; Flags: ignoreversion recursesubdirs createallsubdirs + +[Icons] +Name: "{group}\Orisys Sensor Demo"; Filename: "{app}\camera_gui.exe"; IconFilename: "{app}\orisys_logo_notext.png" +Name: "{group}\{cm:UninstallProgram,Orisys Sensor Demo}"; Filename: "{uninstallexe}" +Name: "{autodesktop}\Orisys Sensor Demo"; Filename: "{app}\camera_gui.exe"; IconFilename: "{app}\orisys_logo_notext.png"; Tasks: desktopicon +Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\Orisys Sensor Demo"; Filename: "{app}\camera_gui.exe"; IconFilename: "{app}\orisys_logo_notext.png"; Tasks: quicklaunchicon + +[Run] +Filename: "{app}\camera_gui.exe"; Description: "{cm:LaunchProgram,Orisys Sensor Demo}"; Flags: nowait postinstall skipifsilent + +[Code] +function InitializeSetup(): Boolean; +begin + Result := True; + // Check if Visual C++ Redistributable is installed (if needed) + // Add checks here if required +end; + diff --git a/camera_gui.py b/camera_gui.py new file mode 100644 index 0000000..f98b563 --- /dev/null +++ b/camera_gui.py @@ -0,0 +1,1147 @@ +""" +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/camera_gui.spec b/camera_gui.spec new file mode 100644 index 0000000..78191ed --- /dev/null +++ b/camera_gui.spec @@ -0,0 +1,83 @@ +# -*- mode: python ; coding: utf-8 -*- +""" +PyInstaller spec file for camera_gui +Run: pyinstaller camera_gui.spec +""" + +import os +from PyInstaller.utils.hooks import collect_data_files, collect_submodules + +block_cipher = None + +# Collect data files +pyqtgraph_datas = collect_data_files('pyqtgraph') +pyqt5_datas = collect_data_files('PyQt5') +cv2_datas = collect_data_files('cv2') + +# Collect hidden imports +hiddenimports = [ + 'PyQt5.QtCore', + 'PyQt5.QtGui', + 'PyQt5.QtWidgets', + 'pyqtgraph', + 'pyqtgraph.graphicsItems', + 'pyqtgraph.widgets', + 'orisys', + 'cv2', + 'numpy', + 'scipy', +] + +# Add orisys submodules if available +try: + orisys_submodules = collect_submodules('orisys') + hiddenimports.extend(orisys_submodules) +except: + pass + +a = Analysis( + ['camera_gui.py'], + pathex=[], + binaries=[], + datas=[ + ('orisys_logo_notext.png', '.'), + *pyqtgraph_datas, + *pyqt5_datas, + *cv2_datas, + ], + hiddenimports=hiddenimports, + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False, +) + +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='camera_gui', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, # No console window + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon='orisys_logo_notext.png', +) + diff --git a/config/compound_eye.png b/config/compound_eye.png index cf20569..91675f5 100644 Binary files a/config/compound_eye.png and b/config/compound_eye.png differ diff --git a/orisys_logo.png b/orisys_logo.png new file mode 100644 index 0000000..ec6d56c Binary files /dev/null and b/orisys_logo.png differ diff --git a/orisys_logo_notext.png b/orisys_logo_notext.png new file mode 100644 index 0000000..a63d4e8 Binary files /dev/null and b/orisys_logo_notext.png differ diff --git a/requirements.txt b/requirements.txt index e8d7b3f..1de2d31 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,6 @@ opencv_python>=4.10.0.84 opencv-contrib-python scipy>=1.10.1 setuptools>=45.2.0 +PyQt5>=5.15.0 +pyqtgraph>=0.12.0 +PyInstaller>=5.0 diff --git a/src/orisys/nHHD.py b/src/orisys/nHHD.py index 444646b..4918a20 100644 --- a/src/orisys/nHHD.py +++ b/src/orisys/nHHD.py @@ -242,8 +242,10 @@ class nHHD(object): self.Sn = numpy.sum(numpy.absolute(self.d)) # print("Sn is:", self.Sn) - self.Stx = abs(numpy.sum(self.h[:, :, 0])) - self.Sty = abs(numpy.sum(self.h[:, :, 1])) + # self.Stx = abs(numpy.sum(self.h[:, :, 0])) + # self.Sty = abs(numpy.sum(self.h[:, :, 1])) + self.Stx = numpy.sum(self.h[:, :, 0]) + self.Sty = numpy.sum(self.h[:, :, 1]) # print("Stx is:", self.Stx) # print("Sty is:", self.Sty) diff --git a/src/orisys/sensor.py b/src/orisys/sensor.py index 69f4c0b..feb2ce5 100644 --- a/src/orisys/sensor.py +++ b/src/orisys/sensor.py @@ -235,6 +235,9 @@ class Sensor: results.append(self.depth if self.depth is not None else np.array([])) return tuple(results) if results else () + def reset(self): + self.tracker.reset() + def disconnect(self): """ Release video capture resources.