Skip to content
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

🆕Multichannel Image Reading #825

Draft
wants to merge 58 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
cfba642
UPD: fix bound
vqdang Jan 29, 2024
8979008
Merge branch 'develop' into bugfix-tiffwsireader-readbounds
shaneahmed Feb 2, 2024
84af337
Merge branch 'develop' into bugfix-tiffwsireader-readbounds
shaneahmed Feb 2, 2024
6aab409
Merge branch 'develop' into bugfix-tiffwsireader-readbounds
shaneahmed Feb 16, 2024
e09e216
Merge branch 'develop' into bugfix-tiffwsireader-readbounds
shaneahmed Feb 21, 2024
249a716
Merge branch 'develop' into bugfix-tiffwsireader-readbounds
shaneahmed Mar 1, 2024
246b2c8
Merge branch 'develop' into bugfix-tiffwsireader-readbounds
shaneahmed Mar 15, 2024
817a526
Merge branch 'develop' into bugfix-tiffwsireader-readbounds
shaneahmed Mar 19, 2024
28a324f
Merge branch 'develop' into bugfix-tiffwsireader-readbounds
shaneahmed Apr 16, 2024
ade11ba
Merge branch 'develop' into bugfix-tiffwsireader-readbounds
shaneahmed Apr 26, 2024
f4ac670
Merge branch 'develop' into bugfix-tiffwsireader-readbounds
shaneahmed Apr 29, 2024
87450b2
Merge branch 'develop' into bugfix-tiffwsireader-readbounds
Abdol May 13, 2024
6fdf9e7
✅ Add test
May 13, 2024
13d3293
Merge branch 'develop' into bugfix-tiffwsireader-readbounds
Abdol May 14, 2024
b5c8861
Merge branch 'develop' into bugfix-tiffwsireader-readbounds
Abdol Jun 7, 2024
a1acbfd
Merge branch 'develop' into bugfix-tiffwsireader-readbounds
shaneahmed Jun 14, 2024
60361c2
Merge branch 'develop' into bugfix-tiffwsireader-readbounds
shaneahmed Jun 21, 2024
609469d
initial draft multichannel reading
measty Jun 21, 2024
aec8390
deepsource
measty Jun 21, 2024
4972332
make .tif be recognised same as .tiff
measty Jun 24, 2024
dccf4db
fix read_rect
measty Jun 26, 2024
bcefc31
optimizations
measty Jun 26, 2024
79dcd51
fix typo
measty Jun 26, 2024
a80655b
get colors from metadata if poss.
measty Jul 2, 2024
83c6954
deepsource
measty Jul 2, 2024
6e58ec5
:white_check_mark: Adding test for mutli_channel visualisation
behnazelhaminia Jul 5, 2024
c4e4232
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 5, 2024
5563e36
replace depreciated funcTickFormatter
measty Jul 5, 2024
79040ca
Merge branch 'multichannel-reading' of https://github.com/TissueImage…
measty Jul 5, 2024
cc66c53
:white_check_mark: Adding test for mutli_channel visualisation
behnazelhaminia Jul 5, 2024
7f4423c
drop background autofluorescence channel if appropriate
measty Aug 7, 2024
e9222ea
Merge branch 'multichannel-reading' of https://github.com/TissueImage…
measty Aug 7, 2024
ba26d1d
Merge branch 'develop' of https://github.com/TissueImageAnalytics/tia…
measty Aug 7, 2024
8e2c11a
remove a print
measty Aug 7, 2024
fad00c3
:white_check_mark: Assert shape and interpolation
behnazelhaminia Aug 9, 2024
464a5a2
:white_check_mark: Test for multichannel visualisation
behnazelhaminia Aug 9, 2024
b380fb4
Merge remote-tracking branch 'origin/multichannel-reading' into multi…
behnazelhaminia Aug 9, 2024
75bbdbd
:white_check_mark: Test for multichannel visualisation
behnazelhaminia Aug 9, 2024
b148ed3
Update remote_samples.yaml
behnazelhaminia Aug 9, 2024
d27f34d
Merge branch 'develop' into multichannel-reading
shaneahmed Aug 9, 2024
93ed2d1
first channel selector draft
measty Aug 21, 2024
7f5b61c
Merge branch 'multichannel-reading' of https://github.com/TissueImage…
measty Aug 21, 2024
a3992d1
Merge branch 'multichannel-reading' of https://github.com/TissueImage…
measty Aug 21, 2024
df5a59d
multichannel ui update
measty Aug 22, 2024
e342290
Merge branch 'develop' of https://github.com/TissueImageAnalytics/tia…
measty Aug 22, 2024
6970bae
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 22, 2024
063aba8
fix a test
measty Aug 22, 2024
71dcf80
Merge branch 'multichannel-reading' of https://github.com/TissueImage…
measty Aug 22, 2024
00eb70e
more efficient channel select ui
measty Aug 25, 2024
883ce40
multichannel ui layout tweak
measty Aug 29, 2024
9df1566
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 29, 2024
ccb62d0
Merge branch 'develop' into multichannel-reading
shaneahmed Sep 20, 2024
edd6f63
robustify level sort
measty Sep 21, 2024
4b17e1e
Merge branch 'multichannel-reading' of https://github.com/TissueImage…
measty Sep 21, 2024
437345d
Merge branch 'develop' of https://github.com/TissueImageAnalytics/tia…
measty Sep 21, 2024
1bb78d8
Merge remote-tracking branch 'origin/multichannel-reading' into multi…
behnazelhaminia Sep 26, 2024
3a024dd
:white_check_mark: Adding small multiplxed image
behnazelhaminia Sep 26, 2024
d4bb297
Merge branch 'develop' into multichannel-reading
shaneahmed Oct 4, 2024
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
1 change: 1 addition & 0 deletions tests/test_app_bokeh.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ def run_app() -> None:
title="Tiatoolbox TileServer",
layers={},
)
app.json.sort_keys = False
CORS(app, send_wildcard=True)
app.run(host="127.0.0.1", threaded=True)

Expand Down
22 changes: 22 additions & 0 deletions tests/test_wsireader.py
Original file line number Diff line number Diff line change
Expand Up @@ -1482,6 +1482,7 @@ def test_wsireader_open(
sample_ome_tiff: Path,
sample_ventana_tif: Path,
sample_regular_tif: Path,
sample_qptiff: Path,
source_image: Path,
tmp_path: pytest.TempPathFactory,
) -> None:
Expand Down Expand Up @@ -1513,6 +1514,9 @@ def test_wsireader_open(
wsi = WSIReader.open(Path(source_image))
assert isinstance(wsi, wsireader.VirtualWSIReader)

wsi = WSIReader.open(sample_qptiff)
assert isinstance(wsi, wsireader.TIFFWSIReader)

img = utils.misc.imread(str(Path(source_image)))
wsi = WSIReader.open(input_img=img)
assert isinstance(wsi, wsireader.VirtualWSIReader)
Expand Down Expand Up @@ -2581,6 +2585,11 @@ def test_jp2_no_header(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"sample_key": "jp2-omnyx-small",
"kwargs": {},
},
{
"reader_class": TIFFWSIReader,
"sample_key": "qptiff_sample",
"kwargs": {},
},
],
ids=[
"AnnotationReaderOverlaid",
Expand All @@ -2591,6 +2600,7 @@ def test_jp2_no_header(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"NGFFWSIReader",
"OpenSlideWSIReader (Small SVS)",
"OmnyxJP2WSIReader",
"TIFFReader_Multichannel",
],
)
def wsi(request: requests.request, remote_sample: Callable) -> WSIReader:
Expand Down Expand Up @@ -2812,3 +2822,15 @@ def test_read_multi_channel(source_image: Path) -> None:
assert region.shape == (100, 50, (new_img_array.shape[-1]))
assert np.abs(np.median(region.astype(int) - target.astype(int))) == 0
assert np.abs(np.mean(region.astype(int) - target.astype(int))) < 0.2


def test_visualise_multi_channel(sample_qptiff: Path) -> None:
"""Test visualising a multi-channel qptiff multiplex image."""
wsi = wsireader.TIFFWSIReader(sample_qptiff, post_proc="auto")
wsi2 = wsireader.TIFFWSIReader(sample_qptiff, post_proc=None)

region = wsi.read_rect(location=(0, 0), size=(50, 100))
region2 = wsi2.read_rect(location=(0, 0), size=(50, 100))

assert region.shape == (100, 50, 3)
assert region2.shape == (100, 50, 7)
1 change: 1 addition & 0 deletions tiatoolbox/cli/visualize.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def run_app() -> None:
title="Tiatoolbox TileServer",
layers={},
)
app.json.sort_keys = False
CORS(app, send_wildcard=True)
app.run(host="127.0.0.1", threaded=True)

Expand Down
4 changes: 4 additions & 0 deletions tiatoolbox/data/remote_samples.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,7 @@ files:
url: [ *testdata, "annotation/test2_config.json"]
nuclick-output:
url: [*modelroot, "predictions/nuclei_mask/nuclick-output.npy"]
qptiff_sample:
url: [*wsis, "multiplexed_example.qptiff"]
qptiff_sample_small:
url: [ *wsis, "multiplexed_example_small.qptiff" ]
6 changes: 5 additions & 1 deletion tiatoolbox/models/dataset/classification.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,11 @@ def __init__( # skipcq: PY-R1000 # noqa: PLR0915
elif auto_get_mask and mode == "wsi" and mask_path is None:
# if no mask provided and `wsi` mode, generate basic tissue
# mask on the fly
mask_reader = self.reader.tissue_mask(resolution=1.25, units="power")
try:
mask_reader = self.reader.tissue_mask(resolution=1.25, units="power")
except:
# if power is None, try with mpp
mask_reader = self.reader.tissue_mask(resolution=6.0, units="mpp")
# ? will this mess up ?
mask_reader.info = self.reader.info

Expand Down
121 changes: 121 additions & 0 deletions tiatoolbox/utils/postproc_defs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""Module to provide postprocessing classes."""

from __future__ import annotations

import colorsys
import warnings

import numpy as np


class MultichannelToRGB:
"""Class to convert multi-channel images to RGB images."""

def __init__(
self: MultichannelToRGB,
color_dict: list[tuple[float, float, float]] | None = None,
) -> None:
"""Initialize the MultichannelToRGB converter.

Args:
color_dict: Dict of channel names with RGB colors for each channel. If not
provided, a set of distinct colors will be auto-generated.

"""
self.colors = None
self.color_dict = color_dict
self.is_validated = False
self.channels = None
self.enhance = 1.0

def validate(self: MultichannelToRGB, n: int) -> None:
"""Validate the input color_dict on first read from image.

Checks that n is either equal to the number of colors provided, or is
one less. In the latter case it is assumed that the last channel is background
autofluorescence and is not in the tiff and we will drop it from
the color_dict with a warning.

Args:
n (int): Number of channels

"""
n_colors = len(self.colors)
if n_colors == n:
self.is_validated = True
return

if n_colors - 1 == n:
self.colors = self.colors[:n]
self.channels = [c for c in self.channels if c < n]
self.is_validated = True
warnings.warn(
"""Number of channels in image is one less than number of channels in
dict. Assuming last channel is background autofluorescence and ignoring
it. If this is not the case please provide a manual color_dict.""",
stacklevel=2,
)
return

msg = f"Number of colors: {n_colors} does not match channels in image: {n}."
raise ValueError(msg)

def generate_colors(self: MultichannelToRGB, n_channels: int) -> np.ndarray:
"""Generate a set of visually distinct colors.

Args:
n_channels (int): Number of channels/colors to generate

Returns:
np.ndarray: Array of RGB colors

"""
self.color_dict = {
f"channel_{i}": colorsys.hsv_to_rgb(i / n_channels, 1, 1)
for i in range(n_channels)
}

def __call__(self: MultichannelToRGB, image: np.ndarray) -> np.ndarray:
"""Convert a multi-channel image to an RGB image.

Args:
image (np.ndarray): Input image of shape (H, W, N)

Returns:
np.ndarray: RGB image of shape (H, W, 3)

"""
n = image.shape[2]

if n < 5: # noqa: PLR2004
# assume already rgb(a) so just return image
return image

if self.colors is None:
self.generate_colors(n)

if not self.is_validated:
self.validate(n)

# Convert to RGB image
rgb_image = (
np.einsum(
"hwn,nc->hwc",
image[:, :, self.channels],
self.colors[self.channels, :],
optimize=True,
)
* self.enhance
)

# Clip to ensure in valid range and return
return np.clip(rgb_image, 0, 255).astype(np.uint8)

def __setattr__(self: MultichannelToRGB, name: str, value: np.Any) -> None:
"""Ensure that colors is updated if color_dict is updated."""
if name == "color_dict" and value is not None:
self.colors = np.array(list(value.values()), dtype=np.float32)
if self.channels is None:
self.channels = list(range(len(value)))

super().__setattr__(name, value)
Loading
Loading