Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
101 commits
Select commit Hold shift + click to select a range
b5ee428
fix zarr checking
measty Jan 16, 2025
1ac61c5
use cross-version syntax for zarr.group
measty Jan 16, 2025
f12b048
more zarr v3 changes
measty Jan 16, 2025
a0e1650
Merge branch 'develop' into fix-zarr-check
shaneahmed Jan 24, 2025
9d19a5e
:pushpin: Remove Pin from Zarr
shaneahmed Jan 31, 2025
2212eef
Merge branch 'develop' into fix-zarr-check
shaneahmed Feb 7, 2025
eb4cecd
Merge branch 'develop' into fix-zarr-check
shaneahmed Feb 21, 2025
4b65046
Merge branch 'develop' into fix-zarr-check
shaneahmed Mar 3, 2025
67e5088
Merge branch 'develop' into fix-zarr-check
shaneahmed Mar 19, 2025
cba6f24
Merge branch 'develop' into fix-zarr-check
shaneahmed Apr 11, 2025
5557be5
Merge branch 'develop' into fix-zarr-check
shaneahmed Apr 25, 2025
5c9144f
Merge branch 'develop' into fix-zarr-check
shaneahmed May 23, 2025
1b4da41
Merge branch 'develop' into fix-zarr-check
shaneahmed Jun 9, 2025
1eee555
Merge branch 'develop' into fix-zarr-check
shaneahmed Jun 13, 2025
cc36cc7
Merge branch 'develop' into fix-zarr-check
shaneahmed Jun 20, 2025
c93c3da
Merge branch 'develop' into fix-zarr-check
shaneahmed Jul 11, 2025
a2dd86f
Merge branch 'develop' into fix-zarr-check
shaneahmed Aug 15, 2025
8ea2ff6
Merge branch 'develop' into fix-zarr-check
shaneahmed Sep 5, 2025
c2b79b9
Merge branch 'develop' into fix-zarr-check
shaneahmed Oct 3, 2025
beb3d24
Merge branch 'develop' into fix-zarr-check
shaneahmed Oct 10, 2025
464f3ec
Merge branch 'develop' into fix-zarr-check
shaneahmed Oct 16, 2025
32df9b8
Merge branch 'develop' into fix-zarr-check
shaneahmed Mar 5, 2026
4c226a0
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 5, 2026
e6484ac
Merge branch 'develop' into fix-zarr-check
shaneahmed Mar 12, 2026
ec53cc9
:pushpin: Pin `zarr` and `tifffile`
shaneahmed Mar 12, 2026
b25838b
:pushpin: Pin `tifffile`
shaneahmed Mar 12, 2026
9ca7b4d
:pushpin: Update Python Versions
shaneahmed Mar 12, 2026
0bb7d6a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 12, 2026
06c041f
:bug: Fix setup.py to include 3.14
shaneahmed Mar 12, 2026
c1e5c97
Merge remote-tracking branch 'origin/dev-remove-python3.10' into dev-…
shaneahmed Mar 12, 2026
6d2c84a
:fire: Remove openslide from requirements.conda.yml
shaneahmed Mar 12, 2026
ff72d53
:fire: Remove Python 3.14
shaneahmed Mar 12, 2026
bdafc46
Merge branch 'dev-remove-python3.10' into fix-zarr-check
shaneahmed Mar 12, 2026
01492d2
Merge branch 'develop' into fix-zarr-check
shaneahmed Mar 16, 2026
11937c8
:bug: Fix Semantic Segmentor
shaneahmed Mar 16, 2026
e137acf
:bug: Fix misc.py
shaneahmed Mar 17, 2026
fe9e16c
:bug: Fix `wsireader.py`
shaneahmed Mar 17, 2026
d12bb1c
:white_check_mark: Force skip test
shaneahmed Mar 18, 2026
2522a54
:bug: Fix NGFF Reader
shaneahmed Mar 19, 2026
9e162ab
:bug: Fix NGFF Reader
shaneahmed Mar 19, 2026
4beb04a
:white_check_mark: Use FsspecStore for remote files.
shaneahmed Mar 19, 2026
750ca02
Merge remote-tracking branch 'origin/fix-zarr-check' into fix-zarr-check
shaneahmed Mar 19, 2026
efd7765
:recycle: Update how `register_codec` is used
shaneahmed Mar 19, 2026
bd7299a
:white_check_mark: Add support to read s3 using NGFFWSIReader
shaneahmed Mar 19, 2026
62daa88
:pencil2: Fix typos
shaneahmed Mar 19, 2026
33d9c8f
:bug: Fix `nucleus_detector.py`
shaneahmed Mar 23, 2026
3fc7f73
:bug: Zarr uses str indexing instead of int
shaneahmed Mar 23, 2026
a1b86ff
:white_check_mark: Test with np full
shaneahmed Mar 25, 2026
0edae64
:white_check_mark: Fix mtsegmentor patches and tiles_no_metadata
shaneahmed Mar 25, 2026
4c8633c
:white_check_mark: Fix mtsegmentor patches
shaneahmed Mar 25, 2026
2a67908
:white_check_mark: Fix test_single_task_mtsegmentor
shaneahmed Mar 25, 2026
e904129
:white_check_mark: Fix test_wsi_mtsegmentor_correct_nonsquare_shape a…
shaneahmed Mar 25, 2026
a99d744
:white_check_mark: Fix test_wsi_segmentor_annotationstore
shaneahmed Mar 25, 2026
0cfe3a1
Merge branch 'develop' into fix-zarr-check
shaneahmed Mar 26, 2026
2dcc471
:white_check_mark: Fix test_micronet_output
shaneahmed Mar 26, 2026
9ac1ad2
Merge branch 'develop' into fix-zarr-check
shaneahmed Mar 30, 2026
af22da9
Merge branch 'develop' into fix-zarr-check
shaneahmed Mar 31, 2026
306a1f7
:arrow_up: `FsspecJsonWSIReader` Zarr 3 Fix (#1049)
aacic Apr 2, 2026
ec74cb2
Merge branch 'develop' into fix-zarr-check
shaneahmed Apr 7, 2026
65ade70
Merge branch 'develop' into fix-zarr-check
shaneahmed Apr 9, 2026
1f387e4
fix mypy errors
Jiaqi-Lv Apr 15, 2026
aefa5d8
:bug: Fix missing mask for contours
shaneahmed Apr 16, 2026
6f9e6b7
:hammer: Use `skip` for follow imports
shaneahmed Apr 16, 2026
07b4746
fix mypy type errors
Jiaqi-Lv Apr 16, 2026
d7d95d2
:hammer: Mark s3 test for NGFF as expected to fail
shaneahmed Apr 17, 2026
7a70666
:bug: Fix deepsource error
shaneahmed Apr 17, 2026
b759b0c
:bug: Fix deepsource error cyclomatic complexity too high.
shaneahmed Apr 17, 2026
58762df
:bug: Fix instance test with zarr.Array
shaneahmed Apr 17, 2026
ad6cf1f
:white_check_mark: Add tests for coverage
shaneahmed Apr 21, 2026
2d3f874
:white_check_mark: Add tests for wsireader coverage
shaneahmed Apr 22, 2026
8bc83c8
:white_check_mark: Add tests for multi_task_segmentor coverage
shaneahmed Apr 22, 2026
4e8dc5c
:fire: Remove dtype object check
shaneahmed Apr 22, 2026
845749a
:bug: Fix "store" attribute error with dictionary
shaneahmed Apr 22, 2026
5b120b4
:bulb: Address Co-Pilot comments
shaneahmed Apr 22, 2026
7f8dcd2
:bulb: Address Co-Pilot comments
shaneahmed Apr 22, 2026
be395e2
:white_check_mark: Add tests to improve coverage
shaneahmed Apr 22, 2026
38efdd9
:white_check_mark: Add tests to improve coverage
shaneahmed Apr 24, 2026
ccb0a14
:technologist: Address quality check issues
shaneahmed Apr 24, 2026
0d341d7
Merge branch 'develop' into fix-zarr-check
shaneahmed Apr 29, 2026
4534250
:bug: Fix pip install workflow
shaneahmed Apr 29, 2026
c0a72af
Merge branch 'develop' into fix-zarr-check
shaneahmed May 6, 2026
2abff8b
:bug: Replace `create_dataset` with `create_array`
shaneahmed May 6, 2026
9dd0a71
:bug: Fix test_clear_zarr
shaneahmed May 6, 2026
8e4dca6
:bug: Fix chunksize 0 unsupported by zarr v3.2.0+
shaneahmed May 6, 2026
323eca1
add cerberus initial attempt
measty May 7, 2026
f190f83
Merge branch 'fix-zarr-check' of https://github.com/TissueImageAnalyt…
measty May 7, 2026
46119f5
remove alternative architectures
measty May 8, 2026
6d16f3a
add test
measty May 8, 2026
d981346
Merge branch 'develop' into add-cerberus
shaneahmed May 8, 2026
25d8171
restructure code
measty May 8, 2026
5c5e304
add tests
measty May 8, 2026
a6bed60
Merge branch 'add-cerberus' of https://github.com/TissueImageAnalytic…
measty May 8, 2026
6a6cc40
halo postproc
measty May 22, 2026
27e84dc
fix broken margin behaviour
measty May 22, 2026
159959f
Merge branch 'develop' of https://github.com/TissueImageAnalytics/tia…
measty May 27, 2026
16ba015
add docstrings
measty May 27, 2026
280d65b
deepsource fixes
measty May 27, 2026
0c4ca2f
mypy fixes
measty May 27, 2026
1eed5af
fix test
measty May 27, 2026
a572ac5
add tests
measty May 27, 2026
6d5c595
postproc test
measty May 27, 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
257 changes: 256 additions & 1 deletion tests/engines/test_multi_task_segmentor.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@
DaskDelayedJSONStore,
MultiTaskSegmentor,
_clear_zarr,
_crop_halo_post_process_output,
_get_postproc_tile_read_bounds,
_get_sel_indices_margin_lines,
_normalise_postproc_halo,
_post_save_json_store,
_process_instance_predictions,
_save_multitask_vertical_to_cache,
Expand Down Expand Up @@ -889,9 +892,10 @@ class FakeVM:
)

# --- Call function ---
new_zarr, new_da = _save_multitask_vertical_to_cache(
new_zarr, new_da, zarr_group = _save_multitask_vertical_to_cache(
probabilities_zarr=probabilities_zarr,
probabilities_da=probabilities_da,
zarr_group=None,
probabilities=probabilities,
idx=idx,
tqdm_loop=tqdm_loop,
Expand All @@ -905,11 +909,44 @@ class FakeVM:

# new_zarr must be a real zarr array
assert isinstance(new_zarr[idx], zarr.Array)
assert zarr_group is not None

# Data was written correctly
assert np.array_equal(new_zarr[idx][:], np.array([[1, 2, 3]]))


def test_multitask_vertical_merge_continues_after_zarr_spill(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Test multitask vertical merge appends all chunks after spilling to Zarr."""

class FakeVM:
"""Fake psutil.virtual_memory() with extremely low available memory."""

available = 1

monkeypatch.setattr(psutil, "virtual_memory", FakeVM)

values = np.arange(8 * 3, dtype=np.float32).reshape(8, 3, 1)
canvas = [da.from_array(values, chunks=(2, 3, 1))]
count = [da.from_array(np.ones_like(values), chunks=(2, 3, 1))]
output_locs_y = np.array([[0, 2], [2, 4], [4, 6], [6, 8]])

result = merge_multitask_vertical_chunkwise(
canvas=canvas,
count=count,
output_locs_y_=output_locs_y,
zarr_group=None,
save_path=tmp_path / "vertical.zarr",
memory_threshold=0,
output_shape=(8, 3),
verbose=False,
)

assert result[0].shape == values.shape
assert np.array_equal(result[0].compute(), values)


def test_qupath_feature_class_dict_lookup_fails() -> None:
"""Test qupath_feature_class_dict lookup fails."""
qupath_json = DaskDelayedJSONStore.__new__(DaskDelayedJSONStore)
Expand Down Expand Up @@ -1079,6 +1116,215 @@ def test_get_tile_info_small_image_triggers_early_return(
assert np.all(flag == 0)


def test_postproc_halo_bounds_and_output_crop() -> None:
"""Test halo-expanded tile output is cropped and shifted to core space."""
halo_xy = _normalise_postproc_halo((3, 2))
assert np.array_equal(halo_xy, np.array([2, 3]))

read_bounds = _get_postproc_tile_read_bounds(
tile_bounds=(4, 5, 10, 11),
postproc_halo_xy=halo_xy,
image_shape=(12, 13),
)
assert read_bounds == (2, 2, 12, 13)

predictions = np.arange(11 * 10).reshape(11, 10)
info_dict = {
"box": np.array(
[
[2, 3, 4, 5],
[5, 6, 7, 8],
[9, 6, 11, 8],
],
dtype=np.int32,
),
"centroid": np.array(
[
[3, 4],
[6, 7],
[10, 7],
],
dtype=np.float32,
),
"contours": np.array(
[
[[2, 3], [4, 3], [4, 5], [2, 5]],
[[5, 6], [7, 6], [7, 8], [5, 8]],
[[9, 6], [11, 6], [11, 8], [9, 8]],
],
dtype=np.int32,
),
"type": np.array([1, 2, 3], dtype=np.int32),
}

cropped = _crop_halo_post_process_output(
post_process_output=(
{
"task_type": "gland",
"seg_type": "instance",
"predictions": predictions,
"info_dict": info_dict,
},
),
tile_bounds=(4, 5, 10, 11),
tile_read_bounds=read_bounds,
)[0]

assert np.array_equal(cropped["predictions"], predictions[3:9, 2:8])
assert np.array_equal(cropped["info_dict"]["type"], np.array([1, 2]))
assert np.array_equal(
cropped["info_dict"]["box"],
np.array([[0, 0, 2, 2], [3, 3, 5, 5]], dtype=np.int32),
)
assert np.array_equal(
cropped["info_dict"]["centroid"],
np.array([[1, 1], [4, 4]], dtype=np.float32),
)
assert np.array_equal(
cropped["info_dict"]["contours"][0],
np.array([[0, 0], [2, 0], [2, 2], [0, 2]], dtype=np.int32),
)


def test_postproc_halo_ownership_without_centroids() -> None:
"""Test halo ownership falls back to boxes and padded contours."""
read_bounds = (2, 2, 12, 13)
predictions = np.arange(11 * 10).reshape(11, 10)

box_cropped = _crop_halo_post_process_output(
post_process_output=(
{
"task_type": "gland",
"seg_type": "instance",
"predictions": predictions,
"info_dict": {
"box": np.array(
[
[2, 3, 4, 5],
[9, 6, 11, 8],
],
dtype=np.int32,
),
"type": np.array([1, 2], dtype=np.int32),
},
},
),
tile_bounds=(4, 5, 10, 11),
tile_read_bounds=read_bounds,
)[0]
assert np.array_equal(
box_cropped["info_dict"]["box"],
np.array([[0, 0, 2, 2]], dtype=np.int32),
)
assert np.array_equal(box_cropped["info_dict"]["type"], np.array([1]))

pad_value = np.iinfo(np.int32).min
contour_cropped = _crop_halo_post_process_output(
post_process_output=(
{
"task_type": "gland",
"seg_type": "instance",
"predictions": predictions,
"info_dict": {
"contours": np.array(
[
[[2, 3], [4, 3], [4, 5], [2, 5]],
[[9, 6], [11, 6], [11, 8], [9, 8]],
[[pad_value, pad_value]] * 4,
],
dtype=np.int32,
),
"type": np.array([1, 2, 3], dtype=np.int32),
},
},
),
tile_bounds=(4, 5, 10, 11),
tile_read_bounds=read_bounds,
)[0]
assert np.array_equal(contour_cropped["info_dict"]["type"], np.array([1]))
assert np.array_equal(
contour_cropped["info_dict"]["contours"][0],
np.array([[0, 0], [2, 0], [2, 2], [0, 2]], dtype=np.int32),
)


def test_process_tile_mode_uses_postproc_halo(
track_tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test tile mode expands reads and crops outputs when halo is set."""
seg = MultiTaskSegmentor.__new__(MultiTaskSegmentor)
seg.verbose = False
seg.num_workers = 1
seg.mask_bounds = (0, 0, 10, 10)
seg.mask_padding = (0, 0, 0, 0)
seg.dataloader = SimpleNamespace(
dataset=SimpleNamespace(
reader=SimpleNamespace(slide_dimensions=lambda **_: (10, 10)),
),
)
seg._ioconfig = SimpleNamespace(
highest_input_resolution={},
tile_shape=(4, 4),
to_baseline=lambda: SimpleNamespace(margin=0),
)

tile_info_sets = [
[
np.array([[2, 2, 6, 6]], dtype=np.int32),
np.array([[1, 1, 1, 1]], dtype=np.int32),
],
[
np.array([[6, 6, 10, 10]], dtype=np.int32),
np.array([[1, 1, 1, 1]], dtype=np.int32),
],
]
seg._get_tile_info = lambda **_: tile_info_sets
recorded_bounds = []
expanded_predictions = np.arange(64, dtype=np.uint8).reshape(8, 8)

def _compute_tile(tile_bounds: tuple[int, int, int, int]) -> tuple[dict]:
"""Return one halo-expanded post-processing output."""
recorded_bounds.append(tile_bounds)
return (
{
"task_type": "instance",
"seg_type": "instance",
"predictions": expanded_predictions,
"info_dict": {
"box": np.empty((0, 4), dtype=np.int32),
"centroid": np.empty((0, 2), dtype=np.float32),
"contours": np.empty((0, 0, 2), dtype=np.int32),
"prob": np.empty((0,), dtype=np.float32),
"type": np.empty((0,), dtype=np.int32),
},
},
)

seg._compute_tile = _compute_tile
monkeypatch.setattr(
"tiatoolbox.models.engine.multi_task_segmentor.tqdm_dask_progress_bar",
lambda **kwargs: kwargs["write_tasks"],
)

output = seg._process_tile_mode(
probabilities=[da.zeros((10, 10, 1), chunks=(10, 10, 1))],
save_path=track_tmp_path / "halo.zarr",
memory_threshold=100,
return_predictions=(True,),
postproc_halo=2,
)

assert recorded_bounds == [(0, 0, 8, 8)]
assert len(output) == 1
predictions = output[0]["predictions"]
assert np.array_equal(predictions[2:6, 2:6], expanded_predictions[2:6, 2:6])
assert np.count_nonzero(predictions[:2, :]) == 0
assert np.count_nonzero(predictions[:, :2]) == 0
assert np.count_nonzero(predictions[6:, :]) == 0
assert np.count_nonzero(predictions[:, 6:]) == 0


class FakeSeg(MultiTaskSegmentor):
"""Minimal subclass that allows us to override internals cleanly."""

Expand Down Expand Up @@ -1166,6 +1412,7 @@ def fake_store_probabilities(
*_: Any, # noqa: ANN401
**__: Any, # noqa: ANN401
) -> tuple[zarr.Array | None, da.Array | None]:
"""Record unexpected probability-store calls during merge tests."""
nonlocal called_store
called_store = True
return None, None
Expand Down Expand Up @@ -1612,26 +1859,31 @@ def test_post_save_json_store_deletes_empty_store(
# ---- Proxy object that LOOKS like a zarr.Group ----
class GroupProxy:
def __init__(self: GroupProxy, group: zarr.Group, path: Path | str) -> None:
"""Wrap a Zarr group with a path used by cleanup code."""
self._group = group
self.path = path
self.store = group.store

# Make isinstance(proxy, zarr.Group) return True
@property
def __class__(self: GroupProxy) -> type[zarr.Group]:
"""Expose the wrapped object as a Zarr group for isinstance."""
return zarr.Group

# Delegate attribute access
def __getattr__(
self: GroupProxy, item: str
) -> zarr.Group | zarr.Array | str | int | float | Iterable[str]:
"""Delegate unknown attributes to the wrapped Zarr group."""
return getattr(self._group, item)

# Delegate mapping behavior
def keys(self: GroupProxy) -> Iterable[str]:
"""Return keys from the wrapped Zarr group."""
return self._group.keys()

def __getitem__(self: GroupProxy, item: str) -> zarr.Group | zarr.Array:
"""Return an item from the wrapped Zarr group."""
return self._group[item]

processed_predictions = GroupProxy(root, "dummy")
Expand All @@ -1640,6 +1892,7 @@ def __getitem__(self: GroupProxy, item: str) -> zarr.Group | zarr.Array:
called = {"flag": False}

def fake_rmtree(path: Path | str, *, ignore_errors: bool) -> None: # noqa: ARG001
"""Record that cleanup attempted to remove an empty Zarr store."""
called["flag"] = True

monkeypatch.setattr(shutil, "rmtree", fake_rmtree)
Expand Down Expand Up @@ -1723,6 +1976,7 @@ def fake_save_qupath_json(
save_path: Path | None, # noqa: ARG001
qupath_json: dict[str, Any],
) -> dict[str, Any]:
"""Return generated QuPath JSON instead of writing it to disk."""
return qupath_json

monkeypatch.setattr(
Expand Down Expand Up @@ -1759,6 +2013,7 @@ def _build_single_qupath_feature(
scale_factor: tuple[float, float],
class_colors: dict[int, Any],
) -> dict[str, Any]:
"""Delegate feature construction to the production JSON store."""
return DaskDelayedJSONStore._build_single_qupath_feature(
self, i, class_dict, origin, scale_factor, class_colors
)
Expand Down
Loading
Loading