From bd488224ccd77c3aee559d8e89c03707508cc2bd Mon Sep 17 00:00:00 2001 From: ga84mun Date: Mon, 7 Sep 2026 08:23:09 +0000 Subject: [PATCH 1/3] add no_squash option for ct models --- TPTBox/segmentation/VibeSeg/inference_nnunet.py | 7 ++++--- TPTBox/segmentation/VibeSeg/vibeseg.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/TPTBox/segmentation/VibeSeg/inference_nnunet.py b/TPTBox/segmentation/VibeSeg/inference_nnunet.py index 0ea9988..3a14dae 100644 --- a/TPTBox/segmentation/VibeSeg/inference_nnunet.py +++ b/TPTBox/segmentation/VibeSeg/inference_nnunet.py @@ -124,6 +124,7 @@ def run_inference_on_file( _key_ResEnc: str = "__nnUNet*ResEnc", fail_on_missing_memory=False, _cpu_chunks: int | None = None, + no_squash=False, logger=logger, ) -> tuple[Image_Reference, np.ndarray | None]: """Load a VibeSeg model and run inference on the supplied NIfTI images. @@ -344,9 +345,9 @@ def run_inference_on_file( if orientation is not None: logger.print("orientation", orientation, f"from {input_nii[0].orientation}") if verbose else None input_nii = [i.reorient(orientation) for i in input_nii] - - logger.print("squash to fit float16") if verbose else None - input_nii = [squash_so_it_fits_in_float16(i) for i in input_nii] + if not no_squash: + logger.print("squash to fit float16") if verbose else None + input_nii = [squash_so_it_fits_in_float16(i) for i in input_nii] if zoom is not None: logger.print("rescale", f"{zoom=} from {input_nii[0].zoom}") if verbose else None diff --git a/TPTBox/segmentation/VibeSeg/vibeseg.py b/TPTBox/segmentation/VibeSeg/vibeseg.py index 6bec72a..aa92490 100644 --- a/TPTBox/segmentation/VibeSeg/vibeseg.py +++ b/TPTBox/segmentation/VibeSeg/vibeseg.py @@ -86,7 +86,7 @@ defaults = { 100: {"memory_base": 5500, "memory_factor": 25}, - 12: {"memory_base": 7000, "memory_factor": 200}, + 12: {"memory_base": 7000, "memory_factor": 200, "no_squash": True}, } From 38b6f55f887296a66a398e6f281f9cb588bc9346 Mon Sep 17 00:00:00 2001 From: ga84mun Date: Mon, 7 Sep 2026 08:24:39 +0000 Subject: [PATCH 2/3] small bugfixes --- TPTBox/core/np_utils.py | 8 +++++++- examples/nako/stitching_T2w.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/TPTBox/core/np_utils.py b/TPTBox/core/np_utils.py index c7200f4..1d3325d 100755 --- a/TPTBox/core/np_utils.py +++ b/TPTBox/core/np_utils.py @@ -161,7 +161,13 @@ def np_volume(arr: UINTARRAY, include_zero: bool = False) -> dict[int, int]: """ # np.bincount wins decisively when there are many labels (e.g. connected-component maps); # cc3d statistics is faster for the few-label case typical of anatomical segmentations. - counts = np.bincount(arr.ravel()) if int(arr.max()) > 256 else cc3dstatistics(arr, use_crop=not include_zero)["voxel_counts"] + if int(arr.max()) > 256: + counts = np.bincount(arr.ravel()) + else: + try: + counts = cc3dstatistics(arr, use_crop=not include_zero)["voxel_counts"] + except ValueError: + counts = np.bincount(arr.ravel()) if include_zero: return {idx: i for idx, i in enumerate(counts) if i > 0} return {idx: i for idx, i in enumerate(counts) if i > 0 and idx != 0} diff --git a/examples/nako/stitching_T2w.py b/examples/nako/stitching_T2w.py index 85de67e..85ae1ee 100644 --- a/examples/nako/stitching_T2w.py +++ b/examples/nako/stitching_T2w.py @@ -11,7 +11,7 @@ logger = Print_Logger() arg_parser = argparse.ArgumentParser() arg_parser.add_argument("-i", "--inputfolder", help="input folder (where the rawdata folder is located)", required=True) -arg_parser.add_argument("-p", "--outparant", help="input folder (where the rawdata folder is located)", default="rawdata_stitched") +arg_parser.add_argument("-p", "--outparant", help="input folder (where the rawdata folder is located)", default="rawdata-stitched") arg_parser.add_argument("-s", "--sleep", type=float, default=0, help="sleep after each save") arg_parser.add_argument("-r", "--rawdata", type=str, default="rawdata", help="the rawdata folder to be searched") args = arg_parser.parse_args() From 7b913e7a88b4ea14f9492e5fa718d6afb734f53d Mon Sep 17 00:00:00 2001 From: ga84mun Date: Mon, 7 Sep 2026 08:25:01 +0000 Subject: [PATCH 3/3] add settings to review gui --- gui/snapshot_reviewer/review_gui.py | 89 ++++++++++++++++++++++-- gui/snapshot_reviewer/slicer_launcher.py | 8 ++- 2 files changed, 90 insertions(+), 7 deletions(-) diff --git a/gui/snapshot_reviewer/review_gui.py b/gui/snapshot_reviewer/review_gui.py index 4fb60bc..e41d8dc 100644 --- a/gui/snapshot_reviewer/review_gui.py +++ b/gui/snapshot_reviewer/review_gui.py @@ -113,9 +113,17 @@ PREFETCH_COUNT = 20 # how many upcoming snapshots to warm in the background CACHE_MAX_SIZE = 250 # how many decoded pixmaps to keep buffered (LRU) +# Cap on how many entries the review queue holds after filtering. 0 = no cap. +# Keeps the QListWidget snappy on very large datasets. +MAX_QUEUE_ENTRIES = 0 + # Default derivatives folder scanned for snapshots (JPG/PNG). CLI: --parent-dir. DEFAULT_SNAPSHOT_PARENT = "derivatives-VIBESeg-12-points-snp" +# Image fam-keys auto-checked in the Slicer launch dialog (pressing L). +# Overridable via the ⚙ Settings dialog; multiple entries allowed. +SLICER_AUTO_SELECT_KEYS: list[str] = ["ct"] + STYLESHEET = f""" QMainWindow, QWidget {{ background: {BG}; @@ -797,6 +805,8 @@ def __init__( prefetch_count: int, cache_max_size: int, buttons_per_row: int, + slicer_auto_select_keys: list[str] | None = None, + max_queue_entries: int = 0, parent: QWidget | None = None, ): super().__init__(parent) @@ -838,6 +848,16 @@ def __init__( self.btn_row_spin.setRange(1, 12) self.btn_row_spin.setValue(int(buttons_per_row)) form.addRow("Buttons per row (restart):", self.btn_row_spin) + + self.max_queue_spin = QSpinBox() + self.max_queue_spin.setRange(0, 1_000_000) + self.max_queue_spin.setValue(int(max_queue_entries)) + self.max_queue_spin.setSpecialValueText("unlimited") + form.addRow("Max entries in review queue (0 = ∞):", self.max_queue_spin) + + self.slicer_keys_edit = QLineEdit(", ".join(slicer_auto_select_keys or [])) + self.slicer_keys_edit.setPlaceholderText("ct, mri, t1 (comma or space separated)") + form.addRow("Slicer auto-selected image keys [L]:", self.slicer_keys_edit) layout.addLayout(form) buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) @@ -849,12 +869,16 @@ def values(self) -> dict: """Return the edited settings as a keyword-mapping dict.""" deriv_lines = [ln.strip() for ln in self.deriv_edit.toPlainText().splitlines()] deriv = [ln for ln in deriv_lines if ln] + raw_keys = self.slicer_keys_edit.text().replace(",", " ").split() + slicer_keys = [k.strip() for k in raw_keys if k.strip()] return { "slicer_exe": self.slicer_edit.text().strip(), "derivatives_search": deriv, "prefetch_count": int(self.prefetch_spin.value()), "cache_max_size": int(self.cache_spin.value()), "buttons_per_row": int(self.btn_row_spin.value()), + "slicer_auto_select_keys": slicer_keys, + "max_queue_entries": int(self.max_queue_spin.value()), } @@ -874,6 +898,7 @@ def __init__( prefetch_count: int | None = None, cache_max_size: int | None = None, buttons_per_row: int | None = None, + max_queue_entries: int | None = None, ): super().__init__() self.dataset_path = dataset_path @@ -915,12 +940,31 @@ def __init__( else (self.log.get_setting("ui.buttons_per_row", str(BUTTONS_PER_ROW)) or BUTTONS_PER_ROW) ), ) + self.max_queue_entries: int = max( + 0, + int( + max_queue_entries + if max_queue_entries is not None + else (self.log.get_setting("ui.max_queue_entries", str(MAX_QUEUE_ENTRIES)) or MAX_QUEUE_ENTRIES) + ), + ) + stored_keys = self.log.get_setting("slicer.auto_select_keys", None) + if stored_keys: + try: + self.slicer_auto_select_keys: list[str] = [s for s in json.loads(stored_keys) if isinstance(s, str)] + except Exception: + self.slicer_auto_select_keys = list(SLICER_AUTO_SELECT_KEYS) + else: + self.slicer_auto_select_keys = list(SLICER_AUTO_SELECT_KEYS) + # Persist the resolved values so they are stable across restarts. self.log.set_setting("slicer.exe", self.slicer_exe) + self.log.set_setting("slicer.auto_select_keys", json.dumps(self.slicer_auto_select_keys)) self.log.set_setting("derivatives.search", json.dumps(self.derivatives_search)) self.log.set_setting("image.prefetch_count", str(self.prefetch_count)) self.log.set_setting("image.cache_max_size", str(self.cache_max_size)) self.log.set_setting("ui.buttons_per_row", str(self.buttons_per_row)) + self.log.set_setting("ui.max_queue_entries", str(self.max_queue_entries)) self.all_snapshots: list[Path] = [] self.queue: list[Path] = [] @@ -1226,6 +1270,9 @@ def _apply_filter(self): # Keep any temporarily injected path even if it would be filtered out if self._injected_path and self._injected_path not in filtered: filtered.insert(0, self._injected_path) + # Cap queue length so the QListWidget stays fast on huge datasets. + if self.max_queue_entries > 0 and len(filtered) > self.max_queue_entries: + filtered = filtered[: self.max_queue_entries] self.queue = filtered if self.current_idx >= len(self.queue): self.current_idx = max(0, len(self.queue) - 1) @@ -1316,6 +1363,8 @@ def _open_settings(self): self.prefetch_count, self.cache_max_size, self.buttons_per_row, + self.slicer_auto_select_keys, + self.max_queue_entries, self, ) if dlg.exec() != QDialog.DialogCode.Accepted: @@ -1325,15 +1374,20 @@ def _open_settings(self): self.slicer_exe = v["slicer_exe"] self.derivatives_search = list(v["derivatives_search"]) or list(_DERIVATIVES_SEARCH) old_prefetch, old_cache, old_bpr = self.prefetch_count, self.cache_max_size, self.buttons_per_row + old_max_queue = self.max_queue_entries self.prefetch_count = v["prefetch_count"] self.cache_max_size = v["cache_max_size"] self.buttons_per_row = v["buttons_per_row"] + self.max_queue_entries = int(v["max_queue_entries"]) + self.slicer_auto_select_keys = list(v["slicer_auto_select_keys"]) self.log.set_setting("slicer.exe", self.slicer_exe) self.log.set_setting("derivatives.search", json.dumps(self.derivatives_search)) self.log.set_setting("image.prefetch_count", str(self.prefetch_count)) self.log.set_setting("image.cache_max_size", str(self.cache_max_size)) self.log.set_setting("ui.buttons_per_row", str(self.buttons_per_row)) + self.log.set_setting("ui.max_queue_entries", str(self.max_queue_entries)) + self.log.set_setting("slicer.auto_select_keys", json.dumps(self.slicer_auto_select_keys)) # Reset the cached BIDS_Global_info so the new derivatives list applies. if hasattr(self, "_slicer_bgi"): @@ -1348,6 +1402,9 @@ def _open_settings(self): note += " Buttons-per-row change takes effect after a restart." if self.prefetch_count != old_prefetch: note += " Prefetch tuned." + if self.max_queue_entries != old_max_queue: + self._apply_filter() + note += " Queue cap applied." self.status_bar.showMessage(note, 5000) def _refresh_queue_list(self): @@ -1530,6 +1587,7 @@ def _mark(self, verdict_key: str): if p is None or verdict_key not in VERDICT_META: return reason = self.reason_edit.text().strip() + marked_idx = self.current_idx self._next() QApplication.processEvents() if verdict_key == "good": @@ -1541,16 +1599,25 @@ def _mark(self, verdict_key: str): actual_vk = actual["verdict"] if actual else verdict_key label = VERDICT_META.get(actual_vk, (verdict_key,))[0] self.status_bar.showMessage(f"{label}: {p.name}", 3000) - self._post_mark() + self._post_mark(marked_idx=marked_idx, marked_vk=actual_vk) - def _post_mark(self): + def _post_mark(self, marked_idx: int | None = None, marked_vk: str | None = None): # Warm the next 10 snapshots in the background so stepping forward # (or jumping back into recently-seen territory) doesn't stall on I/O. self._prefetch_upcoming() self._update_stats() self._update_verdict_log() - self._refresh_queue_list() - self._show_current() + # Update just the single row that changed instead of rebuilding the + # entire QListWidget — at 10k entries a full rebuild on every keypress + # is what makes the UI feel sluggish. + if marked_idx is not None and 0 <= marked_idx < self.queue_list.count(): + item = self.queue_list.item(marked_idx) + if item is not None: + meta = VERDICT_META.get(marked_vk) if marked_vk else None + if meta is not None: + item.setForeground(QColor(meta[1])) + else: + self._refresh_queue_list() def _open_in_slicer(self): """Open the current snapshot's BIDS family in 3D Slicer.""" @@ -1588,6 +1655,19 @@ def _on_overwrite(jpg_path: Path, path_str: str, _fam: dict[str, list[BIDS_FILE] for k1, k2, coord in p2.items(): p[k1, k2] = coord p.save(f) + # out = _fam["ct"][0].get_changed_path( + # "nii.gz", + # "msk", + # parent="derivatives-final-points", + # info={"seg": "treg", "mod": None}, + # ) + # logger.on_debug("unlink", out, out.exists()) + # out.unlink(missing_ok=True) + # for t in _fam.get("msk_seg-treg", []): + # logger.on_debug(t) + # if t.parent == "derivatives-final-points": + # logger.on_debug("unlink", t.file["nii.gz"]) + # t.file["nii.gz"].unlink(missing_ok=True) dlg = SlicerLaunchDialog( p, @@ -1597,6 +1677,7 @@ def _on_overwrite(jpg_path: Path, path_str: str, _fam: dict[str, list[BIDS_FILE] parent=self, slicer_exe=self.slicer_exe, derivatives_search=self.derivatives_search, + auto_select_keys=self.slicer_auto_select_keys, ) dlg.exec() diff --git a/gui/snapshot_reviewer/slicer_launcher.py b/gui/snapshot_reviewer/slicer_launcher.py index c76e65e..d872128 100644 --- a/gui/snapshot_reviewer/slicer_launcher.py +++ b/gui/snapshot_reviewer/slicer_launcher.py @@ -376,6 +376,7 @@ def __init__( parent: QWidget | None = None, slicer_exe: str | None = None, derivatives_search: list[str] | None = None, + auto_select_keys: list[str] | None = None, ): super().__init__(parent) self.setWindowTitle("Open in Slicer") @@ -387,6 +388,7 @@ def __init__( self._watchers: list[SlicerFileWatcher] = [] self._slicer_exe = slicer_exe or SLICER_EXE self._derivatives_search = list(derivatives_search or _DERIVATIVES_SEARCH) + self._auto_select_keys = [k for k in (auto_select_keys or ["ct"]) if k] # Build fam self._fam = build_fam_for_snapshot(snp_path, dataset_path, bgi, self._derivatives_search) @@ -419,7 +421,7 @@ def _build_ui(self): for key, bf in self._images: p = bf.file.get("nii.gz") or bf.file.get("nii", "") cb = QCheckBox(f"{key}\n {Path(str(p)).name}") - cb.setChecked(True) + cb.setChecked(key in self._auto_select_keys) img_v.addWidget(cb) self._img_checks.append((cb, bf)) else: @@ -435,7 +437,7 @@ def _build_ui(self): for key, bf in self._masks: p = bf.file.get("nii.gz") or bf.file.get("nii", "") cb = QCheckBox(f"{key}\n {Path(str(p)).name}") - cb.setChecked(False) + cb.setChecked(key in self._auto_select_keys) msk_v.addWidget(cb) self._msk_checks.append((cb, bf)) else: @@ -450,7 +452,7 @@ def _build_ui(self): for key, bf in self._markups: p = bf.file.get("json", "") cb = QCheckBox(f"{key}\n {Path(str(p)).name}") - cb.setChecked(False) + cb.setChecked(key in self._auto_select_keys) mrk_v.addWidget(cb) self._mrk_checks.append((cb, bf)) else: