#!/usr/bin/env python3 """ OmniParser UI Element Detection - FastAPI Server for HF Spaces Full REST API + Web UI served on port 7860 Features: - /api/analyze - POST image for UI element detection - /api/health - Health check - / - HTML web interface - Automatic model initialization on startup - CORS enabled for cross-origin requests """ import os import sys import json import time import base64 import cv2 import numpy as np import io import csv from pathlib import Path from typing import Dict, Any, Optional, Tuple, List from contextlib import asynccontextmanager import threading from fastapi import FastAPI, File, UploadFile, HTTPException, Request from fastapi.responses import JSONResponse, HTMLResponse, FileResponse from fastapi.staticfiles import StaticFiles from fastapi.middleware.cors import CORSMiddleware import uvicorn from PIL import Image # Configure OmniParser os.environ["OMP_NUM_THREADS"] = "4" # Add OmniParser to path dynamically omoi_root = Path(__file__).parent sys.path.insert(0, str(omoi_root / 'OmniParser')) from util.omniparser import Omniparser from config import get_omniparser_config # ============ Utility Functions ============ def to_rgb(img: np.ndarray) -> Optional[np.ndarray]: """Convert image to BGR format.""" if img is None: return None if len(img.shape) == 2: return cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) if img.shape[2] == 4: return cv2.cvtColor(img, cv2.COLOR_BGRA2BGR) return img def extract_coordinates(matches: List[Dict]) -> List[Dict]: """Extract coordinates from matches for JSON export.""" coords = [] for i, match in enumerate(matches, 1): bbox = match['bbox'] center = match['center'] coords.append({ 'element_id': f"crop_{i:04d}", 'x': center['x'], 'y': center['y'], 'x1': bbox['x1'], 'y1': bbox['y1'], 'x2': bbox['x2'], 'y2': bbox['y2'], 'width': bbox['width'], 'height': bbox['height'], 'confidence': match['confidence'], 'template_file': match['template_file'] }) return coords def match_ui_elements( original_image_array: np.ndarray, cropped_images_dir: str, threshold: float = 0.7 ) -> Tuple[list, Dict]: """Match cropped UI templates against original image.""" original_img_rgb = to_rgb(original_image_array) if original_img_rgb is None: raise ValueError("Failed to convert original image") img_height, img_width = original_img_rgb.shape[:2] # Load templates templates = {} template_files = sorted(Path(cropped_images_dir).glob('crop_*.png')) for template_file in template_files: template_img = cv2.imread(str(template_file), cv2.IMREAD_UNCHANGED) if template_img is not None: template_img_rgb = to_rgb(template_img) templates[template_file.name] = template_img_rgb # Match templates matches = [] for template_name, template_img in templates.items(): try: if template_img.shape[0] > img_height or template_img.shape[1] > img_width: continue if template_img.shape[0] < 4 or template_img.shape[1] < 4: continue result = cv2.matchTemplate(original_img_rgb, template_img, cv2.TM_CCOEFF_NORMED) _, max_val, _, max_loc = cv2.minMaxLoc(result) if max_val >= threshold: template_h, template_w = template_img.shape[:2] x1, y1 = max_loc x2 = x1 + template_w y2 = y1 + template_h center_x = (x1 + x2) / 2 center_y = (y1 + y2) / 2 matches.append({ 'template_id': template_name.replace('.png', ''), 'template_file': template_name, 'confidence': float(max_val), 'bbox': { 'x1': int(x1), 'y1': int(y1), 'x2': int(x2), 'y2': int(y2), 'width': int(template_w), 'height': int(template_h) }, 'center': { 'x': int(center_x), 'y': int(center_y) } }) except Exception: continue matches.sort(key=lambda x: x['confidence'], reverse=True) metadata = { 'image_size': {'width': img_width, 'height': img_height}, 'templates_loaded': len(templates), 'threshold': threshold, 'matches_found': len(matches) } return matches, metadata def visualize_matches( original_image_array: np.ndarray, matches: list ) -> np.ndarray: """Create visualization with bounding boxes.""" img = original_image_array.copy() for match in matches: bbox = match['bbox'] center = match['center'] confidence = match['confidence'] template_id = match['template_id'] # Draw bounding box color = (0, 255, 0) # Green thickness = 2 cv2.rectangle(img, (bbox['x1'], bbox['y1']), (bbox['x2'], bbox['y2']), color, thickness) # Draw center point cv2.circle(img, (center['x'], center['y']), 3, (0, 0, 255), -1) # Red # Draw label label = f"{template_id} ({confidence:.2f})" cv2.putText(img, label, (bbox['x1'], bbox['y1'] - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 0, 0), 1) return img def matches_to_csv(matches: list) -> str: """Convert matches to CSV format.""" output = io.StringIO() writer = csv.writer(output) writer.writerow([ 'Element_ID', 'X', 'Y', 'X1', 'Y1', 'X2', 'Y2', 'Width', 'Height', 'Confidence' ]) for i, match in enumerate(matches, 1): bbox = match['bbox'] center = match['center'] writer.writerow([ f"crop_{i:04d}", center['x'], center['y'], bbox['x1'], bbox['y1'], bbox['x2'], bbox['y2'], bbox['width'], bbox['height'], f"{match['confidence']:.4f}" ]) return output.getvalue() # ============ FastAPI Setup ============ # Global OmniParser instance omniparser = None omniparser_lock = threading.Lock() @asynccontextmanager async def lifespan(app: FastAPI): """Initialize and cleanup on server startup/shutdown.""" global omniparser print("\n" + "="*60) print("š Initializing OmniParser...") print("="*60) try: with omniparser_lock: config = get_omniparser_config() print(f"ā Config loaded from: {config['omniparser_dir']}") print(f"ā Loading YOLO model...") omniparser = Omniparser(config) print(f"ā OmniParser initialized successfully!") print("="*60 + "\n") except Exception as e: print(f"ā ERROR during initialization: {str(e)}") import traceback traceback.print_exc() print("="*60 + "\n") yield # Application runs here # Cleanup print("\n[Server] Shutting down...") # Create FastAPI app app = FastAPI( title="OmniParser UI Detection API", description="Detects and locates UI elements in screenshots", version="1.0.0", lifespan=lifespan ) # Add CORS middleware app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # ============ Web Interface ============ HTML_UI = """
Upload a UI screenshot to detect and locate all UI elements
Processing image... (this may take 30-60 seconds)