diff --git a/js/showdiffraction/index.tsx b/js/showdiffraction/index.tsx index 823519b7..cc06d30a 100644 --- a/js/showdiffraction/index.tsx +++ b/js/showdiffraction/index.tsx @@ -568,6 +568,9 @@ function ShowDiffraction() { const [, setRingRemoveRequest] = useModelState("_ring_remove_request"); const [dpColormap, setDpColormap] = useModelState("dp_colormap"); const [dpScaleMode, setDpScaleMode] = useModelState("dp_scale_mode"); + const [displayDenoise, setDisplayDenoise] = useModelState("denoise"); + const [detectDenoise, setDetectDenoise] = useModelState("detect_denoise"); + const [showDetectionView, setShowDetectionView] = useModelState("show_detection_view"); const [dpInvert, setDpInvert] = useModelState("dp_invert"); const [dpVminPct, setDpVminPct] = useModelState("dp_vmin_pct"); const [dpVmaxPct, setDpVmaxPct] = useModelState("dp_vmax_pct"); @@ -1732,6 +1735,14 @@ function ShowDiffraction() { Log Sqrt + Denoise + + Detect + {centerMode === "manual" && ( click to set )} @@ -1891,6 +1902,7 @@ function ShowDiffraction() { ["Azim", "Azimuthal profile", showAzimuthal, () => setShowAzimuthal(!showAzimuthal)], ["HKL", "hkl labels", showHkl, () => setShowHkl(!showHkl)], ["Mask View", "Mask overlay", showMask, () => setShowMask(!showMask)], + ["Detect View", "Detection frame view", showDetectionView, () => setShowDetectionView(!showDetectionView)], ["Invert", "Invert colormap", dpInvert, () => setDpInvert(!dpInvert)], ["Stats", "Statistics", showStats, () => setShowStats(!showStats)], ["Quality", "Analysis quality", showQc, toggleQuality], diff --git a/src/quantem/widget/showdiffraction.py b/src/quantem/widget/showdiffraction.py index 7777a55c..5708fbb5 100644 --- a/src/quantem/widget/showdiffraction.py +++ b/src/quantem/widget/showdiffraction.py @@ -2166,6 +2166,7 @@ def _load_initial_state(self, state) -> None: self.load_state_dict(self._resolve_state(state)) def _ingest_data(self, data): + self._filter_cache = {} array = to_numpy(data) is_integer = np.issubdtype(array.dtype, np.integer) array = array.astype(np.float32) @@ -2287,7 +2288,17 @@ def _detection_frame(self) -> np.ndarray: mode = self._resolve_detect_denoise(frame) if mode == "none": return frame - return apply_display_filter(frame, mode=mode, sigma=2.0) + return self._filtered_frame(mode) + + def _filtered_frame(self, mode: str) -> np.ndarray: + key = (int(self.frame_idx), mode) + cached = self._filter_cache.get(key) + if cached is None: + cached = apply_display_filter(self._displayed_frame(), mode=mode, sigma=2.0) + if len(self._filter_cache) >= 8: + self._filter_cache.pop(next(iter(self._filter_cache))) + self._filter_cache[key] = cached + return cached def _resolve_detect_denoise(self, frame: np.ndarray) -> str: mode = self.detect_denoise @@ -2317,7 +2328,7 @@ def _update_frame(self, change=None): if self.show_detection_view: frame = self._detection_frame() elif self.denoise != "none": - frame = apply_display_filter(self._displayed_frame(), mode=self.denoise, sigma=2.0) + frame = self._filtered_frame(self.denoise) else: frame = self._displayed_frame() self.dp_stats = [ diff --git a/tests/test_showdiffraction_denoise.py b/tests/test_showdiffraction_denoise.py index 127d6edc..68e79f8f 100644 --- a/tests/test_showdiffraction_denoise.py +++ b/tests/test_showdiffraction_denoise.py @@ -216,6 +216,21 @@ def test_recover_predicted_rings_rescues_missed_reflection(): assert [x["hkl"] for x in sorted(w.rings, key=lambda y: y["radius_px"])] == list(hkls) +def test_filtered_frames_are_cached_per_mode(): + dp, _ = _spot_dp() + counts = np.random.default_rng(5).poisson(dp * 0.05).astype(np.float32) + + w = ShowDiffraction(counts, detect_denoise="anscombe", verbose=False) + first = w._detection_frame() + assert w._detection_frame() is first + + w.detect_denoise = "gaussian" + assert w._detection_frame() is not first + + w._ingest_data(counts[None]) + assert w._detection_frame() is not first + + def test_detect_denoise_state_and_validation(): dp, _ = _spot_dp() w = ShowDiffraction(dp.astype(np.float32), detect_denoise="gaussian", verbose=False) diff --git a/tests/test_showdiffraction_frontend_source.py b/tests/test_showdiffraction_frontend_source.py new file mode 100644 index 00000000..1fe5e8cc --- /dev/null +++ b/tests/test_showdiffraction_frontend_source.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +import pathlib + + +def test_showdiffraction_denoise_controls_bind_synced_traits() -> None: + repo_root = pathlib.Path(__file__).resolve().parents[1] + source = (repo_root / "js" / "showdiffraction" / "index.tsx").read_text( + encoding="utf-8" + ) + + assert 'useModelState("denoise")' in source + assert 'useModelState("detect_denoise")' in source + assert 'useModelState("show_detection_view")' in source + + assert ">Denoise" in source + assert ">Detect" in source + assert '"Detect View"' in source + + +def test_slow_denova_modes_stay_out_of_the_dropdowns() -> None: + repo_root = pathlib.Path(__file__).resolve().parents[1] + source = (repo_root / "js" / "showdiffraction" / "index.tsx").read_text( + encoding="utf-8" + ) + + assert '"denova_tv"' not in source + assert '"denova_tv12"' not in source