Keep how it works keep how the logic work just improve it and make it better and make it run on gpu using torch_directml

# --- YOLO SINGLETON ---
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")