Skip to content

Commit 1cc14a7

Browse files
committed
feat: add YOLO detection model (v0.5.0)
Fine-tuned YOLO11s on GroundCUA (224K tiles, 9 classes, 100 epochs). Bundled at uitag/models/yolo-ui.pt (18 MB), enabled via --yolo flag. Detection coverage on ScreenSpot-Pro (1,581 targets, 26 apps, 3 platforms): Vision + YOLO: 90.8% center-hit (text 92.7%, icon 87.6%) Vision-only: 57.3% (text 66.4%, icon 42.5%) Pipeline: uitag/yolo.py handles tiled inference (640x640, 20% overlap), cross-tile NMS, and lazy model loading. Source "yolo" at merge priority 2. Docs updated: README, performance, api, troubleshooting, research, contributing, release notes, demo GIF.
1 parent 8b78029 commit 1cc14a7

20 files changed

Lines changed: 4321 additions & 268 deletions

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ docs/f1.3-coreml-acceleration.md
3535
# Pre-compiled Swift binary (compile locally with: swiftc -O tools/vision-detect.swift -o tools/vision-detect)
3636
tools/vision-detect
3737

38-
# CoreML models (generated, large)
39-
models/
38+
# CoreML models (generated, large — root level only)
39+
/models/
4040

4141
# Environment variables
4242
.env

CONTRIBUTING.md

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
1+
---
2+
title: CONTRIBUTING
3+
type: note
4+
permalink: uitag/contributing
5+
---
6+
17
# Contributing to uitag
28

39
## Quick Start
410

511
```bash
6-
# Clone and install
12+
# Clone and install (includes YOLO dependencies)
713
git clone https://github.com/swaylenhayes/uitag.git
814
cd uitag
9-
uv pip install -e ".[dev]"
15+
uv pip install -e ".[dev,yolo]"
1016

1117
# Run tests
1218
uv run pytest
@@ -18,21 +24,28 @@ pre-commit install
1824

1925
## Architecture
2026

21-
uitag runs a 6-stage detection pipeline:
27+
uitag runs a multi-stage detection pipeline:
2228

2329
```
24-
Screenshot → [1] Apple Vision → [2] Tiling → [3] Florence-2 → [4] Merge → [5] Annotate → [6] Manifest
30+
Screenshot → [1] Apple Vision → [2] Merge → [3] OCR Correction → [4] Text Grouping → [5] Annotate → [6] Manifest
31+
↘ [opt] YOLO tiled detection ↗ (--yolo)
32+
↘ [legacy] Tiling → Florence-2 ↗ (--florence)
2533
```
2634

2735
| Module | What it does |
2836
|--------|-------------|
2937
| `uitag/vision.py` | Apple Vision via Swift subprocess (text + rectangles) |
30-
| `uitag/quadrants.py` | Object-aware image tiling (avoids splitting UI elements) |
31-
| `uitag/florence.py` | Florence-2 detection token parsing |
38+
| `uitag/yolo.py` | YOLO tiled detection (640x640 tiles, cross-tile NMS) |
3239
| `uitag/merge.py` | IoU-based deduplication with source priority |
40+
| `uitag/correct.py` | OCR correction (Cyrillic homoglyphs, invisible Unicode, NFC) |
41+
| `uitag/group.py` | Text block grouping (adjacent lines to paragraphs) |
3342
| `uitag/annotate.py` | SoM numbered overlay rendering |
3443
| `uitag/manifest.py` | JSON manifest generation |
35-
| `uitag/run.py` | Pipeline orchestrator — `run_pipeline()` is the main entry point |
44+
| `uitag/run.py` | Pipeline orchestrator |
45+
| `uitag/rescan.py` | Multi-crop OCR rescan for low-confidence text |
46+
| `uitag/filter.py` | Florence-2 detection filtering (legacy) |
47+
| `uitag/quadrants.py` | Object-aware image tiling for Florence-2 (legacy) |
48+
| `uitag/florence.py` | Florence-2 detection token parsing (legacy) |
3649
| `uitag/backends/` | `DetectionBackend` protocol + MLX/CoreML implementations |
3750

3851
## Making Changes
@@ -46,16 +59,16 @@ Screenshot → [1] Apple Vision → [2] Tiling → [3] Florence-2 → [4] Merge
4659

4760
### Test Split
4861

49-
- `uv run pytest` — fast tests only (~50 tests, no model required)
62+
- `uv run pytest` — fast tests only (134 tests, no model required)
5063
- `uv run pytest --run-slow` — includes tests that load Florence-2 (requires macOS + model download)
5164

5265
Tests marked `@pytest.mark.slow` need the Florence-2 model and a macOS system with Apple Vision. CI runs fast tests only.
5366

5467
## What's Welcome
5568

56-
- **Bug fixes**especially around edge cases in detection merging or tiling
57-
- **New backends**implement `DetectionBackend` protocol (see `uitag/backends/base.py`)
58-
- **Test coverage** — more edge cases for quadrant splitting, IoU merging
59-
- **Documentation** examples, tutorials, manifest format docs
69+
- Bug fixes, especially around detection merging, tiling, or cross-tile NMS
70+
- New detection backends (implement `DetectionBackend` protocol in `uitag/backends/base.py`)
71+
- Test coverage for edge cases in quadrant splitting, IoU merging, YOLO tile boundary handling
72+
- Documentation improvements, examples, and tutorials
6073

6174
See [open issues](https://github.com/swaylenhayes/uitag/issues) for specific ideas.

README.md

Lines changed: 104 additions & 56 deletions
Large diffs are not rendered by default.

docs/api.md

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
---
2+
title: api
3+
type: note
4+
permalink: uitag/docs/api
5+
---
6+
17
# API Reference
28

39
uitag is a Set-of-Mark detection pipeline for macOS. This reference covers the public Python API for library consumers.
@@ -31,10 +37,11 @@ result, annotated_image, manifest_json = run_pipeline(
3137
iou_threshold=0.5,
3238
recognition_level="accurate",
3339
backend=None,
40+
use_yolo=False,
3441
)
3542
```
3643

37-
Runs the full 6-stage detection pipeline: Apple Vision, quadrant split, Florence-2 inference, merge/dedup, SoM annotation, and manifest generation.
44+
Runs the detection pipeline: Apple Vision (text + rectangles), optional YOLO tiled detection (if `use_yolo=True`), optional Florence-2 inference (if `backend` provided or `--florence` used), merge/dedup, OCR correction, text block grouping, SoM annotation, and manifest generation.
3845

3946
### Parameters
4047

@@ -46,6 +53,7 @@ Runs the full 6-stage detection pipeline: Apple Vision, quadrant split, Florence
4653
| `iou_threshold` | `float` | `0.5` | IoU threshold for duplicate suppression during merge. |
4754
| `recognition_level` | `str` | `"accurate"` | Apple Vision OCR mode: `"accurate"` or `"fast"`. |
4855
| `backend` | `DetectionBackend \| None` | `None` | Florence-2 inference backend. When `None`, uses `MLXBackend`. |
56+
| `use_yolo` | `bool` | `False` | Enable YOLO tiled detection. Adds ~2s, closes icon gap. Requires `ultralytics`. |
4957

5058
### Return Value
5159

@@ -64,7 +72,7 @@ Returns a 3-tuple: `tuple[PipelineResult, Image.Image, str]`
6472
| `FileNotFoundError` | The input image does not exist, or the Swift Vision tool cannot be found. |
6573
| `RuntimeError` | The Apple Vision subprocess fails (e.g., not on macOS, corrupt image). |
6674

67-
Exceptions from Florence-2 model loading (network errors, missing dependencies) propagate directly from `mlx_vlm`.
75+
When `use_yolo=True`, a `FileNotFoundError` is raised if the YOLO model weights are not found. An `ImportError` propagates if `ultralytics` is not installed (`pip install uitag[yolo]`). Florence-2 exceptions propagate from `mlx_vlm`.
6876

6977
### Example
7078

@@ -95,22 +103,24 @@ A single detected UI element. Defined as a dataclass in `uitag/types.py`.
95103

96104
| Field | Type | Description |
97105
|-------|------|-------------|
98-
| `label` | `str` | Element label -- text content (for Vision text) or detection class name (for Florence-2). |
106+
| `label` | `str` | Element label text content (for Vision text), class name (for Florence-2), or grouped text (for text blocks). |
99107
| `x` | `int` | Left edge of bounding box in pixels. |
100108
| `y` | `int` | Top edge of bounding box in pixels. |
101109
| `width` | `int` | Bounding box width in pixels. |
102110
| `height` | `int` | Bounding box height in pixels. |
103-
| `confidence` | `float` | Detection confidence score (0.0--1.0). Florence-2 detections are always `0.5` (the model does not emit per-box scores). |
104-
| `source` | `str` | Detection source identifier. One of: `"vision_text"`, `"vision_rect"`, `"florence2"`. |
111+
| `confidence` | `float` | Detection confidence score (0.0--1.0). Vision provides real scores; YOLO provides model confidence; Florence-2 detections default to `0.5`. |
112+
| `source` | `str` | Detection source identifier. One of: `"vision_text"`, `"vision_rect"`, `"vision_text_block"`, `"yolo"`, `"florence2"`. |
105113
| `som_id` | `int \| None` | SoM marker number (1-indexed). `None` until `merge_detections()` assigns sequential IDs sorted by position (top-to-bottom, left-to-right). |
106114

107-
**Source values:**
115+
__Source values:__
108116

109117
| Value | Origin |
110118
|-------|--------|
111119
| `"vision_text"` | Apple Vision text recognition |
112120
| `"vision_rect"` | Apple Vision rectangle detection |
113-
| `"florence2"` | Florence-2 object detection via MLX |
121+
| `"vision_text_block"` | Grouped adjacent text lines (paragraph-level) |
122+
| `"yolo"` | YOLO tiled detection (only with `--yolo`) |
123+
| `"florence2"` | Florence-2 object detection via MLX (only with `--florence`, legacy) |
114124

115125
### `PipelineResult`
116126

@@ -127,18 +137,24 @@ Output of the full detection pipeline. Defined as a dataclass in `uitag/types.py
127137
| `image_height` | `int` | Height of the input image in pixels. |
128138
| `timing_ms` | `dict` | Pipeline timing breakdown. Defaults to an empty dict. |
129139

130-
**`timing_ms` keys** (populated by `run_pipeline`):
140+
__`timing_ms` keys__ (populated by `run_pipeline`):
131141

132142
| Key | Type | Description |
133143
|-----|------|-------------|
134144
| `vision_ms` | `float` | Total Apple Vision stage wall time. |
135-
| `split_x` | `int` | Vertical cut X coordinate chosen by object-aware tiling. |
136-
| `split_y` | `int` | Horizontal cut Y coordinate chosen by object-aware tiling. |
137-
| `split_x_clean` | `bool` | Whether the X split avoided all bounding boxes. |
138-
| `split_y_clean` | `bool` | Whether the Y split avoided all bounding boxes. |
139-
| `florence_total_ms` | `float` | Total Florence-2 inference time across all quadrants. |
140-
| `florence_backend` | `str` | Name of the backend used (e.g. `"mlx"`). |
141-
| `florence_per_quadrant_ms` | `list[float]` | Per-quadrant inference times (if the backend provides them). |
145+
| `yolo_ms` | `float` | Total YOLO tiled inference time (only with `--yolo`). |
146+
| `yolo_tiles` | `int` | Number of 640x640 tiles processed (only with `--yolo`). |
147+
| `yolo_raw_dets` | `int` | Raw detections before cross-tile NMS (only with `--yolo`). |
148+
| `yolo_nms_dets` | `int` | Detections after NMS (only with `--yolo`). |
149+
| `merge_ms` | `float` | Merge and deduplication stage time. |
150+
| `correct_ms` | `float` | OCR correction stage time. |
151+
| `corrections` | `int` | Number of labels corrected. |
152+
| `group_ms` | `float` | Text block grouping stage time. |
153+
| `groups_formed` | `int` | Number of text blocks formed. |
154+
| `annotate_ms` | `float` | SoM annotation rendering time. |
155+
| `manifest_ms` | `float` | JSON manifest generation time. |
156+
| `florence_total_ms` | `float` | Total Florence-2 inference time (only with `--florence`). |
157+
| `florence_backend` | `str` | Backend used for Florence-2 (only with `--florence`). |
142158

143159
---
144160

@@ -226,7 +242,7 @@ detect_on_quadrant(
226242

227243
`list[Detection]` -- Detections with coordinates translated to full-image space. All have `source="florence2"` and `confidence=0.5`.
228244

229-
**Note:** This function saves the PIL Image to a temporary file (mlx_vlm requires a file path), runs inference, then deletes the temp file. The model is lazy-loaded as a singleton on the first call to any Florence-2 detection function.
245+
This function saves the PIL Image to a temporary file (mlx_vlm requires a file path), runs inference, then deletes the temp file. The model is lazy-loaded as a singleton on the first call to any Florence-2 detection function.
230246

231247
#### Example
232248

@@ -264,7 +280,7 @@ merge_detections(
264280

265281
| Name | Type | Default | Description |
266282
|------|------|---------|-------------|
267-
| `detections` | `list[Detection]` | *(required)* | All detections from all sources (Vision + Florence-2), unmerged. |
283+
| `detections` | `list[Detection]` | *(required)* | All detections from all sources (Vision + YOLO + Florence-2), unmerged. |
268284
| `iou_threshold` | `float` | `0.5` | When two detections overlap above this IoU threshold, the lower-priority one is discarded. |
269285

270286
#### Source Priority
@@ -273,9 +289,11 @@ When overlapping detections are found, the higher-priority source is kept:
273289

274290
| Source | Priority | Rationale |
275291
|--------|----------|-----------|
276-
| `"vision_text"` | 3 (highest) | Apple Vision text is the most precise for text elements. |
277-
| `"vision_rect"` | 2 | Apple Vision rectangles are accurate but less specific. |
278-
| `"florence2"` | 1 (lowest) | Florence-2 detections fill gaps not caught by Vision. |
292+
| `"vision_text"` | 3 (highest) | Apple Vision text has OCR content — most useful for labeling. |
293+
| `"vision_text_block"` | 3 (highest) | Grouped text inherits Vision text priority. |
294+
| `"vision_rect"` | 2 | Apple Vision rectangles are accurate but lack semantic labels. |
295+
| `"yolo"` | 2 | YOLO detections have class labels (Button, Menu, etc.) but no OCR. |
296+
| `"florence2"` | 1 (lowest) | Florence-2 detections fill gaps (legacy, superseded by YOLO). |
279297

280298
#### Returns
281299

@@ -324,7 +342,7 @@ render_som(
324342

325343
#### Returns
326344

327-
`PIL.Image.Image` -- A **copy** of the input image (converted to RGB) with bounding boxes and numbered markers drawn. The original image is not modified.
345+
`PIL.Image.Image` -- A copy of the input image (converted to RGB) with bounding boxes and numbered markers drawn. The original image is not modified.
328346

329347
Colors cycle through 8 values in `SOM_COLORS`: red, green, blue, orange, purple, cyan, yellow, pink. The color for each detection is determined by `(som_id - 1) % 8`.
330348

@@ -454,7 +472,7 @@ Each entry in `elements`:
454472
| `label` | `string` | Element label (text content or detection class). |
455473
| `bbox` | `object` | Bounding box with `x`, `y`, `width`, `height` (all integers, pixels). |
456474
| `confidence` | `number` | Detection confidence score (0.0--1.0). |
457-
| `source` | `string` | Detection source: `"vision_text"`, `"vision_rect"`, or `"florence2"`. |
475+
| `source` | `string` | Detection source: `"vision_text"`, `"vision_rect"`, `"vision_text_block"`, `"yolo"`, or `"florence2"`. |
458476

459477
### Example Manifest
460478

@@ -473,16 +491,17 @@ Each entry in `elements`:
473491
},
474492
{
475493
"som_id": 2,
476-
"label": "toolbar",
477-
"bbox": { "x": 0, "y": 0, "width": 1920, "height": 40 },
478-
"confidence": 0.5,
479-
"source": "florence2"
494+
"label": "Button",
495+
"bbox": { "x": 2752, "y": 305, "width": 101, "height": 104 },
496+
"confidence": 0.87,
497+
"source": "yolo"
480498
}
481499
],
482500
"timing_ms": {
483501
"vision_ms": 980.1,
484-
"florence_total_ms": 592.4,
485-
"florence_backend": "mlx"
502+
"yolo_ms": 2150.3,
503+
"yolo_tiles": 32,
504+
"merge_ms": 3.3
486505
}
487506
}
488-
```
507+
```
355 KB
Loading

docs/examples/hero-after-yolo.png

702 KB
Loading

0 commit comments

Comments
 (0)