Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
3e82dba
Migrate to PyQt6 and run SAM/DINO inference in-process (#4)
cofade May 22, 2026
687612e
refactor: Reorganize package into responsibility-focused subpackages …
claude May 22, 2026
df7ed79
refactor: Extract theme, I/O wrappers, and image utils from ImageAnno…
claude May 22, 2026
8562616
refactor: Extract ProjectController from ImageAnnotator (Phase 3a)
claude May 22, 2026
857ea48
refactor: Extract ImageController from ImageAnnotator (Phase 3b)
claude May 22, 2026
794f4c7
refactor: Extract SAMController from ImageAnnotator (Phase 4a)
claude May 22, 2026
0a91a07
refactor: Extract DINOController from ImageAnnotator (Phase 4b)
claude May 22, 2026
10d6495
refactor: Extract YOLOController from ImageAnnotator (Phase 4c)
claude May 22, 2026
8f2558e
refactor: Extract AnnotationController from ImageAnnotator (Phase 5a)
claude May 22, 2026
fd9372f
refactor: Extract ClassController from ImageAnnotator (Phase 5b)
claude May 22, 2026
a5d33d5
refactor: Decouple ImageLabel from ImageAnnotator via Qt signals (Pha…
claude May 22, 2026
b415c61
docs+refactor: Address Phase 6 senior-reviewer findings
claude May 22, 2026
dc7f7a8
refactor: Extract per-tool handlers from ImageLabel (Phase 7)
claude May 22, 2026
c9d5f65
docs+refactor: Address Phase 7 senior-reviewer findings
claude May 22, 2026
6019789
refactor: Extract UI assembly into ui/menu_bar + ui/sidebar (Phase 8)
claude May 22, 2026
590e3be
docs+refactor: Address Phase 8 senior-reviewer findings
claude May 22, 2026
80c2afb
docs(phase9): Refresh CLAUDE.md + arc42 to reflect the post-refactor …
claude May 22, 2026
418814f
docs: Address Phase 9 senior-reviewer nits
claude May 22, 2026
e788a50
fix: Address three QA issues from PR #5 manual testing
Jun 10, 2026
a8806d0
fix: Remove hardcoded colors from DINO phrase panel labels (P1 review)
Jun 10, 2026
40210c1
refactor: Phase 1 β€” reorganize package into responsibility-focused su…
cofade Jun 10, 2026
133bf66
Merge pull request #6 from cofade/claude/modular-refactoring-phase2
cofade Jun 10, 2026
9f884e2
Merge pull request #7 from cofade/claude/modular-refactoring-phase3
cofade Jun 10, 2026
37c6156
Merge pull request #8 from cofade/claude/modular-refactoring-phase3b
cofade Jun 10, 2026
4145394
Merge pull request #9 from cofade/claude/modular-refactoring-phase4a
cofade Jun 10, 2026
0a6b8ec
Merge remote-tracking branch 'origin/master' into claude/modular-refa…
Jun 10, 2026
79030a3
Merge pull request #10 from cofade/claude/modular-refactoring-phase4b
cofade Jun 10, 2026
e7d5a6b
Merge remote-tracking branch 'origin/master' into claude/modular-refa…
Jun 10, 2026
4ffbc6c
Merge pull request #11 from cofade/claude/modular-refactoring-phase4c
cofade Jun 10, 2026
ec69507
Merge remote-tracking branch 'origin/master' into claude/modular-refa…
Jun 10, 2026
4111738
Merge pull request #12 from cofade/claude/modular-refactoring-phase5a
cofade Jun 10, 2026
d056c5b
Merge remote-tracking branch 'origin/master' into claude/modular-refa…
Jun 10, 2026
d778d41
Merge pull request #13 from cofade/claude/modular-refactoring-phase5b
cofade Jun 10, 2026
30d34fb
Merge remote-tracking branch 'origin/master' into claude/modular-refa…
Jun 10, 2026
48e7786
Merge pull request #14 from cofade/claude/modular-refactoring-phase6
cofade Jun 10, 2026
2b37389
Merge remote-tracking branch 'origin/master' into claude/modular-refa…
Jun 10, 2026
8404928
Merge pull request #15 from cofade/claude/modular-refactoring-phase7
cofade Jun 10, 2026
b8c7934
Merge remote-tracking branch 'origin/master' into claude/modular-refa…
Jun 10, 2026
4cc7432
Merge pull request #16 from cofade/claude/modular-refactoring-phase8
cofade Jun 10, 2026
7949ee6
Merge remote-tracking branch 'origin/master' into claude/modular-refa…
Jun 10, 2026
191ccba
fix: Address senior reviewer feedback β€” ADR numbering, dead code, naming
Jun 10, 2026
2e962e0
fix: Replace remaining dead singular highlighted_annotation in delete…
Jun 10, 2026
99905ce
fix: Prevent dataset splitter crash on missing source images
cofade Jun 10, 2026
11fca02
refactor: Remove legacy SAM Magic Wand tool
cofade Jun 10, 2026
a9539c2
Merge pull request #17 from cofade/claude/modular-refactoring-phase9
cofade Jun 10, 2026
9f7bb49
feat: Low-vision mode β€” continuous UI font zoom with persistence
cofade Jun 10, 2026
f0b1879
fix: Scale the compact DINO config panel with the UI font zoom
cofade Jun 11, 2026
a34a299
Merge pull request #18 from cofade/feature/low-vision-ui-zoom
cofade Jun 11, 2026
54ea6fe
Merge upstream master (content already incorporated)
cofade Jun 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 48 additions & 22 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ python -m src.digitalsreeni_image_annotator.main

Python 3.10+ | PyQt6 6.7+ | Ultralytics 8.3.27 (SAM 2) | NumPy | OpenCV | Shapely

**Test suite**: `tests/` (pytest + pytest-qt). 65 tests pass on PyQt6.
**Test suite**: `tests/` (pytest + pytest-qt). 94 tests pass on PyQt6.

## Documentation

Expand All @@ -40,23 +40,44 @@ See [docs/README.md](docs/README.md) for full documentation index.

```
src/digitalsreeni_image_annotator/
β”œβ”€β”€ main.py # Entry point
β”œβ”€β”€ annotator_window.py # ImageAnnotator - main window, project state
β”œβ”€β”€ image_label.py # ImageLabel - display, mouse events, rendering
β”œβ”€β”€ sam_utils.py # SAMUtils - SAM model management
β”œβ”€β”€ utils.py # Utility functions
β”œβ”€β”€ export_formats.py # COCO, YOLO, Pascal VOC exporters
β”œβ”€β”€ import_formats.py # COCO, YOLO importers
└── [tool dialogs] # Standalone utility windows
β”œβ”€β”€ main.py # Entry point
β”œβ”€β”€ annotator_window.py # ImageAnnotator - thin orchestrator
β”œβ”€β”€ app_settings.py # QSettings UI prefs: ui_font_pt, dark_mode (ADR-020)
β”œβ”€β”€ utils.py # Utility functions (calculate_area, …)
β”œβ”€β”€ __init__.py # Public API re-exports
β”‚
β”œβ”€β”€ core/ # constants, annotation_utils, image_utils
β”œβ”€β”€ controllers/ # 7 controllers (project, image, sam, dino,
β”‚ # yolo, annotation, class) + io_controller
β”œβ”€β”€ widgets/
β”‚ β”œβ”€β”€ image_label.py # ImageLabel canvas widget (dispatcher)
β”‚ β”œβ”€β”€ canvas_context.py # CanvasContext read accessor (ADR-018)
β”‚ └── tools/ # Per-tool handlers (ADR-019): rectangle,
β”‚ # polygon, paint, eraser
β”œβ”€β”€ inference/ # sam_utils.py, dino_utils.py
β”œβ”€β”€ io/ # export_formats.py, import_formats.py
β”œβ”€β”€ ui/ # menu_bar, sidebar, shortcuts, theme, stylesheets
└── dialogs/ # Standalone tool dialogs (statistics,
# splitter, augmenter, … 16 files)
```

## Key Classes

| Class | File | Responsibility |
|-------|------|----------------|
| `ImageAnnotator` | annotator_window.py | Main window, state (`all_annotations`, `class_mapping`, etc.) |
| `ImageLabel` | image_label.py | Image display, zoom/pan, annotation interaction |
| `SAMUtils` | sam_utils.py | Load SAM models, run inference |
| `ImageAnnotator` | annotator_window.py | Thin orchestrator β€” holds controllers, wires signals, delegates almost everything |
| `ImageLabel` | widgets/image_label.py | Canvas display, zoom/pan, event dispatch to tool handlers |
| `CanvasContext` | widgets/canvas_context.py | Narrow read view of main-window state for ImageLabel (ADR-018) |
| `ToolHandler` (+ 4 subclasses) | widgets/tools/ | Per-tool mouse/key handling (rectangle, polygon, paint, eraser) (ADR-019) |
| `ProjectController` | controllers/project_controller.py | `.iap` save/load, auto-save, `is_loading_project` guard |
| `ImageController` | controllers/image_controller.py | TIFF/CZI loading, multi-dim slicing, image/slice switching |
| `AnnotationController` | controllers/annotation_controller.py | Annotation CRUD, sort, edit-mode, finish_polygon/rectangle |
| `ClassController` | controllers/class_controller.py | Class add/delete/rename/colour/visibility |
| `SAMController` | controllers/sam_controller.py | SAM model picker, debounce, in-flight guard (ADR-013) |
| `DINOController` | controllers/dino_controller.py | DINO single/batch detection, batch review, temp-class workflow |
| `YOLOController` | controllers/yolo_controller.py | YOLO training menu + prediction wiring |
| `SAMUtils` | inference/sam_utils.py | Load SAM models, run inference |
| `DINOUtils` | inference/dino_utils.py | Grounding-DINO model load + inference |

See [Building Block View](docs/05_building_block_view.md) for detailed class documentation.

Expand All @@ -68,7 +89,9 @@ See [Building Block View](docs/05_building_block_view.md) for detailed class doc
2. Set `image_label.current_tool` on click
3. Handle mouse events in `ImageLabel` (mousePressEvent, mouseMoveEvent)
4. Render in `ImageLabel.paintEvent()`
5. Call `main_window.add_annotation()` to commit
5. Commit via `self.annotationCommitted.emit(annotation_dict)` β€” the
orchestrator routes it to `AnnotationController.add_annotation_to_list`
(see ADR-018)

### Working with Annotations

Expand Down Expand Up @@ -141,7 +164,7 @@ See [Runtime View](docs/06_runtime_view.md#multi-dimensional-image-loading) for
| Dark mode contrast | No hardcoded `background:` / `color:` in widget `setStyleSheet(...)` | Hardcoded greys override `soft_dark_stylesheet.py` and punch bright boxes into the sidebar. Add a global rule first, then write the widget. See [No Hardcoded Colors Rule](docs/08_crosscutting_concepts.md#dark-mode--no-hardcoded-colors-rule). |
| DINO review state | `image_label.temp_annotations` is a single field, **not** per-image β€” must be re-synced from `dino_batch_results` on every image/slice switch via `_refresh_dino_temp_for_current` | Otherwise the first image's masks bleed onto every subsequent slice during navigation. See [DINO Temp Annotations](docs/08_crosscutting_concepts.md#dino-temp-annotations--single-field-many-images). |
| DINO batch over stacks | Use `_collect_dino_batch_work_items()` to flatten regular images + every loaded slice; don't iterate `self.all_images` directly | Multi-dim images appear in `all_images` as a single entry β€” slices live in `self.image_slices[base_name]` and were silently skipped. |
| DINO Enter/Escape during review | Application-wide `_DINOReviewEventFilter`, gated on pending temp_annotations + no modal + no text input | `QListWidget` consumes Enter for `itemActivated` before `ImageLabel.keyPressEvent` sees it. See [ADR-015](docs/09_architecture_decisions.md#adr-015-application-wide-event-filter-for-dino-review-shortcuts). |
| DINO Enter/Escape during review | Application-wide `DINOReviewEventFilter`, gated on pending temp_annotations + no modal + no text input | `QListWidget` consumes Enter for `itemActivated` before `ImageLabel.keyPressEvent` sees it. See [ADR-015](docs/09_architecture_decisions.md#adr-015-application-wide-event-filter-for-dino-review-shortcuts). |
| Auto-accept dropdown | Honored by **both** `run_dino_detection_single` and `run_dino_detection_batch` | Easy to forget in the single path because the combo is labeled "batch". |
| GPU model unload | `model.cpu()` β†’ `gc.collect()` β†’ `torch.cuda.empty_cache()` + `ipc_collect()` + `synchronize()` β€” full reclaim requires app restart due to per-process CUDA context | Setting refs to None alone leaves circular refs pinned and shows zero Task Manager drop. See [Releasing Model GPU Memory](docs/08_crosscutting_concepts.md#releasing-model-gpu-memory). |
| Export image-path lookup | Exact-key match first, substring fallback only | `"bee.jpg" in "honeybee.jpg"` is True β€” substring-only matching writes the wrong file. See [Export Format Filename Matching](docs/08_crosscutting_concepts.md#export-format-filename-matching). |
Expand All @@ -161,17 +184,18 @@ See [Runtime View](docs/06_runtime_view.md#multi-dimensional-image-loading) for
| 6 | Commit: `feat: Description` or `fix: Description` | Clear, descriptive messages |
| 7 | Push & create PR | `git push origin feature/branch` |

### Testing Checklist (Manual β€” No Automated Tests)
### Testing Checklist

Before opening a PR, verify at minimum:

1. **Launch the app** β€” no import errors, main window renders
2. **Golden path** β€” perform the new feature's primary workflow end-to-end
3. **Edge cases** β€” empty state, cancel/escape, large images, missing model files
4. **Dark mode** β€” toggle and check rendering of new UI elements
5. **Save/load roundtrip** β€” if the feature touches `.iap` project files, save, close, reopen, verify state restored
6. **Adjacent features** β€” verify no regression in SAM, annotation tools, export formats
7. **Inference features** β€” if touching `sam_utils.py` or `dino_utils.py`, verify the model loads end-to-end (no silent load failure), returns masks/boxes, and the UI stays responsive during inference (timers, redraws, progress dialog cancels keep firing β€” see ADR-013)
1. **Smoke tests pass** β€” `pytest tests/integration/test_smoke.py -v`. This includes the AST-based `test_annotator_window_inline_imports_are_resolvable` which catches stale relative imports inside function bodies after any module move (see ADR-016). A launch that "looks clean" is NOT sufficient β€” inline imports fail only when the function is called at runtime.
2. **Launch the app** β€” no import errors, main window renders
3. **Golden path** β€” perform the new feature's primary workflow end-to-end
4. **Edge cases** β€” empty state, cancel/escape, large images, missing model files
5. **Dark mode** β€” toggle and check rendering of new UI elements
6. **Save/load roundtrip** β€” if the feature touches `.iap` project files, save, close, reopen, verify state restored
7. **Adjacent features** β€” verify no regression in SAM, annotation tools, export formats
8. **Inference features** β€” if touching `sam_utils.py` or `dino_utils.py`, verify the model loads end-to-end (no silent load failure), returns masks/boxes, and the UI stays responsive during inference (timers, redraws, progress dialog cancels keep firing β€” see ADR-013)

### arc42 Documentation Update Rules

Expand Down Expand Up @@ -213,6 +237,8 @@ See [Risks and Technical Debt](docs/11_risks_and_technical_debt.md) for full lis
| Global | Action |
|--------|--------|
| Ctrl+N/O/S | New/Open/Save Project |
| Ctrl+Shift+= / Ctrl+Shift+- | UI font bigger/smaller (8-24pt, persisted via QSettings) |
| Ctrl+Shift+0 | Reset UI font size |
| F1 | Help |

| Canvas | Action |
Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,8 @@ You should see `True` and your GPU name. For other platforms or driver combinati
- To use SAM2-assisted annotation:
- Select a model from the "Pick a SAM Model" dropdown. It's recommended to use smaller models like SAM2 tiny or SAM2 small. SAM2 large is not recommended as it may crash the application on systems with limited resources.
- Note: When you select a model for the first time, the application needs to download it. This process may take a few seconds to a minute, depending on your internet connection speed. Subsequent uses of the same model will be faster as it will already be cached locally, in your working directory.
- Click the "SAM-Assisted" button to activate the tool.
- Draw a rectangle around objects of interest to allow SAM2 to automatically detect objects.
- Note that SAM2 provides various outputs with different scores, and only the top-scoring region will be displayed. If the desired result isn't achieved on the first try, draw again.
- Click the "SAM-box" button and draw a rectangle around an object of interest, or click the "SAM-points" button and left-click points inside the object (right-click adds negative points to exclude regions).
- SAM2 displays the top-scoring mask as a temporary prediction β€” press Enter to accept it or Esc to discard it. If the desired result isn't achieved on the first try, draw the box again or adjust the points.
- For low-quality images where SAM2 may not auto-detect objects, manual tools may be necessary.
- When SAM2 auto-detect partial objects, use polygon or paint brush tools to manually define the remaining region and use the Merge tool to combine both annotations into one.
- When SAM2 over-annotates objects, extending the annotation beyond object's boundaries, use the Eraser tool to clean up the edges.
Expand Down
103 changes: 89 additions & 14 deletions docs/05_building_block_view.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,32 @@
```
src/digitalsreeni_image_annotator/
β”œβ”€β”€ main.py # Entry point, initializes QApplication
β”œβ”€β”€ annotator_window.py # ImageAnnotator - main window
β”œβ”€β”€ image_label.py # ImageLabel - custom display widget
β”œβ”€β”€ sam_utils.py # SAMUtils - SAM model management
└── utils.py # Utility functions
β”œβ”€β”€ annotator_window.py # ImageAnnotator - main window orchestrator
β”œβ”€β”€ app_settings.py # QSettings-backed UI prefs (font size, dark mode) β€” ADR-020
β”œβ”€β”€ utils.py # Cross-cutting utilities
β”œβ”€β”€ core/ # Constants, annotation utils, image utils
β”‚ β”œβ”€β”€ constants.py
β”‚ └── annotation_utils.py
β”œβ”€β”€ widgets/
β”‚ β”œβ”€β”€ image_label.py # ImageLabel - canvas widget; dispatcher
β”‚ β”œβ”€β”€ canvas_context.py # CanvasContext - narrow read view (ADR-018)
β”‚ └── tools/ # Per-tool handlers (ADR-019)
β”‚ β”œβ”€β”€ base.py # ToolHandler base
β”‚ β”œβ”€β”€ rectangle_tool.py
β”‚ β”œβ”€β”€ polygon_tool.py
β”‚ β”œβ”€β”€ paint_tool.py
β”‚ └── eraser_tool.py
β”œβ”€β”€ controllers/ # Project/Image/SAM/DINO/YOLO/Annotation/Class
β”œβ”€β”€ inference/ # sam_utils.py, dino_utils.py
β”‚ β”œβ”€β”€ sam_utils.py
β”‚ └── dino_utils.py
β”œβ”€β”€ io/ # export_formats.py, import_formats.py
β”‚ β”œβ”€β”€ export_formats.py
β”‚ └── import_formats.py
β”œβ”€β”€ ui/ # menu_bar, sidebar, theme, stylesheets
β”‚ β”œβ”€β”€ default_stylesheet.py
β”‚ └── soft_dark_stylesheet.py
└── dialogs/ # Standalone tool dialogs
```

### ImageAnnotator (annotator_window.py)
Expand All @@ -56,27 +78,51 @@ current_slice: str # Currently displayed slice
- `export_annotations()`: Export to various formats
- `import_annotations()`: Import from COCO/YOLO

### ImageLabel (image_label.py)
### ImageLabel (widgets/image_label.py)

**Responsibility**: Image display and annotation interaction
**Responsibility**: Canvas widget β€” image display, navigation
(zoom/pan), committed-annotation rendering, SAM bbox/points overlays,
DINO temp-annotation rendering, polygon edit mode (modal). Per-tool
mouse/key handling lives in `widgets/tools/*` (see ADR-019); ImageLabel
dispatches events to the active handler.

**Key Attributes**:
```python
current_tool: str # Active annotation tool
current_tool: str # Active annotation tool (route via set_active_tool)
zoom_factor: float # Current zoom level
annotations: dict # Displayed annotations
class_colors: dict # Class color mapping
temp_paint_mask: np.ndarray # Temporary paint strokes
temp_paint_mask: np.ndarray # In-progress paint stroke (owned by PaintBrushTool)
temp_eraser_mask: np.ndarray # In-progress eraser stroke (owned by EraserTool)
current_rectangle: list # In-progress rectangle (owned by RectangleTool)
current_annotation: list # In-progress polygon points (owned by PolygonTool)
sam_positive_points: list # SAM positive points
sam_negative_points: list # SAM negative points
editing_polygon: dict | None # Polygon being edited (modal sub-state)
_tools: dict[str, ToolHandler] # Per-tool handlers
_ctx: CanvasContext # Narrow read view of main-window state (ADR-018)
```

**Key Methods**:
- `mousePressEvent()`: Handle mouse clicks for annotation
- `mouseMoveEvent()`: Handle mouse dragging
- `paintEvent()`: Render image and annotations
- `zoom_in()`, `zoom_out()`: Zoom controls
- `start_painting()`, `start_erasing()`: Brush tools
- `mousePressEvent()` / `mouseMoveEvent()` / `mouseReleaseEvent()` /
`mouseDoubleClickEvent()`: Ctrl-modifier pan/zoom branches first,
then SAM/edit-mode branches, then dispatch to
`active_tool_handler.on_mouse_X()`.
- `keyPressEvent()`: Enter / Escape / Delete / brush-size keys. Modal
branches (DINO temp, sam_points, sam_box, editing_polygon)
consume first; otherwise routed to `handler.on_enter()` /
`on_escape()`.
- `paintEvent()`: image β†’ committed annotations β†’ editing polygon β†’
SAM overlays β†’ all tool handlers' `paint_overlay()` β†’ tool-size
indicator β†’ DINO temp annotations.
- `set_active_tool(name)`: switches `current_tool` and gives the
previous handler a chance to clean up via `deactivate()`.
- `check_unsaved_changes()`: iterates handlers' `has_unsaved_state()`
and prompts the user.

**Communication**: emits ~20 Qt signals connected to controller slots
in `ImageAnnotator._connect_image_label_signals` (ADR-018). Reads
main-window state through `CanvasContext`.

### SAMUtils (sam_utils.py)

Expand Down Expand Up @@ -147,6 +193,33 @@ DINO's xyxy boxes feed directly into `SAMUtils.apply_sam_predictions_batch()`,
which returns segmentation polygons (xywh bbox is derived from the polygon at
export time β€” see [Cross-cutting Concepts](08_crosscutting_concepts.md)).

## Level 3: Controllers

Seven `QObject` controllers plus an `io_controller` helper module
carve `ImageAnnotator` into single-responsibility owners that the
orchestrator delegates to. Each `QObject` controller holds `self.mw
= main_window` and owns one slice of behaviour; the
`io_controller` is a thin module of UI-wrapper functions around the
pure `io/` formatters and does not need to hold state. The
orchestrator keeps pass-through methods so external call sites
(menus, signal wiring, the test harness) don't need to reach into
the controller graph.

| Controller | Responsibility |
|------------|----------------|
| `ProjectController` | `.iap` save/load, auto-save, backup/restore, missing-image prompts, window-title sync. Owns the `is_loading_project` autosave guard (load/save round-trip safety, v0.8.12). |
| `ImageController` | Open / load / switch images and slices. TIFF + CZI loaders, the multi-dim `DimensionDialog`, the `[-ndim:]` axis-slice bug fix from the v0.9.0 era. |
| `AnnotationController` | Annotation CRUD, list sorting, highlight, edit-mode entry/exit, `finish_polygon`, `finish_rectangle`, `replace_annotations` (eraser path). Validates writes before mutating `all_annotations`. |
| `ClassController` | Class add / delete / rename / colour / visibility. `update_slice_list_colors`, `is_class_visible`. |
| `SAMController` | SAM box/points tool lifecycle, debounce timer, `_sam_inference_in_flight` re-entrancy guard (ADR-013), model picker. |
| `DINOController` | Single + batch detection, batch review navigation, temp-annotation accept/reject, custom-model browse, `DINOReviewEventFilter` ownership (ADR-015). |
| `YOLOController` | Training menu, `TrainingThread`, prediction dialog, result processing. |
| `io_controller` *(module-level functions, not a class)* | Thin UI wrappers around the pure `io/export_formats.py` and `io/import_formats.py` modules. |

Communication: `ImageLabel` does not import controllers directly β€”
it emits Qt signals (ADR-018) that the orchestrator connects to
controller slots in `_connect_image_label_signals()`.

## Level 3: Export/Import Subsystem

### Export Formats (export_formats.py)
Expand Down Expand Up @@ -270,7 +343,9 @@ ImageAnnotator (main window)
└── launches ──> Tool Dialogs (utilities)

ImageLabel
β”œβ”€β”€ references ──> ImageAnnotator (callbacks)
β”œβ”€β”€ emits signals to ──> ImageAnnotator (writes; see ADR-018)
β”œβ”€β”€ reads via ──> CanvasContext (paint/eraser size, current class,
β”‚ class_mapping, is_class_visible, scroll_area, …)
└── uses ──> utils (area, bbox calculations)

SAMUtils
Expand Down
2 changes: 1 addition & 1 deletion docs/06_runtime_view.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ User presses Enter
└─> update() to show final annotation
```

## SAM-Assisted Annotation
## SAM-Assisted Annotation (SAM-box / SAM-points)

```
User selects SAM model
Expand Down
Loading
Loading