makeitfr commited on
Commit
25207a2
·
verified ·
1 Parent(s): 099fe70

Upload ui_element_api_server.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. ui_element_api_server.py +436 -0
ui_element_api_server.py ADDED
@@ -0,0 +1,436 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ UI Element Detection API Server
3
+ Combines OmniParser UI detection with template matching to provide
4
+ precise coordinates for all UI elements in an image.
5
+
6
+ Usage:
7
+ python ui_element_api_server.py --port 8001
8
+
9
+ Then POST a PNG image to: http://localhost:8001/analyze
10
+
11
+ Response includes:
12
+ - JSON coordinates data
13
+ - CSV format data
14
+ - Visualization PNG with bounding boxes
15
+ """
16
+
17
+ import cv2
18
+ import numpy as np
19
+ import json
20
+ import os
21
+ import sys
22
+ import io
23
+ import time
24
+ import base64
25
+ from pathlib import Path
26
+ from contextlib import asynccontextmanager
27
+ from fastapi import FastAPI, File, UploadFile, HTTPException
28
+ from fastapi.responses import JSONResponse, FileResponse
29
+ import argparse
30
+ import uvicorn
31
+ from typing import Dict, Any, Optional, Tuple
32
+ from PIL import Image
33
+ import csv
34
+ import tempfile
35
+ import threading
36
+
37
+ # Add OmniParser to path dynamically
38
+ from pathlib import Path
39
+ omoi_root = Path(__file__).parent
40
+ sys.path.insert(0, str(omoi_root / 'OmniParser'))
41
+ from util.omniparser import Omniparser
42
+ from config import get_omniparser_config
43
+
44
+ # ============ Utility Functions ============
45
+
46
+ def to_rgb(img: np.ndarray) -> Optional[np.ndarray]:
47
+ """Converts image to BGR format (3 channels)."""
48
+ if img is None:
49
+ return None
50
+ if len(img.shape) == 2:
51
+ return cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
52
+ if img.shape[2] == 4:
53
+ return cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
54
+ return img
55
+
56
+ def match_ui_elements(
57
+ original_image_array: np.ndarray,
58
+ cropped_images_dir: str,
59
+ threshold: float = 0.7
60
+ ) -> Tuple[list, Dict]:
61
+ """
62
+ Match cropped UI templates against original image.
63
+ Returns list of matches and metadata.
64
+ """
65
+ original_img_rgb = to_rgb(original_image_array)
66
+ if original_img_rgb is None:
67
+ raise ValueError("Failed to convert original image")
68
+
69
+ img_height, img_width = original_img_rgb.shape[:2]
70
+
71
+ # Load templates
72
+ templates = {}
73
+ template_files = sorted(Path(cropped_images_dir).glob('crop_*.png'))
74
+
75
+ for template_file in template_files:
76
+ template_img = cv2.imread(str(template_file), cv2.IMREAD_UNCHANGED)
77
+ if template_img is not None:
78
+ template_img_rgb = to_rgb(template_img)
79
+ templates[template_file.name] = template_img_rgb
80
+
81
+ # Match templates
82
+ matches = []
83
+ for template_name, template_img in templates.items():
84
+ try:
85
+ if template_img.shape[0] > img_height or template_img.shape[1] > img_width:
86
+ continue
87
+ if template_img.shape[0] < 4 or template_img.shape[1] < 4:
88
+ continue
89
+
90
+ result = cv2.matchTemplate(original_img_rgb, template_img, cv2.TM_CCOEFF_NORMED)
91
+ _, max_val, _, max_loc = cv2.minMaxLoc(result)
92
+
93
+ if max_val >= threshold:
94
+ template_h, template_w = template_img.shape[:2]
95
+ x1, y1 = max_loc
96
+ x2 = x1 + template_w
97
+ y2 = y1 + template_h
98
+
99
+ center_x = (x1 + x2) / 2
100
+ center_y = (y1 + y2) / 2
101
+
102
+ matches.append({
103
+ 'template_id': template_name.replace('.png', ''),
104
+ 'template_file': template_name,
105
+ 'confidence': float(max_val),
106
+ 'bbox': {
107
+ 'x1': int(x1),
108
+ 'y1': int(y1),
109
+ 'x2': int(x2),
110
+ 'y2': int(y2),
111
+ 'width': int(template_w),
112
+ 'height': int(template_h)
113
+ },
114
+ 'center': {
115
+ 'x': int(center_x),
116
+ 'y': int(center_y)
117
+ },
118
+ 'bbox_ratio': {
119
+ 'x1': x1 / img_width,
120
+ 'y1': y1 / img_height,
121
+ 'x2': x2 / img_width,
122
+ 'y2': y2 / img_height
123
+ }
124
+ })
125
+ except Exception:
126
+ continue
127
+
128
+ matches.sort(key=lambda x: x['confidence'], reverse=True)
129
+
130
+ metadata = {
131
+ 'image_size': {'width': img_width, 'height': img_height},
132
+ 'templates_loaded': len(templates),
133
+ 'threshold': threshold,
134
+ 'matches_found': len(matches)
135
+ }
136
+
137
+ return matches, metadata
138
+
139
+ def visualize_matches(
140
+ original_image_array: np.ndarray,
141
+ matches: list
142
+ ) -> np.ndarray:
143
+ """Create visualization with bounding boxes."""
144
+ img = original_image_array.copy()
145
+
146
+ for match in matches:
147
+ bbox = match['bbox']
148
+ center = match['center']
149
+ confidence = match['confidence']
150
+ template_id = match['template_id']
151
+
152
+ # Draw bounding box
153
+ color = (0, 255, 0) # Green
154
+ thickness = 2
155
+ cv2.rectangle(img, (bbox['x1'], bbox['y1']), (bbox['x2'], bbox['y2']), color, thickness)
156
+
157
+ # Draw center point
158
+ cv2.circle(img, (center['x'], center['y']), 3, (0, 0, 255), -1) # Red
159
+
160
+ # Draw label
161
+ label = f"ID:{template_id} ({confidence:.2f})"
162
+ cv2.putText(img, label, (bbox['x1'], bbox['y1'] - 5),
163
+ cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 0, 0), 1)
164
+
165
+ return img
166
+
167
+ def matches_to_csv(matches: list, image_width: int, image_height: int) -> str:
168
+ """Convert matches to CSV format (returns string)."""
169
+ output = io.StringIO()
170
+ writer = csv.writer(output)
171
+ writer.writerow([
172
+ 'Element_ID', 'Template_File', 'Confidence',
173
+ 'X1', 'Y1', 'X2', 'Y2', 'Width', 'Height',
174
+ 'Center_X', 'Center_Y',
175
+ 'Ratio_X1', 'Ratio_Y1', 'Ratio_X2', 'Ratio_Y2'
176
+ ])
177
+
178
+ for match in matches:
179
+ bbox = match['bbox']
180
+ center = match['center']
181
+ ratio = match['bbox_ratio']
182
+
183
+ writer.writerow([
184
+ match['template_id'],
185
+ match['template_file'],
186
+ f"{match['confidence']:.4f}",
187
+ bbox['x1'], bbox['y1'], bbox['x2'], bbox['y2'],
188
+ bbox['width'], bbox['height'],
189
+ center['x'], center['y'],
190
+ f"{ratio['x1']:.6f}", f"{ratio['y1']:.6f}",
191
+ f"{ratio['x2']:.6f}", f"{ratio['y2']:.6f}"
192
+ ])
193
+
194
+ return output.getvalue()
195
+
196
+ # ============ FastAPI Server ============
197
+
198
+ # Global OmniParser instance
199
+ omniparser = None
200
+ omniparser_lock = threading.Lock()
201
+
202
+ @asynccontextmanager
203
+ async def lifespan(app: FastAPI):
204
+ """Initialize and cleanup on server startup/shutdown."""
205
+ global omniparser
206
+ try:
207
+ with omniparser_lock:
208
+ config = get_omniparser_config()
209
+ omniparser = Omniparser(config)
210
+ print("[Server] OmniParser initialized successfully")
211
+ except Exception as e:
212
+ print(f"[ERROR] Failed to initialize OmniParser: {str(e)}")
213
+ import traceback
214
+ traceback.print_exc()
215
+
216
+ yield # Application runs here
217
+
218
+ # Cleanup (if any)
219
+ print("[Server] Shutting down...")
220
+
221
+ app = FastAPI(
222
+ title="UI Element Detection API",
223
+ description="Detects and locates all UI elements in screenshots",
224
+ lifespan=lifespan
225
+ )
226
+
227
+ @app.get("/health")
228
+ async def health():
229
+ """Health check endpoint."""
230
+ return {"status": "ok", "service": "UI Element Detection API"}
231
+
232
+ @app.post("/analyze")
233
+ async def analyze_image(file: UploadFile = File(...)):
234
+ """
235
+ Analyze an image for UI elements.
236
+
237
+ Returns:
238
+ JSON response with coordinates, CSV data, and base64-encoded visualization
239
+ """
240
+
241
+ if not omniparser:
242
+ raise HTTPException(status_code=503, detail="OmniParser not initialized")
243
+
244
+ try:
245
+ print(f"\n[Analysis] Starting analysis for: {file.filename}")
246
+ start_time = time.time()
247
+
248
+ # 1. Read and decode image
249
+ print("[Step 1] Reading image file...")
250
+ content = await file.read()
251
+ np_array = np.frombuffer(content, np.uint8)
252
+ original_img = cv2.imdecode(np_array, cv2.IMREAD_UNCHANGED)
253
+
254
+ if original_img is None:
255
+ raise HTTPException(status_code=400, detail="Failed to decode image")
256
+
257
+ print(f"[Step 1] Image loaded: {original_img.shape}")
258
+
259
+ # 2. Encode for OmniParser
260
+ print("[Step 2] Encoding for OmniParser...")
261
+ _, buffer = cv2.imencode('.png', original_img)
262
+ image_base64 = base64.b64encode(buffer).decode()
263
+
264
+ # 3. Run OmniParser
265
+ print("[Step 3] Running OmniParser detection...")
266
+ omni_time = time.time()
267
+ _, parsed_content = omniparser.parse(image_base64)
268
+ omni_elapsed = time.time() - omni_time
269
+ print(f"[Step 3] OmniParser complete in {omni_elapsed:.2f}s")
270
+
271
+ # 4. Get cropped images directory
272
+ cropped_dir = '/tmp/omoi_cropped_images'
273
+ if not Path(cropped_dir).exists():
274
+ raise HTTPException(status_code=500, detail="Cropped images directory not found")
275
+
276
+ # 5. Match UI elements
277
+ print("[Step 4] Matching templates...")
278
+ match_time = time.time()
279
+ matches, metadata = match_ui_elements(original_img, cropped_dir, threshold=0.7)
280
+ match_elapsed = time.time() - match_time
281
+ print(f"[Step 4] Matching complete in {match_elapsed:.2f}s - Found {len(matches)} elements")
282
+
283
+ # 6. Create visualization
284
+ print("[Step 5] Creating visualization...")
285
+ viz_img = visualize_matches(original_img, matches)
286
+ _, viz_buffer = cv2.imencode('.png', viz_img)
287
+ viz_base64 = base64.b64encode(viz_buffer).decode()
288
+
289
+ # 7. Generate CSV
290
+ print("[Step 6] Generating CSV...")
291
+ csv_data = matches_to_csv(matches, metadata['image_size']['width'], metadata['image_size']['height'])
292
+
293
+ # 8. Prepare response
294
+ print("[Step 7] Preparing response...")
295
+ response_data = {
296
+ 'status': 'success',
297
+ 'processing_time_seconds': time.time() - start_time,
298
+ 'timing': {
299
+ 'omniparser_seconds': omni_elapsed,
300
+ 'template_matching_seconds': match_elapsed
301
+ },
302
+ 'image_info': {
303
+ 'filename': file.filename,
304
+ 'size': metadata['image_size']
305
+ },
306
+ 'analysis': {
307
+ 'total_elements_detected': len(matches),
308
+ 'elements': matches
309
+ },
310
+ 'exports': {
311
+ 'csv_data': csv_data,
312
+ 'visualization_png_base64': viz_base64
313
+ }
314
+ }
315
+
316
+ total_time = time.time() - start_time
317
+ print(f"[Analysis] Complete in {total_time:.2f}s")
318
+
319
+ return JSONResponse(content=response_data)
320
+
321
+ except HTTPException:
322
+ raise
323
+ except Exception as e:
324
+ print(f"[ERROR] Analysis failed: {str(e)}")
325
+ import traceback
326
+ traceback.print_exc()
327
+ raise HTTPException(status_code=500, detail=f"Analysis failed: {str(e)}")
328
+
329
+ @app.post("/analyze_batch")
330
+ async def analyze_batch(file: UploadFile = File(...)):
331
+ """
332
+ Analyze image and return as separate parts for easier client handling.
333
+
334
+ Returns:
335
+ {
336
+ 'metadata': analysis metadata,
337
+ 'coordinates_json': full coordinates data,
338
+ 'csv_data': CSV string,
339
+ 'visualization_png_base64': visualization image
340
+ }
341
+ """
342
+
343
+ if not omniparser:
344
+ raise HTTPException(status_code=503, detail="OmniParser not initialized")
345
+
346
+ try:
347
+ print(f"\n[Batch Analysis] Starting for: {file.filename}")
348
+
349
+ # Read image
350
+ content = await file.read()
351
+ np_array = np.frombuffer(content, np.uint8)
352
+ original_img = cv2.imdecode(np_array, cv2.IMREAD_UNCHANGED)
353
+
354
+ if original_img is None:
355
+ raise HTTPException(status_code=400, detail="Failed to decode image")
356
+
357
+ # Run OmniParser
358
+ image_base64 = base64.b64encode(cv2.imencode('.png', original_img)[1]).decode()
359
+ _, parsed_content = omniparser.parse(image_base64)
360
+
361
+ # Match templates
362
+ cropped_dir = '/tmp/omoi_cropped_images'
363
+ matches, metadata = match_ui_elements(original_img, cropped_dir, threshold=0.7)
364
+
365
+ # Create visualization
366
+ viz_img = visualize_matches(original_img, matches)
367
+ _, viz_buffer = cv2.imencode('.png', viz_img)
368
+ viz_base64 = base64.b64encode(viz_buffer).decode()
369
+
370
+ # CSV data
371
+ csv_data = matches_to_csv(matches, metadata['image_size']['width'], metadata['image_size']['height'])
372
+
373
+ # Create JSON structure
374
+ coordinates_json = {
375
+ 'source_image': file.filename,
376
+ 'image_size': metadata['image_size'],
377
+ 'total_elements': len(matches),
378
+ 'elements': matches
379
+ }
380
+
381
+ return JSONResponse(content={
382
+ 'metadata': {
383
+ 'filename': file.filename,
384
+ 'image_size': metadata['image_size'],
385
+ 'total_elements_detected': len(matches),
386
+ 'templates_loaded': metadata['templates_loaded']
387
+ },
388
+ 'coordinates_json': coordinates_json,
389
+ 'csv_data': csv_data,
390
+ 'visualization_png_base64': viz_base64
391
+ })
392
+
393
+ except Exception as e:
394
+ print(f"[ERROR] Batch analysis failed: {str(e)}")
395
+ raise HTTPException(status_code=500, detail=str(e))
396
+
397
+ if __name__ == "__main__":
398
+ import multiprocessing
399
+
400
+ parser = argparse.ArgumentParser(description='UI Element Detection API Server')
401
+ parser.add_argument('--host', type=str, default='127.0.0.1', help='Host to bind to')
402
+ parser.add_argument('--port', type=int, default=8001, help='Port to listen on')
403
+ parser.add_argument('--reload', action='store_true', help='Enable auto-reload')
404
+ parser.add_argument('--workers', type=int, default=1, help='Number of worker processes (default: 1, use for production with module import)')
405
+ args = parser.parse_args()
406
+
407
+ # Get CPU count for reference
408
+ cpu_count = multiprocessing.cpu_count()
409
+
410
+ print(f"\n{'='*70}")
411
+ print("UI Element Detection API Server - Optimized")
412
+ print(f"{'='*70}")
413
+ print(f"Starting server on http://{args.host}:{args.port}")
414
+ print(f"CPU Cores Available: {cpu_count}")
415
+ print(f"Workers: {args.workers} (direct mode - async concurrency enabled)")
416
+ print(f"\nEndpoints:")
417
+ print(f" POST /analyze - Analyze image with details")
418
+ print(f" POST /analyze_batch - Analyze image with structured response")
419
+ print(f" GET /health - Health check")
420
+ print(f"{'='*70}\n")
421
+
422
+ try:
423
+ # Run with async concurrency instead of multiple workers for direct instantiation
424
+ uvicorn.run(
425
+ app,
426
+ host=args.host,
427
+ port=args.port,
428
+ reload=args.reload,
429
+ loop="auto"
430
+ )
431
+ except KeyboardInterrupt:
432
+ print("\n[Server] Shutting down...")
433
+ except Exception as e:
434
+ print(f"\n[ERROR] Server error: {str(e)}")
435
+ import traceback
436
+ traceback.print_exc()