Hey fix my code i want it to use ONNX here is the new one and old one

class YOLOEnsemble:
    _instance = None
    _lock = threading.Lock()

    def __init__(self, models_config, class_names=None):
        """
        models_config: List of tuples (model_path, imgsz)
        class_names: dict mapping class indices to readable names, e.g., {0:'person'}
        """
        self.models = []  # [{'session':..., 'input_name':..., 'imgsz':...}, ...]
        self.names = class_names if class_names else {0: 'person'}
        self.cache_lock = threading.Lock()
        self._load_models(models_config)

    def _load_models(self, models_config):
        """Load ONNX models with DirectML EP."""
        for model_path, imgsz in models_config:
            try:
                onnx_path = model_path if model_path.endswith(".onnx") else model_path.replace('.pt', '.onnx')

                providers = ['DmlExecutionProvider']
                sess_options = ort.SessionOptions()
                sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL

                session = ort.InferenceSession(onnx_path, sess_options, providers=providers)
                input_name = session.get_inputs()[0].name
                output_names = [o.name for o in session.get_outputs()]

                logging.info(f"Loaded ONNX model '{onnx_path}' with DirectML: imgsz={imgsz}")
                logging.debug(f"Input name: {input_name}, Output names: {output_names}")

                self.models.append({
                    'session': session,
                    'input_name': input_name,
                    'imgsz': imgsz
                })

            except Exception as e:
                logging.error(f"Failed to load ONNX model {model_path}: {e}")

    def preprocess(self, frame, imgsz):
        """
        Preprocess input image to ONNX input format:
        Resize, normalize [0..1], HWC->CHW, add batch dim.
        """
        resized = cv2.resize(frame, (imgsz, imgsz), interpolation=cv2.INTER_LINEAR)
        img = resized.astype(np.float32) / 255.0
        # Model expects RGB order normalized to [0,1]
        img = np.transpose(img, (2, 0, 1))  # HWC to CHW
        img = np.expand_dims(img, axis=0)
        img = np.ascontiguousarray(img)
        return img

    def postprocess(self, outputs, conf_thres=0.3, iou_threshold=0.25, max_det=40):
        """
        Postprocess ONNX output tensor to boxes, scores, classes arrays.

        Expected output shape:
          (batch_size=1, N, 6) with columns [x1, y1, x2, y2, conf, class_id]

        Returns:
          boxes (M,4), scores (M,), classes (M,)
        """
        preds = outputs[0]

        # Print debug info about model output shape
        logging.debug(f"Raw model output shape: {preds.shape}")

        # If output has batch dim
        if preds.ndim == 3 and preds.shape[0] == 1:
            preds = preds[0]
        elif preds.ndim == 2:
            pass
        else:
            logging.warning(f"Unexpected output shape from model: {preds.shape}")
            return np.empty((0, 4)), np.empty((0,)), np.empty((0,), dtype=np.int32)

        if preds.shape[1] < 6:
            logging.warning(f"Output second dimension smaller than expected 6: {preds.shape}")
            return np.empty((0, 4)), np.empty((0,)), np.empty((0,), dtype=np.int32)

        # Filter predictions by confidence threshold
        conf_mask = preds[:, 4] > conf_thres
        preds = preds[conf_mask]

        if preds.shape[0] == 0:
            return np.empty((0, 4)), np.empty((0,)), np.empty((0,), dtype=np.int32)

        boxes = preds[:, 0:4]  # x1,y1,x2,y2
        scores = preds[:, 4]
        classes = preds[:, 5].astype(np.int32)

        # Apply NMS to eliminate overlapping boxes
        keep = self.fast_nms(boxes, scores, iou_threshold)
        if len(keep) > max_det:
            keep = keep[:max_det]

        return boxes[keep], scores[keep], classes[keep]

    def fast_nms(self, boxes, scores, iou_threshold=0.25):
        """
        Non-Maximum Suppression (NMS) to filter overlapping bounding boxes.
        """
        if len(boxes) == 0:
            return []

        boxes = np.array(boxes, dtype=np.float32)
        scores = np.array(scores, dtype=np.float32)

        indices = np.argsort(scores)[::-1]
        keep = []

        while indices.size > 0:
            current = indices[0]
            keep.append(current)
            if indices.size == 1:
                break

            remaining = indices[1:]
            current_box = boxes[current]
            remaining_boxes = boxes[remaining]

            x1 = np.maximum(current_box[0], remaining_boxes[:, 0])
            y1 = np.maximum(current_box[1], remaining_boxes[:, 1])
            x2 = np.minimum(current_box[2], remaining_boxes[:, 2])
            y2 = np.minimum(current_box[3], remaining_boxes[:, 3])

            inter_area = np.maximum(0, x2 - x1) * np.maximum(0, y2 - y1)
            area_current = (current_box[2] - current_box[0]) * (current_box[3] - current_box[1])
            area_remaining = (remaining_boxes[:, 2] - remaining_boxes[:, 0]) * (remaining_boxes[:, 3] - remaining_boxes[:, 1])
            union = area_current + area_remaining - inter_area
            iou = inter_area / np.maximum(union, 1e-10)

            indices = remaining[iou <= iou_threshold]

        return keep

    def predict(self, frame, conf_thres=0.3):
        """
        Run inference on frame for all models.

        Returns list of DummyResults each with .boxes.xyxy, .boxes.conf, .boxes.cls torch tensors.
        """
        all_results = []
        original_height, original_width = frame.shape[:2]

        # Debug info
        logging.debug(f"Predict called with frame shape: {frame.shape}")

        for model_info in self.models:
            session = model_info['session']
            input_name = model_info['input_name']
            imgsz = model_info['imgsz']

            input_tensor = self.preprocess(frame, imgsz)

            try:
                outputs = session.run(None, {input_name: input_tensor})

                boxes, scores, classes = self.postprocess(outputs, conf_thres=conf_thres, iou_threshold=0.25, max_det=40)

                logging.debug(f"Model dets: boxes={boxes.shape}, scores={scores.shape}")

                if boxes.shape[0] == 0:
                    continue

                scale_x = original_width / imgsz
                scale_y = original_height / imgsz

                boxes[:, [0, 2]] *= scale_x
                boxes[:, [1, 3]] *= scale_y

                boxes_torch = torch.from_numpy(boxes).float()
                scores_torch = torch.from_numpy(scores).float()
                classes_torch = torch.from_numpy(classes).float()

                class DummyBoxes:
                    def __init__(self, xyxy, conf, cls):
                        self.xyxy = xyxy
                        self.conf = conf
                        self.cls = cls

                class DummyResults:
                    def __init__(self, boxes, scale_x, scale_y, ow, oh):
                        self.boxes = boxes
                        self.scale_x = scale_x
                        self.scale_y = scale_y
                        self.original_width = ow
                        self.original_height = oh

                dummy_boxes = DummyBoxes(boxes_torch, scores_torch, classes_torch)
                dummy_results = DummyResults(dummy_boxes, scale_x, scale_y, original_width, original_height)

                all_results.append(dummy_results)

            except Exception as e:
                logging.warning(f"ONNX DirectML inference failed: {e}")
                continue

        return all_results

    @classmethod
    def get_instance(cls, models_config, class_names=None):
        with cls._lock:
            if cls._instance is None:
                cls._instance = YOLOEnsemble(models_config, class_names)
            return cls._instance


class DetectionThread(QtCore.QThread):
    detection_signal = QtCore.pyqtSignal(list, list, list, list, list)

    def __init__(self, settings):
        super().__init__()
        self.settings = settings
        self.running = False
        self.frame_lock = threading.Lock()

        # BitBlt setup variables
        self.user32 = ctypes.windll.user32
        self.gdi32 = ctypes.windll.gdi32
        self.dc = None
        self.memdc = None
        self.bitmap = None
        self.bitmapinfo = None

        # Tracker
        self.tracker = Sort(max_age=10, min_hits=1, iou_threshold=0.2)
        self.frame_skip = 0
        self.last_detection_time = 0

        # Screen info
        self.screen_width = 0
        self.screen_height = 0
        self.center_x = 0
        self.center_y = 0

        # Load your ensemble ONNX models here (add more if you want)
        model_paths = [
            ("models/yolo11n.onnx", 256),
            # ("models/other_model.onnx", imgsz), etc
        ]

        self.yolo_ensemble = YOLOEnsemble.get_instance(model_paths)

    def setup_bitblt(self):
        try:
            hwnd = self.user32.GetDesktopWindow()
            self.dc = self.user32.GetDC(hwnd)
            self.memdc = self.gdi32.CreateCompatibleDC(self.dc)

            class BITMAPINFOHEADER(ctypes.Structure):
                _fields_ = [
                    ("biSize", ctypes.c_uint32),
                    ("biWidth", ctypes.c_int),
                    ("biHeight", ctypes.c_int),
                    ("biPlanes", ctypes.c_short),
                    ("biBitCount", ctypes.c_short),
                    ("biCompression", ctypes.c_uint32),
                    ("biSizeImage", ctypes.c_uint32),
                    ("biXPelsPerMeter", ctypes.c_int),
                    ("biYPelsPerMeter", ctypes.c_int),
                    ("biClrUsed", ctypes.c_uint32),
                    ("biClrImportant", ctypes.c_uint32)
                ]

            self.bitmapinfo = BITMAPINFOHEADER()
            self.bitmapinfo.biSize = ctypes.sizeof(BITMAPINFOHEADER)
            self.bitmapinfo.biPlanes = 1
            self.bitmapinfo.biBitCount = 32
            self.bitmapinfo.biCompression = 0

            return True
        except Exception as e:
            logging.error(f"BitBlt setup failed: {e}")
            return False

    def capture_screen_region(self, x, y, width, height):
        try:
            self.bitmapinfo.biWidth = width
            self.bitmapinfo.biHeight = -height  # top-down bitmap
            self.bitmapinfo.biSizeImage = width * height * 4

            self.bitmap = self.gdi32.CreateCompatibleBitmap(self.dc, width, height)
            self.gdi32.SelectObject(self.memdc, self.bitmap)

            if not self.gdi32.BitBlt(self.memdc, 0, 0, width, height, self.dc, x, y, 0x00CC0020):
                raise Exception("BitBlt failed")

            buf_size = width * height * 4
            buffer = (ctypes.c_byte * buf_size)()

            self.gdi32.GetDIBits(self.memdc, self.bitmap, 0, height, buffer,
                                 ctypes.byref(self.bitmapinfo), 0)

            array = np.frombuffer(buffer, dtype=np.uint8)
            array = array.reshape(height, width, 4)

            self.gdi32.DeleteObject(self.bitmap)
            self.bitmap = None

            return array

        except Exception as e:
            logging.error(f"BitBlt capture failed: {e}")
            return None

    def cleanup_bitblt(self):
        try:
            if self.bitmap:
                self.gdi32.DeleteObject(self.bitmap)
                self.bitmap = None
            if self.memdc:
                self.gdi32.DeleteDC(self.memdc)
                self.memdc = None
            if self.dc:
                self.user32.ReleaseDC(None, self.dc)
                self.dc = None
        except Exception as e:
            logging.error(f"BitBlt cleanup failed: {e}")

    def fast_nms(self, boxes, scores, iou_threshold=0.25):
        if len(boxes) == 0:
            return []
        boxes = np.asarray(boxes, dtype=np.float32)
        scores = np.asarray(scores, dtype=np.float32)
        indices = np.argsort(scores)[::-1]

        keep = []
        while indices.size > 0:
            current = indices[0]
            keep.append(current)
            if indices.size == 1:
                break
            remaining = indices[1:]
            current_box = boxes[current]
            remaining_boxes = boxes[remaining]

            x1 = np.maximum(current_box[0], remaining_boxes[:, 0])
            y1 = np.maximum(current_box[1], remaining_boxes[:, 1])
            x2 = np.minimum(current_box[2], remaining_boxes[:, 2])
            y2 = np.minimum(current_box[3], remaining_boxes[:, 3])

            inter_area = np.maximum(0, x2 - x1) * np.maximum(0, y2 - y1)
            current_area = (current_box[2] - current_box[0]) * (current_box[3] - current_box[1])
            rem_area = (remaining_boxes[:, 2] - remaining_boxes[:, 0]) * (remaining_boxes[:, 3] - remaining_boxes[:, 1])
            union = current_area + rem_area - inter_area
            iou = inter_area / np.maximum(union, 1e-10)

            indices = remaining[iou <= iou_threshold]
        return keep

    def transform_coordinates(self, boxes, crop_x, crop_y, scale_x, scale_y):
        transformed = []
        for box in boxes:
            x1, y1, x2, y2 = box
            x1_screen = int(x1 * scale_x + crop_x)
            y1_screen = int(y1 * scale_y + crop_y)
            x2_screen = int(x2 * scale_x + crop_x)
            y2_screen = int(y2 * scale_y + crop_y)
            transformed.append((x1_screen, y1_screen, x2_screen, y2_screen))
        return transformed

    def run(self):
        self.running = True

        if not self.yolo_ensemble.models:
            logging.error("No models loaded")
            return

        if not self.setup_bitblt():
            logging.error("BitBlt setup failed")
            return

        self.screen_width = self.user32.GetSystemMetrics(0)
        self.screen_height = self.user32.GetSystemMetrics(1)
        self.center_x = self.screen_width >> 1
        self.center_y = self.screen_height >> 1

        logging.info(f"BitBlt detection started: {self.screen_width}x{self.screen_height}")

        last_time = time.perf_counter()

        try:
            while self.running:
                current_time = time.perf_counter()

                fov_radius = clamp(self.settings.get('fov_radius', 150), 50,
                                   min(self.screen_width, self.screen_height) >> 1)

                crop_x = max(self.center_x - fov_radius, 0)
                crop_y = max(self.center_y - fov_radius, 0)
                crop_width = min(fov_radius * 2, self.screen_width - crop_x)
                crop_height = min(fov_radius * 2, self.screen_height - crop_y)

                if crop_width < 100 or crop_height < 100:
                    continue

                frame = self.capture_screen_region(crop_x, crop_y, crop_width, crop_height)
                if frame is None or frame.size == 0:
                    continue

                frame = np.ascontiguousarray(frame)
                frame = cv2.cvtColor(frame, cv2.COLOR_BGRA2RGB)

                conf_thres = clamp(self.settings.get('confidence', 0.5), 0.2, 0.95)

                all_results = self.yolo_ensemble.predict(frame, conf_thres=conf_thres)

                detections = []
                for results in all_results:
                    if not hasattr(results, 'boxes') or results.boxes.xyxy.shape[0] == 0:
                        continue

                    boxes = results.boxes.xyxy.cpu().numpy()
                    confs = results.boxes.conf.cpu().numpy()
                    classes = results.boxes.cls.cpu().numpy()

                    scale_x = getattr(results, 'scale_x', 1.0)
                    scale_y = getattr(results, 'scale_y', 1.0)

                    screen_boxes = self.transform_coordinates(boxes, crop_x, crop_y, scale_x, scale_y)

                    for i, (box, conf, cls) in enumerate(zip(screen_boxes, confs, classes)):
                        if conf < conf_thres:
                            continue
                        x1_s, y1_s, x2_s, y2_s = box
                        if x1_s >= x2_s or y1_s >= y2_s or x1_s < 0 or y1_s < 0 or x2_s > self.screen_width or y2_s > self.screen_height:
                            continue
                        class_id = int(cls)
                        class_name = self.yolo_ensemble.names.get(class_id, "person")

                        # Distance check from center for FOV radius
                        det_center_x = (x1_s + x2_s) >> 1
                        det_center_y = (y1_s + y2_s) >> 1
                        dx = det_center_x - self.center_x
                        dy = det_center_y - self.center_y
                        dist_sq = dx*dx + dy*dy

                        if dist_sq <= fov_radius * fov_radius:
                            detections.append((x1_s, y1_s, x2_s, y2_s, float(conf), class_name))

                detections.sort(key=lambda x: x[4], reverse=True)

                boxes = [(d[0], d[1], d[2], d[3]) for d in detections]
                confidences = [d[4] for d in detections]
                labels = [d[5] for d in detections]

                if boxes:
                    selected_indices = self.fast_nms(boxes, confidences, iou_threshold=0.25)
                    boxes = [boxes[i] for i in selected_indices]
                    labels = [labels[i] for i in selected_indices]
                    confidences = [confidences[i] for i in selected_indices]

                dets_for_sort = np.array([[*box, conf] for box, conf in zip(boxes, confidences)]) if boxes else np.empty((0,5))

                tracked_objects = self.tracker.update(dets_for_sort)

                tracked_boxes = []
                track_ids = []

                for obj in tracked_objects:
                    x1t, y1t, x2t, y2t, track_id = obj
                    x1t, y1t, x2t, y2t = map(int, (x1t, y1t, x2t, y2t))
                    if x1t < x2t and y1t < y2t and 0 <= x1t < self.screen_width and 0 <= y1t < self.screen_height and x2t <= self.screen_width and y2t <= self.screen_height:
                        tracked_boxes.append((x1t, y1t, x2t, y2t))
                        track_ids.append(int(track_id))

                self.detection_signal.emit(tracked_boxes, labels, confidences, labels, track_ids)

                self.frame_skip += 1
                if self.frame_skip >= 200:
                    fps = 1.0 / max(0.001, current_time - last_time)
                    logging.info(f"BitBlt Performance - FPS: {fps:.1f}, Detections: {len(tracked_boxes)}")
                    self.frame_skip = 0

                last_time = current_time

        except Exception as e:
            logging.error(f"Detection error: {e}")

        finally:
            self.cleanup_bitblt()

    def stop(self):
        self.running = False
        self.cleanup_bitblt()
        logging.info("BitBlt detection stopped")

class YOLOEnsemble:
    _instance = None
    _lock = threading.Lock()

    def __init__(self, models_config):
        self.models = []
        self.names = {}
        self.prediction_cache = {}
        self.cache_lock = threading.Lock()
        self.device = self._setup_directml_device()
        self._load_models(models_config)

    def _setup_directml_device(self):
        """Setup DirectML for maximum AMD GPU performance with fallbacks"""
        try:
            # Try DirectML first
            if torch_directml.is_available():
                device_count = torch_directml.device_count()
                logging.info(f"DirectML available with {device_count} device(s)")
                
                if device_count > 0:
                    # Get the best DirectML device (usually 0)
                    dml_device = torch_directml.device(0)
                    
                    # Maximum DirectML optimizations
                    torch.backends.directml.enabled = True
                    torch.backends.directml.allow_reduced_precision = True
                    
                    logging.info(f"DirectML device initialized: {dml_device}")
                    return dml_device
            
            # Fallback to CUDA if available
            if torch.cuda.is_available():
                cuda_device = torch.device('cuda:0')
                logging.info(f"CUDA device initialized: {cuda_device}")
                return cuda_device
            
            # Final fallback to CPU with warning
            logging.warning("No GPU available, falling back to CPU (performance will be reduced)")
            return torch.device('cpu')
                
        except Exception as e:
            logging.warning(f"GPU setup failed ({e}), falling back to CPU")
            return torch.device('cpu')

    def _load_models(self, models_config):
        """Load models with DirectML optimization for maximum performance"""
        for model_path, imgsz in models_config:
            try:
                # Load YOLO model
                model = YOLO(model_path)
                
                # Force model to DirectML device for 100% GPU utilization
                model.model = model.model.to(self.device)
                
                # Set model to evaluation mode and optimize
                model.model.eval()
                
                # Enable DirectML optimizations
                if hasattr(torch.backends, 'directml') and torch.backends.directml.enabled:
                    # Compile model for maximum DirectML performance
                    try:
                        model.model = torch.jit.optimize_for_inference(model.model)
                        logging.info(f"Model JIT optimized for DirectML: {model_path}")
                    except:
                        logging.info(f"JIT optimization skipped for: {model_path}")
                
                # Zero-latency warmup with DirectML - more aggressive
                dummy = torch.zeros((1, 3, imgsz, imgsz), dtype=torch.float32, device=self.device)
                
                with torch.no_grad():
                    # More aggressive warmup - 15 iterations for maximum DirectML optimization
                    for _ in range(15):
                        try:
                            _ = model.model(dummy)
                        except:
                            # Fallback warmup using predict
                            dummy_np = np.zeros((imgsz, imgsz, 3), dtype=np.uint8)
                            model.predict(
                                source=dummy_np,
                                imgsz=imgsz,
                                device=self.device,
                                verbose=False,
                                stream=False
                            )
                            break
                
                # Clear cache for maximum performance (if DirectML available)
                if hasattr(torch, 'directml') and str(self.device).startswith('privateuseone'):
                    torch.directml.empty_cache()
                elif torch.cuda.is_available() and self.device.type == 'cuda':
                    torch.cuda.empty_cache()
                
                self.models.append((model, imgsz))
                if hasattr(model, "names"):
                    self.names.update(model.names)
                    
                logging.info(f"DirectML model loaded: {model_path} -> {imgsz}px on {self.device}")
                
            except Exception as e:
                logging.error(f"Failed to load model {model_path}: {e}")

    def predict(self, frame, conf_thres=0.3):
        """Ultra-high performance DirectML prediction with zero latency"""
        all_results = []
        original_height, original_width = frame.shape[:2]
        
        # Process all models with DirectML for maximum GPU utilization
        with torch.no_grad():  # Disable gradients for inference speed
            for model, imgsz in self.models:
                try:
                    # Calculate scale factors for coordinate transformation
                    scale_x = original_width / imgsz
                    scale_y = original_height / imgsz
                    
                    # Ultra-fast resize with optimized interpolation
                    resized_frame = cv2.resize(frame, (imgsz, imgsz), interpolation=cv2.INTER_LINEAR)
                    
                    # Maximum performance prediction with DirectML
                    results = model.predict(
                        source=resized_frame,
                        save=False,
                        imgsz=imgsz,
                        conf=conf_thres,
                        iou=0.15,  # More aggressive NMS for speed
                        device=self.device,
                        half=False,  # DirectML works better with float32
                        verbose=False,
                        stream=False,
                        agnostic_nms=True,
                        max_det=40,  # Slightly higher detection count
                        classes=[0],  # Person class only
                        retina_masks=False
                    )[0]
                    
                    # Store transformation data
                    results.scale_x = scale_x
                    results.scale_y = scale_y
                    results.original_width = original_width
                    results.original_height = original_height
                    
                    all_results.append(results)
                    
                except Exception as e:
                    logging.warning(f"DirectML prediction failed: {e}")
                    continue
        
        # No cache clearing to maintain maximum GPU utilization
        # Only clear if memory is getting full
        if hasattr(torch, 'directml') and str(self.device).startswith('privateuseone'):
            pass  # Keep DirectML memory allocated
        elif torch.cuda.is_available() and self.device.type == 'cuda':
            pass  # Keep CUDA memory allocated
        return all_results

    @classmethod
    def get_instance(cls, models_config):
        with cls._lock:
            if cls._instance is None:
                cls._instance = YOLOEnsemble(models_config)
            return cls._instance

def clamp(value, min_val, max_val):
    """Zero-overhead clamp function"""
    return min_val if value < min_val else max_val if value > max_val else value

class DetectionThread(QtCore.QThread):
    detection_signal = QtCore.pyqtSignal(list, list, list, list, list)

    def __init__(self, settings):
        super().__init__()
        self.settings = settings
        self.running = False
        self.frame_lock = threading.Lock()
        
        # BitBlt setup
        self.user32 = ctypes.windll.user32
        self.gdi32 = ctypes.windll.gdi32
        self.dc = None
        self.memdc = None
        self.bitmap = None
        self.bitmapinfo = None
        
        # Ultra-fast tracker with minimal overhead
        self.tracker = Sort(max_age=10, min_hits=1, iou_threshold=0.2)
        self.frame_skip = 0
        self.last_detection_time = 0
        
        # Screen cache
        self.screen_width = 0
        self.screen_height = 0
        self.center_x = 0
        self.center_y = 0        # Load DirectML optimized models
        model_paths = [
            ("models/BestMain.pt", 96),
            ("models/yolo11n.pt", 128)
        ]
        

        self.yolo_ensemble = YOLOEnsemble.get_instance(model_paths)

    def setup_bitblt(self):
        """Initialize BitBlt capture resources"""
        try:
            # Get desktop window handle
            hwnd = self.user32.GetDesktopWindow()
            
            # Get DC and create compatible DC
            self.dc = self.user32.GetDC(hwnd)
            self.memdc = self.gdi32.CreateCompatibleDC(self.dc)
            
            # Create bitmap info structure
            class BITMAPINFOHEADER(ctypes.Structure):
                _fields_ = [
                    ("biSize", ctypes.c_uint32),
                    ("biWidth", ctypes.c_int),
                    ("biHeight", ctypes.c_int),
                    ("biPlanes", ctypes.c_short),
                    ("biBitCount", ctypes.c_short),
                    ("biCompression", ctypes.c_uint32),
                    ("biSizeImage", ctypes.c_uint32),
                    ("biXPelsPerMeter", ctypes.c_int),
                    ("biYPelsPerMeter", ctypes.c_int),
                    ("biClrUsed", ctypes.c_uint32),
                    ("biClrImportant", ctypes.c_uint32)
                ]

            self.bitmapinfo = BITMAPINFOHEADER()
            self.bitmapinfo.biSize = ctypes.sizeof(BITMAPINFOHEADER)
            self.bitmapinfo.biPlanes = 1
            self.bitmapinfo.biBitCount = 32
            self.bitmapinfo.biCompression = 0
            
            return True
            
        except Exception as e:
            logging.error(f"BitBlt setup failed: {e}")
            return False

    def capture_screen_region(self, x, y, width, height):
        """Capture screen region using BitBlt"""
        try:
            # Update bitmap info
            self.bitmapinfo.biWidth = width
            self.bitmapinfo.biHeight = -height  # Negative for top-down
            self.bitmapinfo.biSizeImage = width * height * 4
            
            # Create compatible bitmap
            self.bitmap = self.gdi32.CreateCompatibleBitmap(self.dc, width, height)
            self.gdi32.SelectObject(self.memdc, self.bitmap)
            
            # Perform BitBlt operation
            result = self.gdi32.BitBlt(
                self.memdc, 0, 0, width, height,
                self.dc, x, y,
                0x00CC0020  # SRCCOPY
            )
            
            if not result:
                raise Exception("BitBlt failed")
            
            # Get bitmap bits
            buffer_size = width * height * 4
            buffer = (ctypes.c_byte * buffer_size)()
            
            self.gdi32.GetDIBits(
                self.memdc, self.bitmap, 0, height,
                buffer, ctypes.byref(self.bitmapinfo), 0
            )
            
            # Convert to numpy array
            array = np.frombuffer(buffer, dtype=np.uint8)
            array = array.reshape(height, width, 4)
            
            # Cleanup current bitmap
            self.gdi32.DeleteObject(self.bitmap)
            
            return array
            
        except Exception as e:
            logging.error(f"BitBlt capture failed: {e}")
            return None

    def cleanup_bitblt(self):
        """Clean up BitBlt resources"""
        try:
            if self.bitmap:
                self.gdi32.DeleteObject(self.bitmap)
            if self.memdc:
                self.gdi32.DeleteDC(self.memdc)
            if self.dc:
                self.user32.ReleaseDC(None, self.dc)
        except Exception as e:
            logging.error(f"BitBlt cleanup failed: {e}")

    def fast_nms(self, boxes, scores, iou_threshold=0.25):
        """Ultra-fast NMS implementation with minimal overhead"""
        if len(boxes) == 0:
            return []

        # Convert to numpy arrays for vectorized operations
        boxes = np.asarray(boxes, dtype=np.float32)
        scores = np.asarray(scores, dtype=np.float32)

        # Get sorted indices by score (descending)
        indices = np.argsort(scores)[::-1]
        
        keep = []
        while indices.size > 0:
            # Take the highest scoring box
            current = indices[0]
            keep.append(current)
            
            if indices.size == 1:
                break
            
            # Calculate IoU with vectorized operations
            remaining_indices = indices[1:]
            current_box = boxes[current]
            remaining_boxes = boxes[remaining_indices]
            
            # Fast IoU calculation
            ious = self._fast_iou(current_box, remaining_boxes)
            
            # Keep only boxes with IoU below threshold
            indices = remaining_indices[ious <= iou_threshold]
        
        return keep
    
    def _fast_iou(self, box, boxes):
        """Vectorized IoU calculation with minimal overhead"""
        # Intersection coordinates
        x1 = np.maximum(box[0], boxes[:, 0])
        y1 = np.maximum(box[1], boxes[:, 1])
        x2 = np.minimum(box[2], boxes[:, 2])
        y2 = np.minimum(box[3], boxes[:, 3])
        
        # Intersection area
        intersection = np.maximum(0, x2 - x1) * np.maximum(0, y2 - y1)
        
        # Box areas
        box_area = (box[2] - box[0]) * (box[3] - box[1])
        boxes_area = (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1])
        
        # Union and IoU
        union = box_area + boxes_area - intersection
        return intersection / np.maximum(union, 1e-10)

    def transform_coordinates(self, boxes, crop_x, crop_y, scale_x, scale_y):
        """Zero-latency coordinate transformation"""
        transformed_boxes = []
        
        for box in boxes:
            x1, y1, x2, y2 = box
            
            # Scale and transform in one operation
            x1_screen = int(x1 * scale_x + crop_x)
            y1_screen = int(y1 * scale_y + crop_y)
            x2_screen = int(x2 * scale_x + crop_x)
            y2_screen = int(y2 * scale_y + crop_y)
            
            transformed_boxes.append((x1_screen, y1_screen, x2_screen, y2_screen))
            
        return transformed_boxes

    def run(self):
        """Main detection loop with BitBlt capture and DirectML"""
        self.running = True
        
        if not self.yolo_ensemble.models:
            logging.error("No models loaded")
            return
            
        if not self.setup_bitblt():
            logging.error("BitBlt setup failed")
            return

        # Get screen dimensions
        self.screen_width = self.user32.GetSystemMetrics(0)  # SM_CXSCREEN
        self.screen_height = self.user32.GetSystemMetrics(1)  # SM_CYSCREEN
        self.center_x = self.screen_width >> 1
        self.center_y = self.screen_height >> 1
        
        logging.info(f"BitBlt detection started: {self.screen_width}x{self.screen_height}")
        
        last_time = time.perf_counter()
        
        try:
            while self.running:
                current_time = time.perf_counter()
                
                # Calculate FOV and crop region
                fov_radius = clamp(self.settings.get('fov_radius', 150), 50,
                                 min(self.screen_width, self.screen_height) >> 1)
                
                crop_x = max(self.center_x - fov_radius, 0)
                crop_y = max(self.center_y - fov_radius, 0)
                crop_width = min(fov_radius * 2, self.screen_width - crop_x)
                crop_height = min(fov_radius * 2, self.screen_height - crop_y)
                
                if crop_width < 100 or crop_height < 100:
                    continue
                
                # Capture screen region using BitBlt
                frame = self.capture_screen_region(crop_x, crop_y, crop_width, crop_height)
                
                if frame is None or frame.size == 0:
                    continue
                
                # Convert color space
                frame = cv2.cvtColor(frame, cv2.COLOR_BGRA2RGB)
                
                # Run detection with DirectML
                conf_thres = clamp(self.settings.get('confidence', 0.5), 0.2, 0.95)
                all_results = self.yolo_ensemble.predict(frame, conf_thres=conf_thres)
                
                # Process detections
                detections = []
                for results in all_results:
                    if hasattr(results, 'boxes') and len(results.boxes) > 0:
                        boxes = results.boxes.xyxy.cpu().numpy()
                        confs = results.boxes.conf.cpu().numpy()
                        classes = results.boxes.cls.cpu().numpy()
                        
                        scale_x = getattr(results, 'scale_x', 1.0)
                        scale_y = getattr(results, 'scale_y', 1.0)
                        
                        screen_boxes = self.transform_coordinates(
                            boxes, crop_x, crop_y, scale_x, scale_y
                        )
                        
                        for i, (screen_box, conf, cls) in enumerate(zip(screen_boxes, confs, classes)):
                            if conf < conf_thres:
                                continue
                                
                            x1_screen, y1_screen, x2_screen, y2_screen = screen_box
                            class_id = int(cls)
                            class_name = self.yolo_ensemble.names.get(class_id, "person")
                            
                            if (x1_screen >= x2_screen or y1_screen >= y2_screen or
                                x1_screen < 0 or y1_screen < 0 or
                                x2_screen > self.screen_width or y2_screen > self.screen_height):
                                continue
                                
                            det_center_x = (x1_screen + x2_screen) >> 1
                            det_center_y = (y1_screen + y2_screen) >> 1
                            
                            dx = det_center_x - self.center_x
                            dy = det_center_y - self.center_y
                            dist_squared = dx * dx + dy * dy
                            
                            if dist_squared <= fov_radius * fov_radius:
                                detections.append((
                                    x1_screen, y1_screen, x2_screen, y2_screen,
                                    float(conf), class_name
                                ))

                # Sort by confidence (highest first)
                detections.sort(key=lambda x: x[4], reverse=True)

                # Extract data for processing
                boxes = [(x1, y1, x2, y2) for (x1, y1, x2, y2, _, _) in detections]
                confidences = [conf for (_, _, _, _, conf, _) in detections]
                labels = [label for (_, _, _, _, _, label) in detections]

                # Apply fast NMS
                if boxes:
                    selected_indices = self.fast_nms(boxes, confidences, iou_threshold=0.25)
                    boxes = [boxes[i] for i in selected_indices]
                    labels = [labels[i] for i in selected_indices]
                    confidences = [confidences[i] for i in selected_indices]

                # Prepare tracking data
                dets_for_sort = np.array([
                    [*box, conf] for box, conf in zip(boxes, confidences)
                ]) if boxes else np.empty((0, 5))
                
                # Tracking
                tracked_objects = self.tracker.update(dets_for_sort)

                # Extract tracking results
                tracked_boxes = []
                track_ids = []
                
                for obj in tracked_objects:
                    x1t, y1t, x2t, y2t, track_id = obj
                    
                    x1t, y1t, x2t, y2t = int(x1t), int(y1t), int(x2t), int(y2t)
                    
                    if (x1t < x2t and y1t < y2t and 
                        x1t >= 0 and y1t >= 0 and 
                        x2t <= self.screen_width and y2t <= self.screen_height):
                        
                        tracked_boxes.append((x1t, y1t, x2t, y2t))
                        track_ids.append(int(track_id))

                # Emit detection signal
                self.detection_signal.emit(
                    tracked_boxes, labels, confidences, labels, track_ids
                )
                
                # Performance monitoring
                self.frame_skip += 1
                if self.frame_skip >= 200:
                    self.frame_skip = 0
                    fps = 1.0 / max(0.001, current_time - last_time)
                    logging.info(f"BitBlt Performance - FPS: {fps:.1f}, Detections: {len(tracked_boxes)}")
                
                last_time = current_time
                
        except Exception as e:
            logging.error(f"Detection error: {e}")
        
        finally:
            self.cleanup_bitblt()

    def stop(self):
        """Clean shutdown"""
        self.running = False
        self.cleanup_bitblt()
        logging.info("BitBlt detection stopped")