Hey so yesterday i changend my Detection Thread and Essemble to work on DirectML with ONNX but i recognized that the one what i turned to ONNX works well gives me higher FPS and that what i want but now it doest detect anything anymore im going to show you the New one(ONNX) and the old(only direct ml and it worked) but i want to mainly fix the ONNX version(New one) and i want you to know that i use yolo11n so a very new model and all of this is in one Python file and when you do the fixes can you give me the entire YOLOEnsemble as the first part but all feature everything entire thing should be still ther but with fixes but all features still in and that all goes the same for Detection Thread

New one(ONNX)

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


def __init__(self, models_config):
    self.models = []  # List of {'session':..., 'input_name':..., 'imgsz':...}
    self.names = {0: 'person'}  # You can update this if you have class names
    self.cache_lock = threading.Lock()
    self._load_models(models_config)

def _load_models(self, models_config):
    """Load ONNX models with DirectML for AMD GPU acceleration"""
    for model_path, imgsz in models_config:
        try:
            onnx_path = 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

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

            logging.info(f"Loaded ONNX model with DirectML: {onnx_path} (imgsz={imgsz})")

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

def preprocess(self, frame, imgsz):
    # Resize frame (RGB assumed) to [imgsz, imgsz]
    resized = cv2.resize(frame, (imgsz, imgsz), interpolation=cv2.INTER_LINEAR)
    img = resized.astype(np.float32) / 255.0  # normalize pixel values 0-1
    img = np.transpose(img, (2, 0, 1))  # HWC to CHW
    img = np.expand_dims(img, axis=0)  # Add batch dimension
    img = np.ascontiguousarray(img)
    return img

def fast_nms(self, boxes, scores, iou_threshold=0.25):
    # Same fast_nms as your DetectionThread or shared utility
    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]

        iou = self._fast_iou(current_box, remaining_boxes)
        indices = remaining[iou <= iou_threshold]

    return keep

def _fast_iou(self, box, boxes):
    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])

    inter_area = np.maximum(0, x2 - x1) * np.maximum(0, y2 - y1)

    box_area = (box[2]-box[0]) * (box[3]-box[1])
    boxes_area = (boxes[:,2] - boxes[:,0]) * (boxes[:,3]-boxes[:,1])

    union = box_area + boxes_area - inter_area
    return inter_area / np.maximum(union, 1e-10)

def postprocess(self, outputs, conf_thres=0.3, iou_threshold=0.25, max_det=40):
    """
    Postprocess ONNX model raw output.
    Expected output shape: (batch, N, 6) where last dim [x1, y1, x2, y2, conf, class]
    """
    preds = outputs[0]  # ONNX output, shape (1, N, 6)
    preds = np.squeeze(preds, axis=0)  # (N, 6)

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

    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=int)

    boxes = preds[:, :4]
    scores = preds[:, 4]
    classes = preds[:, 5].astype(int)

    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 predict(self, frame, conf_thres=0.3):
    """
    Perform inference on a single frame with all models in ensemble.
    Returns list of dummy result objects compatible with DetectionThread.
    """
    all_results = []
    original_height, original_width = frame.shape[:2]

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

        # Preprocess image
        input_tensor = self.preprocess(frame, imgsz)

        try:
            outputs = session.run(None, {input_name: input_tensor})
            boxes, scores, classes = self.postprocess(outputs, conf_thres, iou_threshold=0.25, max_det=40)

            # Scale boxes back to original image size
            scale_x = original_width / imgsz
            scale_y = original_height / imgsz
            boxes[:, [0, 2]] *= scale_x
            boxes[:, [1, 3]] *= scale_y

            # Convert numpy arrays to torch tensors to keep interface compatibility
            boxes_torch = torch.from_numpy(boxes).float()
            conf_torch = torch.from_numpy(scores).float()
            class_torch = torch.from_numpy(classes).float()

            # Create a dummy results object with boxes attribute for DetectionThread compatibility
            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, conf_torch, class_torch)
            results = DummyResults(dummy_boxes, scale_x, scale_y, original_width, original_height)

            all_results.append(results)

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

    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
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

    model_paths = [
        ("models/yolo11n.onnx", 256),
    ]

    # ONNX + DirectML model ensemble singleton instance
    # from YOLOEnsemble import YOLOEnsemble  # adjust import as needed
    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:
                # Access torch tensors as numpy for processing
                if not hasattr(results, 'boxes') or not results.boxes.xyxy.shape[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))

            # Sort detections by confidence
            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]

            # Apply NMS with your fast_nms (optional if you trust YOLO output 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]

            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")
def clamp(value, min_val, max_val):
return min_val if value < min_val else max_val if value > max_val else value

Old one

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")