python code import sys
import numpy as np
import mvsdk
import ctypes
from PyQt5.QtWidgets import (QApplication, QMainWindow, QLabel, QPushButton,
                             QVBoxLayout, QHBoxLayout, QWidget, QMessageBox,
                             QComboBox)
from PyQt5.QtCore import (QThread, QObject, pyqtSignal, pyqtSlot, Qt)
from PyQt5.QtGui import QImage, QPixmap

# --- Helper Classes ---
class ResolutionInfo:
    """Stores width/height and matches SDK resolution entries"""
    def __init__(self, width: int, height: int):
        self.width = width
        self.height = height

    def __str__(self) -> str:
        return f"{self.width}x{self.height}"

class CameraConfig:
    """Configuration for a single camera"""
    def __init__(self, cam_id: int, fps: int):
        self.cam_id = cam_id      # 0 or 1 for two cameras
        self.fps = fps            # 3 or 5
        self.target_res = ResolutionInfo(1600, 1200)  # Default target resolution

# --- Camera Worker (Core Capture Logic) ---
class CameraWorker(QObject):
    """Handles capture for ONE camera with proper SDK resolution indexing"""
    image_updated = pyqtSignal(int, QImage)  # (cam_id, QImage)
    error_occurred = pyqtSignal(int, str)   # (cam_id, error_msg)
    finished = pyqtSignal(int)              # (cam_id)
    resolutions_available = pyqtSignal(int, list)  # (cam_id, resolutions)

    def __init__(self, config: CameraConfig):
        super().__init__()
        self.config = config
        self._is_running = False
        self.h_cam = 0            # Camera handle
        self.frame_buffer = None  # Allocated frame buffer
        self.buffer_size = 0      # Buffer size in bytes

    @pyqtSlot()
    def run_capture(self):
        """Main capture loop with proper SDK resolution handling"""
        self._is_running = True
        cam_id = self.config.cam_id

        try:
            # --- Step 1: Enumerate and Initialize Camera ---
            dev_list = mvsdk.CameraEnumerateDevice()
            if cam_id >= len(dev_list):
                raise RuntimeError(f"Camera {cam_id} not connected")

            self.h_cam = mvsdk.CameraInit(dev_list[cam_id], -1, -1)
            if self.h_cam == 0:
                raise RuntimeError(f"Failed to initialize camera {cam_id}")

            # --- Step 2: Get Supported Resolutions ---
            cap = mvsdk.CameraGetCapability(self.h_cam)
            sdk_resolutions = self._get_sdk_resolutions(cap)
            
            # Emit resolutions to UI for display
            self.resolutions_available.emit(cam_id, sdk_resolutions)

            # --- Step 3: Validate and Set Resolution ---
            res_index = self._find_resolution_index(sdk_resolutions)
            if res_index == -1:
                raise RuntimeError(
                    f"Resolution {self.config.target_res} not supported. "
                    f"Supported: {[str(r) for r in sdk_resolutions]}"
                )
            
            # CORRECT SDK USAGE: Set resolution by INDEX (not width/height)
            mvsdk.CameraSetImageResolution(self.h_cam, res_index)

            # --- Step 4: Set FPS and Image Format ---
            mvsdk.CameraSetFrameSpeed(self.h_cam, self.config.fps)
            mvsdk.CameraSetIspOutFormat(self.h_cam, mvsdk.CAMERA_MEDIA_TYPE_RGB8)

            # --- Step 5: Allocate Frame Buffer ---
            active_res = sdk_resolutions[res_index]
            self.buffer_size = active_res.width * active_res.height * 3  # RGB
            self.frame_buffer = mvsdk.CameraAlignMalloc(self.buffer_size, 16)

            # --- Step 6: Start Acquisition ---
            mvsdk.CameraPlay(self.h_cam)

        except Exception as e:
            self.error_occurred.emit(cam_id, f"Setup failed: {str(e)}")
            self._cleanup()
            return

        # --- Main Capture Loop ---
        while self._is_running:
            try:
                # Get raw frame (blocking with 1000ms timeout)
                raw_data, frame_head = mvsdk.CameraGetImageBuffer(self.h_cam, 1000)
                
                # Process to RGB and release raw buffer
                mvsdk.CameraImageProcess(self.h_cam, raw_data, self.frame_buffer, frame_head)
                mvsdk.CameraReleaseImageBuffer(self.h_cam, raw_data)

                # Convert to QImage
                img = self._buffer_to_qimage(frame_head)
                if img:
                    self.image_updated.emit(cam_id, img)

            except mvsdk.CameraException as e:
                if e.error_code == mvsdk.CAMERA_STATUS_TIME_OUT:
                    continue  # Ignore timeouts at low FPS
                self.error_occurred.emit(cam_id, f"Capture error: {e.message}")
                self._is_running = False
            except Exception as e:
                self.error_occurred.emit(cam_id, f"Unexpected error: {str(e)}")
                self._is_running = False

        self._cleanup()

    def _get_sdk_resolutions(self, cap) -> list[ResolutionInfo]:
        """
        Get ALL supported resolutions from the SDK's capability structure.
        Returns list of ResolutionInfo objects.
        """
        resolutions = []
        for i in range(cap.iResolutionRange):
            res_info = cap.pResolutionInfo[i]
            if res_info.iWidth > 0 and res_info.iHeight > 0:
                resolutions.append(ResolutionInfo(res_info.iWidth, res_info.iHeight))
        return resolutions

    def _find_resolution_index(self, sdk_resolutions: list[ResolutionInfo]) -> int:
        """
        Find the SDK index of the target resolution (e.g., 1600x1200).
        Returns -1 if not found.
        """
        for idx, res in enumerate(sdk_resolutions):
            if (res.width == self.config.target_res.width and 
                res.height == self.config.target_res.height):
                return idx
        return -1

    def _buffer_to_qimage(self, frame_head) -> QImage:
        """Convert SDK frame buffer to Qt QImage"""
        try:
            data = (ctypes.c_ubyte * self.buffer_size).from_address(self.frame_buffer)
            arr = np.array(data, dtype=np.uint8).reshape(
                (frame_head.iHeight, frame_head.iWidth, 3)
            )
            return QImage(
                arr.data,
                frame_head.iWidth,
                frame_head.iHeight,
                3 * frame_head.iWidth,
                QImage.Format_RGB888
            ).copy()
        except Exception as e:
            self.error_occurred.emit(self.config.cam_id, f"Image conversion failed: {str(e)}")
            return None

    def _cleanup(self):
        """Release all camera resources"""
        cam_id = self.config.cam_id
        if self.h_cam:
            try:
                mvsdk.CameraStop(self.h_cam)
                mvsdk.CameraUnInit(self.h_cam)
            except Exception as e:
                print(f"Cleanup error (cam {cam_id}): {str(e)}")
            self.h_cam = 0

        if self.frame_buffer:
            mvsdk.CameraAlignFree(self.frame_buffer)
            self.frame_buffer = None

        self.finished.emit(cam_id)

    @pyqtSlot()
    def stop(self):
        """Stop capture loop (called from main thread)"""
        self._is_running = False

# --- Main GUI Window ---
class DualCameraWindow(QMainWindow):
    """Main application window with dual-camera display and controls"""
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Dual Camera MvSDK App")
        self.setGeometry(100, 100, 1600, 700)
        self.camera_count = 2  # Target for two cameras
        self.threads = {}      # {cam_id: QThread}
        self.workers = {}      # {cam_id: CameraWorker}
        self.is_capturing = False

        # --- UI Setup ---
        central = QWidget()
        self.setCentralWidget(central)
        layout = QVBoxLayout(central)

        # Video Displays (Side-by-Side)
        self.display_layout = QHBoxLayout()
        self.displays = [
            QLabel(f"Camera {i+1}\nClick START to begin", alignment=Qt.AlignCenter)
            for i in range(self.camera_count)
        ]
        for display in self.displays:
            display.setStyleSheet("""
                border: 2px solid #666;
                background: #f0f0f0;
                font-size: 14px;
                min-width: 600px;
                min-height: 450px;
            """)
            self.display_layout.addWidget(display)
        layout.addLayout(self.display_layout)

        # Control Panel
        self.controls = QHBoxLayout()
        layout.addLayout(self.controls)

        # Resolution Selector
        self.res_combo = QComboBox()
        self.controls.addWidget(QLabel("Resolution:"))
        self.controls.addWidget(self.res_combo)

        # FPS Selector
        self.fps_combo = QComboBox()
        self.fps_combo.addItems(["3 FPS", "5 FPS"])
        self.controls.addWidget(QLabel("FPS:"))
        self.controls.addWidget(self.fps_combo)

        # Start/Stop Button
        self.start_btn = QPushButton("START")
        self.start_btn.clicked.connect(self.toggle_capture)
        self.controls.addWidget(self.start_btn)

    @pyqtSlot()
    def toggle_capture(self):
        """Start/Stop capture for both cameras"""
        if self.is_capturing:
            self.stop_capture()
        else:
            self.start_capture()

    def start_capture(self):
        """Initialize both cameras with user-selected settings"""
        if self.is_capturing:
            return

        # Get user settings
        target_fps = int(self.fps_combo.currentText().split()[0])
        target_res = self._get_selected_resolution()

        # Configure and start both cameras
        for cam_id in range(self.camera_count):
            config = CameraConfig(cam_id, target_fps)
            config.target_res = target_res
            self._start_worker(config)

        self.is_capturing = True
        self.start_btn.setText("STOP")
        self._disable_controls()

    def _start_worker(self, config: CameraConfig):
        """Start a worker thread for a single camera"""
        cam_id = config.cam_id
        thread = QThread()
        worker = CameraWorker(config)

        # Thread setup
        worker.moveToThread(thread)
        thread.started.connect(worker.run_capture)

        # Signal connections
        worker.image_updated.connect(self._update_display)
        worker.error_occurred.connect(self._handle_error)
        worker.resolutions_available.connect(self._update_resolutions)
        worker.finished.connect(thread.quit)
        worker.finished.connect(worker.deleteLater)
        thread.finished.connect(thread.deleteLater)
        thread.finished.connect(lambda: self._on_thread_finish(cam_id))

        # Store references
        self.threads[cam_id] = thread
        self.workers[cam_id] = worker
        thread.start()

    def stop_capture(self):
        """Stop all camera workers and clean up"""
        if not self.is_capturing:
            return

        for cam_id in self.workers:
            self.workers[cam_id].stop()
            if self.threads[cam_id].isRunning():
                self.threads[cam_id].wait(2000)  # Wait up to 2s

        self.is_capturing = False
        self.start_btn.setText("START")
        self._enable_controls()
        self._reset_displays()

    @pyqtSlot(int, QImage)
    def _update_display(self, cam_id: int, image: QImage):
        """Update the correct camera display"""
        pixmap = QPixmap.fromImage(image).scaled(
            self.displays[cam_id].size(),
            Qt.KeepAspectRatio,
            Qt.SmoothTransformation
        )
        self.displays[cam_id].setPixmap(pixmap)

    @pyqtSlot(int, list)
    def _update_resolutions(self, cam_id: int, resolutions: list[ResolutionInfo]):
        """Populate resolution dropdown with SDK-supported options"""
        if cam_id == 0:  # Use first camera's resolutions for both
            self.res_combo.clear()
            # Sort by width (largest first)
            sorted_res = sorted(resolutions, key=lambda r: -r.width)
            for res in sorted_res:
                self.res_combo.addItem(str(res))
            
            # Select default resolution (1600x1200 if available)
            default_text = "1600x1200"
            idx = self.res_combo.findText(default_text)
            if idx != -1:
                self.res_combo.setCurrentIndex(idx)

    @pyqtSlot(int, str)
    def _handle_error(self, cam_id: int, error: str):
        """Show error message and stop faulty camera"""
        QMessageBox.critical(self, f"Camera {cam_id+1} Error", error)
        self._stop_camera(cam_id)

    def _on_thread_finish(self, cam_id: int):
        """Clean up after a camera thread finishes"""
        self.threads.pop(cam_id, None)
        self.workers.pop(cam_id, None)
        if not self.threads:  # All cameras stopped
            self.stop_capture()

    def _get_selected_resolution(self) -> ResolutionInfo:
        """Parse selected resolution from dropdown"""
        res_text = self.res_combo.currentText()
        w, h = map(int, res_text.split('x'))
        return ResolutionInfo(w, h)

    def _stop_camera(self, cam_id: int):
        """Stop a single camera (used for error handling)"""
        if cam_id in self.workers:
            self.workers[cam_id].stop()
            if self.threads[cam_id].isRunning():
                self.threads[cam_id].wait(2000)

    def _disable_controls(self):
        """Disable UI controls during capture"""
        self.res_combo.setEnabled(False)
        self.fps_combo.setEnabled(False)

    def _enable_controls(self):
        """Enable UI controls when not capturing"""
        self.res_combo.setEnabled(True)
        self.fps_combo.setEnabled(True)

    def _reset_displays(self):
        """Reset display labels to default text"""
        for i, display in enumerate(self.displays):
            display.setText(f"Camera {i+1}\nClick START to begin")

    def closeEvent(self, event):
        """Ensure clean exit when closing window"""
        self.stop_capture()
        event.accept()

# --- Application Entry ---
if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = DualCameraWindow()
    window.show()
    sys.exit(app.exec_())