Skip to content

Migrate to PyQt6 and run SAM/DINO inference in-process#69

Merged
bnsreenu merged 8 commits into
bnsreenu:masterfrom
cofade:claude/compare-branches-f8cxq
May 22, 2026
Merged

Migrate to PyQt6 and run SAM/DINO inference in-process#69
bnsreenu merged 8 commits into
bnsreenu:masterfrom
cofade:claude/compare-branches-f8cxq

Conversation

@cofade

@cofade cofade commented May 21, 2026

Copy link
Copy Markdown
Contributor

@bnsreenu — proposing two coupled changes that I think materially improve the cross-version story for this project. The PR is a single set of commits because the second change unblocks the first; splitting them would require a temporary half-state that's worse than either endpoint.

Closes #67 (PyQt5 + Python 3.14 + Torch DLL conflict → PyQt6 migration).
Closes #68 (multi-dim slice support in DINO "Detect All Images").

Summary

  • PyQt5 → PyQt6 across the codebase (~30 source modules + tests + CI). Bulk enum-namespacing codemod, QAction / QShortcut module relocation, QDesktopWidgetQGuiApplication.primaryScreen(), event.position() + QPointF end-to-end in image_label.py. Documented in ADR-014.
  • Retired the subprocess workers. sam_worker.py, dino_worker.py, and tools/check_worker_isolation.py are deleted. sam_utils.py and dino_utils.py load Ultralytics / Transformers models in-process and cache them on SAMUtils / DINOUtils. Each inference runs on a short-lived QThread; the calling thread pumps its event loop via _run_sync so the public API stays synchronous-looking and the UI stays responsive. ADR-011 is marked Superseded by new ADR-013.

End result: ~1-2 s faster per SAM/DINO call on Windows (no subprocess spawn, no model reload), cleaner Linux runtime (Qt 6 native integration), Python 3.13/3.14 supported out of the box. Bumped 0.8.12 → 0.9.0 to signal the binding switch.

Why they're coupled

The subprocess workers existed to dodge WinError 1114 — a PyQt5 + Torch DLL load-order conflict on Windows + Python 3.14, documented in the old ADR-011. Migrating to PyQt6 eliminates that conflict (Qt6 reshuffled its DLL packaging), so the entire subprocess isolation layer becomes dead code. Doing both in one PR avoids paying the migration tax twice.

Phase 0 coexistence gate

tools/check_pyqt6_torch_coexistence.py imports PyQt6 → torch → torchvision → transformers → ultralytics in that order and constructs a QApplication (the platform plugin is what actually loaded the conflicting DLL in the original failure). Pass = subprocess removal is safe. Verified on Linux + Python 3.11 and Windows + Python 3.14.

What's also in the PR

The migration exposed a handful of latent issues. Rather than ship them as follow-ups against a freshly-broken PyQt6 build, they're addressed in this same PR — every fix has a one-line explanation in arc42 / ADR docs so future maintainers don't re-derive them. Highlights:

DINO + multi-dim images (closes #68):

  • Detect All Images now flattens every loaded slice into the batch via _collect_dino_batch_work_items. The current implementation silently skipped multi-dim slices including the active one.
  • _navigate_to_image_or_slice handles slice names in batch review (slices live in slice_list, not image_list).
  • _refresh_dino_temp_for_current re-syncs image_label.temp_annotations from dino_batch_results on every image / slice switch. Without this, masks from the previously-reviewed image visually bleed onto every subsequent slice.
  • Application-wide _DINOReviewEventFilter makes Enter / Escape work during review regardless of focus (otherwise QListWidget consumes Enter for itemActivated). See ADR-015.
  • Auto-accept dropdown now honored by both Detect Current Image and Detect All Images — was batch-only before.
  • DINO phrase editor: row 0 (class-name phrase) is now renamable; auto-prepend of original class-name in get_phrases_for removed so the rename actually takes effect.

Canvas (pan + zoom-to-cursor):

  • Pan switched to event.globalPosition() — widget-local coords were absorbing half the cursor delta as the scrolled widget shifted under the cursor, producing effective half-speed pan.
  • Cursor-anchored Ctrl+wheel zoom newly implemented. Post-zoom offset derived analytically from viewport().width() because self.width() is stale on zoom-out before layout settles.

Multi-dim TIFF:

  • load_tiff reads tifffile.series[0].axes and pre-fills the dimension dialog with the right T/Z/C/H/W mapping. Previously the default_dimensions[-ndim:] slice silently degraded for ndim ≥ 5 — a 5D TZCYX TIFF produced 2560 one-row "slices" of wrong content on OK.

GPU memory:

  • Tools → Unload AI Models (Free GPU Memory) now uses the full recipe: model.cpu()gc.collect()torch.cuda.empty_cache() + ipc_collect() + synchronize(). Without the explicit gc.collect(), Ultralytics' circular refs pinned the model on the device until the next GC tick — Task Manager / nvidia-smi showed zero drop. Dialog now also discloses that PyTorch keeps a per-process CUDA context that survives unload (requires app restart for full reclaim).

Mechanical PyQt6 cleanup:

  • 15× surviving .exec_().exec() calls across QMenu / QDialog / QApplication / QMessageBox call sites in 6 files (would crash on right-click in the class list, on Project Search, on the DICOM converter, etc.).
  • Missing import traceback in dino_merge_dialog.py (latent NameError).
  • F2 (Snake game) moved from keyPressEvent to QShortcut(ApplicationShortcut) because QTableWidget consumed F2 for in-cell-edit before it bubbled.

Dark mode:

  • Dark mode on by default at startup.
  • Removed hardcoded background: #e0e0e0 from the DINO threshold table header and background: #f5f5f5 from the DINO status label — both punched bright rectangles into the dark sidebar.
  • Added QRadioButton / QCheckBox / QHeaderView / QTableWidget / QSpinBox / QDoubleSpinBox / QComboBox / QGroupBox rules to soft_dark_stylesheet.py. Dataset splitter radio buttons (and a few other widgets) were rendering at the OS default which on Windows dark mode meant near-invisible selected-state indicators.
  • Annotated-slice highlight changed from light blue (173, 216, 230) to muted steel-blue (58, 95, 140) with light text.

YOLO export:

  • Exact-key match first in image_paths.get(image_name); substring fallback only after no exact match. "bee.jpg" in "honeybee.jpg" was True under the old substring-only lookup.

Threading + safety net

  • _run_sync runs the inference callable on a QThread, captures any exception on the worker instance, and re-raises on the calling thread.
  • _inference_in_flight module flag + InferenceBusyError exception serialise concurrent calls. A QMutex was considered and rejected — same-thread re-acquisition of a non-recursive mutex deadlocks, recursive defeats serialisation.
  • apply_sam_prediction carries its own _sam_inference_in_flight flag at the call site, because the SAM debounce timer can fire while an earlier inference is pumping inside _run_sync. Defence in depth.
  • _qimage_to_numpy always returns a fresh copy. The earlier alias-the-QImage-buffer pattern was a latent UAF in the fallback path that threading would make more dangerous.

Quality gate

Five senior-reviewer passes on the diff, each followed by a fix commit. Each pass surfaced real issues (silent KeyError in cross-project state carryover, row-0 rename illusion, missing update_slice_list_colors after auto-accept, orphan batch-review entries, etc.). Final verdict: "Mergeable with the P1 fixes applied."

Test plan

  • All 65 pytest tests/ pass on Linux + Python 3.11 under QT_QPA_PLATFORM=offscreen
  • Full app constructs and renders headlessly
  • Phase 0 coexistence smoke test passes (Linux + Py3.11; Windows + Py3.14 verified locally)
  • Manual UI smoke on Windows: SAM click-segment, SAM box, DINO single + batch (regular images + multi-dim slices), multi-dim TIFF loading + dimension dialog, project save/load roundtrip, dark-mode toggle, Tools → Unload AI Models, YOLO + COCO + Pascal VOC export, all Tool menu dialogs (DICOM converter, image patcher, image augmenter, dataset splitter, slice registration, stack interpolator, annotation statistics, COCO combiner, Merge COCO for Training), right-click context menus, F2 Snake easter egg, project search, project details
  • CI green across Ubuntu / Windows / macOS × Python 3.10-3.13 — please confirm once CI runs

Architecture docs touched

  • ADR-001 (Use PyQt5) marked Superseded → ADR-014
  • ADR-011 (subprocess isolation) marked Superseded → ADR-013
  • ADR-013 (in-process inference + QThread) new
  • ADR-014 (PyQt5 → PyQt6 migration) new
  • ADR-015 (application-wide event filter for DINO review shortcuts) new
  • docs/05_building_block_view.md: SAMUtils block rewritten, DINO subprocess box dropped
  • docs/06_runtime_view.md: SAM and DINO inference sequence updated for in-process path
  • docs/08_crosscutting_concepts.md: new sections for Pan + Zoom Reference Frames, Dark Mode No Hardcoded Colors Rule, Releasing Model GPU Memory, DINO Temp Annotations lifecycle (single field, switch sync, batch over slices, navigation, event filter, auto-accept), Multi-dim TIFF Axis Defaults, Export Format Filename Matching
  • docs/12_glossary.md: "Subprocess Worker" entry marked historical
  • README.md, docs/01_introduction_and_goals.md, docs/02_architecture_constraints.md: PyQt5 → PyQt6
  • PYTHON314_SETUP.md deleted (described migration as future work)
  • TESTING.md: stale Py3.14/DLL section excised, Py3.13 added

Stats

15 files changed in the manual-testing fix pack on top of the original migration commits, with ~1000 lines net added — most of it diagnostic logging + the four new arc42 sections + the application-wide event filter. The subprocess machinery removal in the parent commits is the net deletion (~300 lines).


Happy to discuss any of the calls before merge — especially the dark-mode default, the dino_batch_mode semantics applying to both detect paths, and whether you'd rather keep the diagnostic [DINO] / [SAM] print logs un-gated (current state, per the manual-test session) or move them behind an env var. None of those are load-bearing for the binding migration itself.

claude and others added 8 commits May 19, 2026 18:44
Validates the central hypothesis of the upcoming subprocess-removal
work: that PyQt6 sidesteps the WinError 1114 DLL load-order conflict
on Windows + Python 3.14 that motivated sam_worker.py / dino_worker.py
(see ADR-011).

Run manually before deleting any worker code. Exit code 0 unblocks
Phase 2 of the PyQt5 -> PyQt6 + in-process inference migration.

https://claude.ai/code/session_01ADoBX5VmUYpCrwbkecKMHL
End-to-end migration of the GUI binding. 34 files, ~360 lines changed.
All 65 tests still pass on PyQt6 6.11; the full app constructs and
renders headlessly via QT_QPA_PLATFORM=offscreen.

What changed
------------
- Dependency pins: PyQt5>=5.15 -> PyQt6>=6.7 (requirements.txt, setup.py)
- Bulk import rewrite: `from PyQt5...` -> `from PyQt6...` (28 files)
- Symbol relocations:
  * QAction moved from QtWidgets to QtGui (annotator_window.py)
  * QDesktopWidget removed -> QGuiApplication.primaryScreen() (snake_game.py)
- Enum namespacing (Qt6 requires fully-qualified names everywhere):
  * Qt.AlignmentFlag / MouseButton / KeyboardModifier / Key
  * Qt.PenStyle / BrushStyle / CursorShape / GlobalColor
  * Qt.WindowType / WindowModality / FocusPolicy / TransformationMode
  * Qt.ItemDataRole / ItemFlag / ContextMenuPolicy / ScrollBarPolicy
  * Qt.TextFormat / TextInteractionFlag / MatchFlag / CheckState
  * QMessageBox.StandardButton / .Icon / .ButtonRole
  * QDialog.DialogCode, QFileDialog.AcceptMode / FileMode / Option
  * QAbstractItemView.SelectionMode / SelectionBehavior / EditTrigger
  * QHeaderView.ResizeMode, QSlider.TickPosition
  * QPainter.RenderHint, QImage.Format.Format_*
  * QDialogButtonBox.StandardButton / .ButtonRole
  * QKeySequence.StandardKey
- Modern event API in image_label.py: event.pos()/.x()/.y() -> event.position()
  returning QPointF end-to-end. Scrollbar setValue() takes int() of the
  QPointF delta (the boundary).
- Removed dead workaround in annotator_window.py: clearing
  WindowContextHelpButtonHint from dialog flags. Qt6 already suppresses
  this; the flag itself was removed.
- exec_() -> exec() in main.py entry point.
- CI: add libegl1/libgl1 to the Linux apt-install list (Qt6 needs them).
- Docs: CLAUDE.md, README.md, docs/02_architecture_constraints.md updated
  to reflect PyQt6 and the relaxed Linux support story.

Not touched
-----------
- sam_worker.py, dino_worker.py, sam_utils.py, dino_utils.py subprocess
  pattern (Phase 2, gated on Win+Py3.14 validation).
- exec_() call sites outside main.py (still work as deprecated alias).

https://claude.ai/code/session_01ADoBX5VmUYpCrwbkecKMHL
… wrapping

Phase 2 of the PyQt6 migration. The DLL conflict that motivated the
subprocess workers (ADR-011) no longer applies under PyQt6, so the
~1-2 s subprocess spawn per inference is gone, and models stay cached
across calls.

What changed
------------
- Deleted sam_worker.py (288 lines), dino_worker.py (231 lines),
  tools/check_worker_isolation.py (134 lines).
- Rewrote sam_utils.py end-to-end:
  * SAMUtils inherits QObject, caches the Ultralytics model in self._model
  * change_sam_model() loads eagerly (on worker thread, UI stays alive)
  * apply_sam_points / apply_sam_prediction / apply_sam_predictions_batch
    all run inference on a QThread via the new _run_sync helper
  * Lazy import of torch/ultralytics keeps app startup snappy
  * Added unload() for future Tools-menu memory release
- Rewrote dino_utils.py with the same pattern (DINOUtils as QObject,
  model cached across calls, transformers lazy-imported).
- Added _run_sync: spawns a QThread, pumps the calling thread's
  QEventLoop until done. Public API stays synchronous so the existing
  call sites in annotator_window.py work unchanged. UI events (timers,
  redraws, progress dialog cancels) keep flowing during the wait.
- Removed the stale "If you are on Python 3.14, PyTorch may not yet be
  fully supported" message in change_sam_model error path.
- utils.py docstring: drop sam_worker reference.

Docs
----
- ADR-011 marked Superseded, with pointer to ADR-013.
- New ADR-013 documents the in-process + QThread decision, the latency
  win, and the trade-offs (re-entrancy via QEventLoop pump, no more
  crash isolation).
- arc42 docs/05_building_block_view.md, docs/06_runtime_view.md,
  docs/12_glossary.md updated to drop subprocess wording.
- CLAUDE.md SAM Integration section rewritten.

Verification
------------
- 65 tests pass (pytest, QT_QPA_PLATFORM=offscreen).
- Full app constructs and renders headlessly.
- _run_sync round-trip verified end-to-end against a 0.3 s sleep.
- Phase 0 PyQt6+torch+transformers+ultralytics coexistence smoke test
  passes on Linux+Py3.11. Windows+Py3.14 verification is the user's
  responsibility before this PR ships.

https://claude.ai/code/session_01ADoBX5VmUYpCrwbkecKMHL
The quality gate is blocking by design — the next steps (address P0s,
push, open PR) depend on its findings. Backgrounding it just defers the
work and risks shipping unreviewed code.

https://claude.ai/code/session_01ADoBX5VmUYpCrwbkecKMHL
P0 — correctness
----------------
- _InferenceThread.run no longer swallows exceptions. Stores them on
  the thread instance; _run_sync re-raises on the calling thread.
  Silent model-load failures previously showed up as "No mask matches"
  / "No detections" dialogs with no way to diagnose. (sam_utils.py)
- Added _inference_in_flight module flag + InferenceBusyError. The
  earlier QMutex draft would have deadlocked: same-thread re-acquisition
  of a non-recursive mutex hangs, and a recursive mutex would defeat
  the whole serialization point. A flag with an explicit exception
  surfaces re-entry instead of corrupting the model with concurrent
  .forward() calls. (sam_utils.py)
- Added _sam_inference_in_flight guard in annotator_window.apply_sam_prediction
  — the SAM debounce timer can fire while a previous inference is
  pumping inside _run_sync; the guard skips the re-entrant call so the
  next click + debounce restart issues a fresh inference with the
  up-to-date point set.

P1 — should-fix
---------------
- dino_utils._detect_blocking no longer shuffles the model CPU<->GPU
  on every call. Moving a 1.9 GB DINO base over PCIe was wiping out
  the in-process caching gain the whole PR was meant to deliver.
- change_sam_model now flips current_sam_model AFTER successful load,
  not before. On load failure the state stays consistent.
- _qimage_to_numpy always returns a fresh copy. The fallback path was
  particularly broken: the converted QImage was local, would go out
  of scope at return, and the worker thread's numpy buffer would have
  aliased freed memory.
- Updated misleading dino_utils.detect comment about marshaling — the
  safety actually comes from the .copy() inside _qimage_to_numpy, not
  from where it runs.
- ADR-001 marked Superseded with pointer to new ADR-014.
- New ADR-014 documents the PyQt5->PyQt6 migration decision.
- Updated ADR-013 consequences to honestly describe the re-entrancy
  guards (replaced the "acceptable for now; revisit if users hit it"
  hand-wave the reviewer specifically called out).
- docs/01_introduction_and_goals.md, docs/05_building_block_view.md
  (ASCII diagram), docs/06_runtime_view.md (app.exec_() typo) all
  updated for PyQt6.
- CLAUDE.md: Testing Checklist no longer references deleted sam_worker
  /dino_worker; senior-reviewer agent prompt no longer references
  deleted check_worker_isolation.py.
- .claude/agents/senior-reviewer.md retargeted from PyQt5 -> PyQt6 and
  rewritten to check ADR-013 re-entrancy guards instead of ADR-011's
  retired subprocess isolation.

P2 — opportunistic
------------------
- tools/check_pyqt6_torch_coexistence.py now constructs a QApplication
  after importing torch. Pure import alone does not load Qt's native
  platform plugin (qwindows.dll on Windows) — which is the actual
  site of the historical WinError 1114. The previous green result
  was a false positive on the strictest test.
- CI matrix gains Python 3.13. ADR-013 claims PyQt6+torch coexist on
  modern Pythons; this adds CI evidence (Py3.14 still manual via the
  coexistence script — pip wheels not yet broadly available).
- CI apt-install list deduped (libxcb-cursor0 was listed twice).

Verification
------------
- 65 tests pass.
- Smoke-tested both fixes: exception propagation works
  (boom() raises ValueError out of _run_sync); re-entry detection
  works (timer-driven inner call raises InferenceBusyError while
  outer is pumping).
- coexistence script with QApplication construction passes
  end-to-end on Linux+Py3.11.

https://claude.ai/code/session_01ADoBX5VmUYpCrwbkecKMHL
P0 — correctness (regression unblocked by the first round's fix)
----------------------------------------------------------------
- annotator_window.py: import traceback at module level. The except
  block at the DINO call site (line 3022 pre-edit) referenced
  traceback.print_exc() without the module being importable in scope.
  Before the previous fix dino_utils.detect() returned None on error
  so the except was rarely entered; now it raises for real, so the
  NameError was about to start firing and leave the detect buttons
  permanently disabled with no user-visible dialog.

P1 — should-fix
---------------
- annotator_window.apply_sam_prediction now catches inference
  exceptions. The slot is driven by a QTimer; before this patch a
  CUDA OOM or InferenceBusyError would fall out into PyQt6's
  default unhandled-slot handler (stderr only). InferenceBusyError
  is suppressed silently (defense-in-depth alongside the call-site
  flag); other exceptions show a critical QMessageBox.
- Same wrapping added to the unprotected SAM-batch calls inside both
  DINO flows (single image at line 3063, per-image loop at 3170).
- Wired SAMUtils.unload() and DINOUtils.unload() to a new Tools menu
  entry "Unload AI Models (Free GPU Memory)". The DINO CPU<->GPU
  shuffle was removed in the previous round, which removes the
  automatic between-call free; this gives users on constrained GPUs
  a manual recovery path.
- Bumped version 0.8.12 -> 0.9.0 in setup.py and __init__.py to
  signal the binding change (PyQt5 -> PyQt6) and the in-process
  inference rework. Anyone reading the wheel changelog now sees
  the binding switch in the version.
- docs/05_building_block_view.md SAMUtils block rewritten to match
  the actual class shape (sam_model -> _model, qimage_to_numpy is
  a module-level helper not a method, _run_sync added).
- Deleted PYTHON314_SETUP.md — it described the migration as future
  work, in the present tense, with the now-retired DLL workaround
  as a known issue. Easier to delete than keep coherent.

P2 — cleanup
------------
- Dropped the unused `import traceback` in sam_utils.py
  (_InferenceThread captures exceptions on the instance now; no
  printing inside the worker).
- The "No mask generated." batch fallback now builds a fresh dict
  per bbox via list comprehension instead of `[d] * N` (avoided
  shared-reference footgun).
- Removed the dead `qimage_to_numpy` method on ImageAnnotator —
  module-level `_qimage_to_numpy` in sam_utils superseded it.
- Folded the local `import traceback` inside `add_class`'s except
  block into the module-level import.

Architectural belt-and-braces
-----------------------------
- Added an assert at the top of `_run_sync`: the function MUST be
  called from the GUI thread. The `_inference_in_flight` flag is a
  plain global, not protected against cross-thread access — if a
  future contributor drives inference from a worker thread it
  becomes a true race. The assert is the tripwire. Reviewer
  flagged this as the kind of constraint that gets violated six
  months later when nobody remembers the design.

Verification
------------
- 65 tests still pass.
- Exception propagation and re-entry detection both re-tested in
  the full-app context — outer call returns 'done', timer-driven
  inner call raises InferenceBusyError, both as designed.
- App constructs and renders headlessly.

https://claude.ai/code/session_01ADoBX5VmUYpCrwbkecKMHL
P1
--
- TESTING.md: removed the "Known Issues — Python 3.14 + PyTorch
  Compatibility" section (the WinError 1114 it described is gone
  with the PyQt6 migration), removed the "Milestone 1.2: PyQt6
  Migration" future-work entry (the migration is done), and added
  a brief "Headless Testing" section pointing at the CI deps list.
  Also bumped the CI Python row to mention 3.13. The file was not
  touched by earlier commits in this branch; the reviewer correctly
  pointed out that the branch is what made it wrong, so it's owed.

P2
--
- Replaced the GUI-thread tripwire in sam_utils._run_sync with an
  explicit `if ...: raise RuntimeError(...)` instead of `assert`.
  `python -O` strips asserts; the tripwire was the kind of thing
  that would only matter once it had silently disappeared.

Verification: 65 tests still pass. App still constructs.

https://claude.ai/code/session_01ADoBX5VmUYpCrwbkecKMHL
Closes user-reported regressions and rough edges discovered during Windows
manual testing of PR #4. Covers crashes, UX bugs, and silent failures that
the 65-test pytest-qt suite doesn't exercise.

PyQt5 → PyQt6 mechanical migration gaps:
- 15× .exec_() → .exec() across annotator_window, dino_merge_dialog,
  image_patcher, project_search, snake_game, stack_to_slices. The
  QMenu crash on right-click in the class list (annotator_window:4607)
  was the first user-visible casualty.
- Missing `import traceback` in dino_merge_dialog.py.
- F2 (Snake game) moved from keyPressEvent to QShortcut(ApplicationShortcut)
  so QTableWidget's in-cell-edit doesn't swallow it.

Canvas — pan + zoom-to-cursor:
- Pan now uses event.globalPosition() so the widget shifting under the
  cursor mid-drag doesn't absorb half the delta (former half-speed pan).
- New cursor-anchored Ctrl+wheel zoom; post-zoom offset derived
  analytically from viewport().width() instead of the stale self.width()
  that's wrong on zoom-out before layout settles.

DINO panel + detection:
- Threshold column widths (88 px fixed) + setFrame(True) so values
  "0,25" / "0,50" are readable.
- PhraseEditorPanel auto-reveals on class-add; row-0 phrase is now
  renamable. Removed the silent class-name re-prepend in get_phrases_for
  + _run_for_class so a renamed row-0 actually reaches DINO.
- Auto-accept dropdown now honored by both single + batch paths.
- "Detect All Images" extended to multi-dim image slices via
  _collect_dino_batch_work_items (was silently skipping stacks).
- New _navigate_to_image_or_slice handles slice names in batch review;
  orphan results are popped instead of leaving a half-state.
- temp_annotations is a single field — _refresh_dino_temp_for_current
  syncs it on every switch_slice / switch_image so masks don't bleed
  between slices.
- Application-wide _DINOReviewEventFilter makes Enter / Escape work
  during review regardless of which widget has focus. ADR-015 documents
  the choice over QShortcut and force-focus alternatives.
- dino_batch_results initialised in __init__; dropped 4 lazy-hasattr
  checks.
- Verbose [DINO] / [SAM] diagnostic prints at decision points
  (un-gated per user request — print is the project convention).

Multi-dim TIFF loading:
- load_tiff reads tifffile.series[0].axes and maps Y→H, X→W into the
  app's dimension vocab. DimensionDialog defaults to these hints when
  ndim matches.
- Explicit ndim 3-6 fallback table, plus generic
  ["T"]*(ndim-2) + ["H","W"] for ndim ≥ 7. The earlier
  default_dimensions[-ndim:] of a 4-element list silently degraded for
  5D TZCYX inputs and produced 2560 one-row "slices".

Tools → Unload AI Models:
- Three-step recipe: model.cpu() → gc.collect() → empty_cache +
  ipc_collect + synchronize. Disclosure dialog now mentions the
  per-process CUDA context that survives unload.
- Resets both SAM + DINO dropdowns and disables Detect buttons on unload.

YOLO export:
- image_paths lookup uses exact-key match first, substring fallback only
  (prevents "bee.jpg" matching "honeybee.jpg" by substring).
- Diagnostic [YOLO v5+] / [YOLO v4] prints, warning when a class isn't
  in class_mapping.

Dark mode:
- Dark mode now on by default at startup.
- Removed hardcoded #e0e0e0 / #f5f5f5 from ClassThresholdTable header
  and lbl_dino_status (they punched bright boxes into the dark sidebar).
- Added QRadioButton / QCheckBox / QHeaderView / QTableWidget /
  QSpinBox / QDoubleSpinBox / QComboBox / QGroupBox rules to
  soft_dark_stylesheet so dataset splitter radio buttons + DINO
  panel widgets render with adequate contrast.
- Annotated-slice highlight changed from light blue (173,216,230) to
  muted steel-blue (58,95,140) on dark mode.

Docs:
- ADR-015 added — application-wide event filter for DINO review.
- Cross-cutting concepts gained sections for Pan + Zoom Reference
  Frames, Dark Mode No Hardcoded Colors Rule, Releasing Model GPU
  Memory, DINO Temp Annotations (lifecycle / event filter / batch /
  navigation / auto-accept), Multi-dim TIFF Axis Defaults, Export
  Format Filename Matching.
- CLAUDE.md gained a "Patterns introduced in v0.9.0" index table
  pointing at the arc42 deep-dives so new contributors don't re-derive
  them.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@bnsreenu bnsreenu self-assigned this May 22, 2026
@bnsreenu bnsreenu merged commit c408178 into bnsreenu:master May 22, 2026
12 checks passed
@cofade cofade deleted the claude/compare-branches-f8cxq branch June 7, 2026 19:56
bnsreenu pushed a commit that referenced this pull request Jun 11, 2026
Upstream PR #69 (PyQt6 migration + in-process inference) was developed
on this fork; the fork carries the same content plus later refinements
(ADR-017 torch-first findings in the coexistence check, lazy package
__init__). Verified file-by-file that no upstream-only change exists:
TESTING.md is identical, all other deltas are fork-side additions.
This ours merge makes the branch a descendant of upstream/master so
the PR diff shows exactly the new work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DINO "Detect All Images" silently skips multi-dim image slices Migrate from PyQt5 to PyQt6 to unblock Python 3.14 + Torch coexistence

3 participants