-
Notifications
You must be signed in to change notification settings - Fork 102
🆕 Define NucleusDetector
#967
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Jiaqi-Lv
wants to merge
52
commits into
dev-define-engines-abc
Choose a base branch
from
dev-define-nucleus-detection-engine
base: dev-define-engines-abc
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,540
−82
Open
Changes from 30 commits
Commits
Show all changes
52 commits
Select commit
Hold shift + click to select a range
767441d
copy code from old PR
Jiaqi-Lv d2a9702
preliminiary testing
Jiaqi-Lv 44c4994
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 0f8d4fe
initial prototype
Jiaqi-Lv d42b78a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] b7f829c
clean up
Jiaqi-Lv cba5fd5
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] f2cdcc4
update
Jiaqi-Lv dd99d97
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] c468a8b
update pipeline
Jiaqi-Lv 14f870a
update pipeline
Jiaqi-Lv 6e65fba
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 7eb916e
refactor code
Jiaqi-Lv 8442ac2
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] de83074
clean up
Jiaqi-Lv 17e5422
Merge branch 'dev-define-engines-abc' into dev-define-nucleus-detecti…
shaneahmed f5b1885
update patch mode processing
Jiaqi-Lv 551e43c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 12f985a
tidy up code
Jiaqi-Lv 05b2c7d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 2afbf8c
fix precommit
Jiaqi-Lv 367295d
update test
Jiaqi-Lv 7912abe
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 0a72e8b
improve tests
Jiaqi-Lv f8b4189
improve tests
Jiaqi-Lv 228731c
precommit
Jiaqi-Lv 6c26a0f
fix deepsource
Jiaqi-Lv 7ffea5b
fix deepsource
Jiaqi-Lv 79fc088
Merge branch 'dev-define-engines-abc' into dev-define-nucleus-detecti…
shaneahmed 198982f
Merge branch 'dev-define-engines-abc' into dev-define-nucleus-detecti…
shaneahmed 884bdf0
Merge branch 'dev-define-engines-abc' into dev-define-nucleus-detecti…
shaneahmed d63a7cc
refactor code and improve typing
Jiaqi-Lv a90f748
add test for map_overlap postprocessing
Jiaqi-Lv 2e004f1
refactor postprocessing and saving
Jiaqi-Lv 3638985
update save as zarr
Jiaqi-Lv af6d2c1
improve test coverage
Jiaqi-Lv 1974b2f
improve tests and address comments
Jiaqi-Lv 775e6a1
fix tests
Jiaqi-Lv b5f9c7d
:wrench: Add `run` function
shaneahmed 86e809d
use smaller wsi for testing
Jiaqi-Lv abf02f6
reduce code complexity
Jiaqi-Lv 5428618
add run params
Jiaqi-Lv dd74781
rename function
Jiaqi-Lv 89505ae
:construction: Review and minor changes.
shaneahmed f14e4cb
fix tests
Jiaqi-Lv 54a6447
fix tests
Jiaqi-Lv 656dbb9
fix tests
Jiaqi-Lv 10d09bd
update post processing and saving
Jiaqi-Lv b58dd88
fix tests
Jiaqi-Lv d2eae54
update model postproc function
Jiaqi-Lv d44d943
fix deepsource
Jiaqi-Lv b875e97
update RunParams
Jiaqi-Lv File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,225 @@ | ||
| """Tests for NucleusDetector.""" | ||
|
|
||
| import pathlib | ||
| import shutil | ||
| from collections.abc import Callable | ||
|
|
||
| import dask.array as da | ||
| import numpy as np | ||
| import pandas as pd | ||
| import pytest | ||
|
|
||
| from tiatoolbox.annotation.storage import SQLiteStore | ||
| from tiatoolbox.models.engine.nucleus_detector import NucleusDetector | ||
| from tiatoolbox.utils import env_detection as toolbox_env | ||
| from tiatoolbox.utils.misc import imwrite | ||
| from tiatoolbox.wsicore.wsireader import WSIReader | ||
|
|
||
| device = "cuda" if toolbox_env.has_gpu() else "cpu" | ||
|
|
||
|
|
||
| def _rm_dir(path: pathlib.Path) -> None: | ||
| """Helper func to remove directory.""" | ||
| if pathlib.Path(path).exists(): | ||
| shutil.rmtree(path, ignore_errors=True) | ||
|
|
||
|
|
||
| def check_output(path: pathlib.Path) -> None: | ||
| """Check NucleusDetector output.""" | ||
|
|
||
|
|
||
| def test_nucleus_detection_nms_empty_dataframe() -> None: | ||
| """nucleus_detection_nms should return a copy for empty inputs.""" | ||
| df = pd.DataFrame(columns=["x", "y", "type", "prob"]) | ||
|
|
||
| result = NucleusDetector.nucleus_detection_nms(df, radius=3) | ||
|
|
||
| assert result.empty | ||
| assert result is not df | ||
| assert list(result.columns) == ["x", "y", "type", "prob"] | ||
|
|
||
|
|
||
| def test_nucleus_detection_nms_invalid_radius() -> None: | ||
| """Radius must be strictly positive.""" | ||
| df = pd.DataFrame({"x": [0], "y": [0], "type": [1], "prob": [0.9]}) | ||
|
|
||
| with pytest.raises(ValueError, match="radius must be > 0"): | ||
| NucleusDetector.nucleus_detection_nms(df, radius=0) | ||
|
|
||
|
|
||
| def test_nucleus_detection_nms_invalid_overlap_threshold() -> None: | ||
| """overlap_threshold must lie in (0, 1].""" | ||
| df = pd.DataFrame({"x": [0], "y": [0], "type": [1], "prob": [0.9]}) | ||
|
|
||
| message = r"overlap_threshold must be in \(0\.0, 1\.0\], got 0" | ||
| with pytest.raises(ValueError, match=message): | ||
| NucleusDetector.nucleus_detection_nms(df, radius=1, overlap_threshold=0) | ||
|
|
||
|
|
||
| def test_nucleus_detection_nms_suppresses_overlapping_detections() -> None: | ||
| """Lower-probability overlapping detections are removed.""" | ||
| df = pd.DataFrame( | ||
| { | ||
| "x": [2, 0, 20], | ||
| "y": [1, 0, 20], | ||
| "type": [1, 1, 2], | ||
| "prob": [0.6, 0.9, 0.7], | ||
| } | ||
| ) | ||
|
|
||
| result = NucleusDetector.nucleus_detection_nms(df, radius=5) | ||
|
|
||
| expected = pd.DataFrame( | ||
| {"x": [0, 20], "y": [0, 20], "type": [1, 2], "prob": [0.9, 0.7]} | ||
| ) | ||
| pd.testing.assert_frame_equal(result.reset_index(drop=True), expected) | ||
|
|
||
|
|
||
| def test_nucleus_detection_nms_suppresses_across_types() -> None: | ||
| """Overlapping detections of different types are also suppressed.""" | ||
| df = pd.DataFrame( | ||
| { | ||
| "x": [0, 0, 20], | ||
| "y": [0, 0, 0], | ||
| "type": [1, 2, 1], | ||
| "prob": [0.6, 0.95, 0.4], | ||
| } | ||
| ) | ||
|
|
||
| result = NucleusDetector.nucleus_detection_nms(df, radius=5) | ||
|
|
||
| expected = pd.DataFrame( | ||
| {"x": [0, 20], "y": [0, 0], "type": [2, 1], "prob": [0.95, 0.4]} | ||
| ) | ||
| pd.testing.assert_frame_equal(result.reset_index(drop=True), expected) | ||
|
|
||
|
|
||
| def test_nucleus_detection_nms_retains_non_overlapping_candidates() -> None: | ||
| """Detections with IoU below the threshold are preserved.""" | ||
| df = pd.DataFrame( | ||
| { | ||
| "x": [0, 10], | ||
| "y": [0, 0], | ||
| "type": [1, 1], | ||
| "prob": [0.8, 0.5], | ||
| } | ||
| ) | ||
|
|
||
| result = NucleusDetector.nucleus_detection_nms(df, radius=5, overlap_threshold=0.5) | ||
|
|
||
| expected = pd.DataFrame( | ||
| {"x": [0, 10], "y": [0, 0], "type": [1, 1], "prob": [0.8, 0.5]} | ||
| ) | ||
| pd.testing.assert_frame_equal(result.reset_index(drop=True), expected) | ||
|
|
||
|
|
||
| def test_nucleus_detector_wsi(remote_sample: Callable, tmp_path: pathlib.Path) -> None: | ||
| """Test for nucleus detection engine.""" | ||
| mini_wsi_svs = pathlib.Path(remote_sample("wsi4_512_512_svs")) | ||
|
|
||
| pretrained_model = "mapde-conic" | ||
|
|
||
| save_dir = tmp_path | ||
|
|
||
| nucleus_detector = NucleusDetector(model=pretrained_model) | ||
| _ = nucleus_detector.run( | ||
| patch_mode=False, | ||
| device=device, | ||
| output_type="annotationstore", | ||
| memory_threshold=50, | ||
| images=[mini_wsi_svs], | ||
| save_dir=save_dir, | ||
| overwrite=True, | ||
| ) | ||
|
|
||
| store = SQLiteStore.open(save_dir / "wsi4_512_512.db") | ||
| assert len(store.values()) == 281 | ||
| store.close() | ||
|
|
||
| _rm_dir(save_dir) | ||
|
|
||
|
|
||
| def test_nucleus_detector_patch( | ||
| remote_sample: Callable, tmp_path: pathlib.Path | ||
| ) -> None: | ||
| """Test for nucleus detection engine in patch mode.""" | ||
| mini_wsi_svs = pathlib.Path(remote_sample("wsi4_512_512_svs")) | ||
|
|
||
| wsi_reader = WSIReader.open(mini_wsi_svs) | ||
| patch_1 = wsi_reader.read_rect((0, 0), (252, 252), resolution=0.5, units="mpp") | ||
| patch_2 = wsi_reader.read_rect((252, 252), (252, 252), resolution=0.5, units="mpp") | ||
|
|
||
| pretrained_model = "mapde-conic" | ||
|
|
||
| save_dir = tmp_path | ||
|
|
||
| nucleus_detector = NucleusDetector(model=pretrained_model) | ||
| _ = nucleus_detector.run( | ||
| patch_mode=True, | ||
| device=device, | ||
| output_type="annotationstore", | ||
| memory_threshold=50, | ||
| images=[patch_1, patch_2], | ||
| save_dir=save_dir, | ||
| overwrite=True, | ||
| class_dict=None, | ||
| ) | ||
|
|
||
| store_1 = SQLiteStore.open(save_dir / "0.db") | ||
| assert len(store_1.values()) == 270 | ||
| store_1.close() | ||
|
|
||
| store_2 = SQLiteStore.open(save_dir / "1.db") | ||
| assert len(store_2.values()) == 52 | ||
| store_2.close() | ||
|
|
||
| imwrite(save_dir / "patch_0.png", patch_1) | ||
| imwrite(save_dir / "patch_1.png", patch_2) | ||
| _ = nucleus_detector.run( | ||
| patch_mode=True, | ||
| device=device, | ||
| output_type="zarr", | ||
| memory_threshold=50, | ||
| images=[save_dir / "patch_0.png", save_dir / "patch_1.png"], | ||
| save_dir=save_dir, | ||
| overwrite=True, | ||
| ) | ||
|
|
||
| store_1 = SQLiteStore.open(save_dir / "patch_0.db") | ||
| assert len(store_1.values()) == 270 | ||
| store_1.close() | ||
|
|
||
| store_2 = SQLiteStore.open(save_dir / "patch_1.db") | ||
| assert len(store_2.values()) == 52 | ||
| store_2.close() | ||
|
|
||
| _rm_dir(save_dir) | ||
|
|
||
|
|
||
| def test_nucleus_detector_write_centroid_maps(tmp_path: pathlib.Path) -> None: | ||
| """Test for _write_centroid_maps function.""" | ||
| detection_maps = np.zeros((20, 20, 1), dtype=np.uint8) | ||
| detection_maps = da.from_array(detection_maps, chunks=(20, 20, 1)) | ||
|
|
||
| store = NucleusDetector.write_centroid_maps_to_store( | ||
| detection_maps=detection_maps, class_dict=None | ||
| ) | ||
| assert len(store.values()) == 0 | ||
| store.close() | ||
|
|
||
| detection_maps = np.zeros((20, 20, 1), dtype=np.uint8) | ||
| detection_maps[10, 10, 0] = 1 | ||
| detection_maps = da.from_array(detection_maps, chunks=(20, 20, 1)) | ||
| _ = NucleusDetector.write_centroid_maps_to_store( | ||
| detection_maps=detection_maps, | ||
| save_path=tmp_path / "test.db", | ||
| class_dict={0: "nucleus"}, | ||
| ) | ||
| store = SQLiteStore.open(tmp_path / "test.db") | ||
| assert len(store.values()) == 1 | ||
| annotation = next(iter(store.values())) | ||
| print(annotation) | ||
| assert annotation.properties["type"] == "nucleus" | ||
| assert annotation.geometry.centroid.x == 10.0 | ||
| assert annotation.geometry.centroid.y == 10.0 | ||
| store.close() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.