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

Cucim reader #72

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 27 additions & 0 deletions ngff_zarr/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@

from .cli_input_to_ngff_image import cli_input_to_ngff_image
from .config import config
from .cucim_image_to_multiscales import cucim_image_to_multiscales
from .cucim_image_to_ngff_image import cucim_image_to_ngff_image
from .detect_cli_io_backend import (
ConversionBackend,
conversion_backends_values,
Expand Down Expand Up @@ -336,6 +338,31 @@ def shutdown_client(sig_id, frame): # noqa: ARG001
_multiscales_to_ngff_zarr(
live, args, output_store, rich_dask_progress, multiscales
)
elif input_backend is ConversionBackend.CUCIM:
try:
import cucim

cuimage = cucim.CuImage(str(args.input[0]))
if args.chunks is None:
# Present the existing chunks and resolution levels
multiscales = cucim_image_to_multiscales(cuimage)
else:
ngff_image = cucim_image_to_ngff_image(cuimage)
multiscales = _ngff_image_to_multiscales(
live,
ngff_image,
args,
progress,
rich_dask_progress,
subtitle,
method,
)
_multiscales_to_ngff_zarr(
live, args, output_store, rich_dask_progress, multiscales
)
except ImportError:
sys.stdout.write("[red]Please install the [i]cucim[/i] package.\n")
sys.exit(1)
elif input_backend is ConversionBackend.TIFFFILE:
try:
import tifffile
Expand Down
10 changes: 10 additions & 0 deletions ngff_zarr/cli_input_to_ngff_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from dask.array.image import imread as daimread
from rich import print

from .cucim_image_to_ngff_image import cucim_image_to_ngff_image
from .detect_cli_io_backend import ConversionBackend
from .from_ngff_zarr import from_ngff_zarr
from .itk_image_to_ngff_image import itk_image_to_ngff_image
Expand Down Expand Up @@ -48,6 +49,15 @@ def imread(filename):
return itk_image_to_ngff_image(image)
image = itk.imread(input)
return itk_image_to_ngff_image(image)
if backend is ConversionBackend.CUCIM:
try:
import cucim

cuimage = cucim.CuImage(str(input[0]))
return cucim_image_to_ngff_image(cuimage)
except ImportError:
print("[red]Please install the [i]cucim[/i] package.")
sys.exit(1)
if backend is ConversionBackend.TIFFFILE:
try:
import tifffile
Expand Down
99 changes: 99 additions & 0 deletions ngff_zarr/cucim_image_to_multiscales.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import dask
import dask.array as da
import numpy as np

from .methods._support import _spatial_dims
from .multiscales import Multiscales
from .to_ngff_image import to_ngff_image
from .zarr_metadata import Axis, Dataset, Metadata, Scale, Translation


def _read_region_data(cuimage, location, size, level):
return np.array(cuimage.read_region(location, size, level))


def cucim_image_to_multiscales(cuimage) -> Multiscales:
dims = tuple(d for d in cuimage.dims.lower())
spatial_dims = set(dims).intersection(_spatial_dims)
spatial_dims = [d for d in dims if d in spatial_dims]
spatial_dims_str = "".join(spatial_dims).upper()

images = []

axes = []
for dim in dims:
unit = None
if dim in {"x", "y", "z"}:
axis = Axis(name=dim, type="space", unit=unit)
elif dim == "c":
axis = Axis(name=dim, type="channel", unit=unit)
elif dim == "t":
axis = Axis(name=dim, type="time", unit=unit)
else:
msg = f"Dimension identifier is not valid: {dim}"
raise KeyError(msg)
axes.append(axis)

for level in range(cuimage.resolutions["level_count"]):
scale_dimensions = cuimage.resolutions["level_dimensions"][level]
scale_downsamples = cuimage.resolutions["level_downsamples"][level]
scale_tile_size = cuimage.resolutions["level_tile_sizes"][level]
# hard coded for 2d
blocks = []
for ii in range(scale_dimensions[0] // scale_tile_size[0]):
block_row = []
for jj in range(scale_dimensions[1] // scale_tile_size[1]):
location = (ii * scale_tile_size[0], jj * scale_tile_size[1])
size = scale_tile_size
block_row.append(
dask.delayed(_read_region_data)(cuimage, location, size, level)
)
blocks.append(block_row)
data = da.block(blocks)

spacing = cuimage.spacing(spatial_dims_str)
scale = {d: 1.0 for d in spatial_dims}
for idx, dim in enumerate(spatial_dims):
scale[dim] = spacing[idx] * scale_downsamples

translation = {d: 0.0 for d in spatial_dims}
for idx, dim in enumerate(spatial_dims):
# cucim: Should origin have a dim_order argument like spacing?
translation[dim] = (
cuimage.origin[idx] + spacing[idx] * scale_downsamples / 2
)

image = to_ngff_image(data, dims=dims, translation=translation, scale=scale)
images.append(image)

datasets = []
for index, image in enumerate(images):
path = f"scale{index}/{image.name}"
scale = []
for dim in image.dims:
if dim in image.scale:
scale.append(image.scale[dim])
else:
scale.append(1.0)
translation = []
for dim in image.dims:
if dim in image.translation:
translation.append(image.translation[dim])
else:
translation.append(1.0)
coordinateTransformations = [Scale(scale), Translation(translation)]
dataset = Dataset(
path=path, coordinateTransformations=coordinateTransformations
)
datasets.append(dataset)
metadata = Metadata(
axes=axes,
datasets=datasets,
name=image.name,
coordinateTransformations=None,
)
return Multiscales(
images=images,
metadata=metadata,
scale_factors=cuimage.resolutions["level_downsamples"],
)
22 changes: 22 additions & 0 deletions ngff_zarr/cucim_image_to_ngff_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import numpy as np

from .methods._support import _spatial_dims
from .ngff_image import NgffImage
from .to_ngff_image import to_ngff_image


def cucim_image_to_ngff_image(cuimage) -> NgffImage:
data = np.array(cuimage)
dims = tuple(d for d in cuimage.dims.lower())
spatial_dims = set(dims).intersection(_spatial_dims)
spatial_dims = [d for d in dims if d in spatial_dims]
spatial_dims_str = "".join(spatial_dims).upper()
translation = {d: 0.0 for d in spatial_dims}
for idx, dim in enumerate(spatial_dims):
# cucim: Should origin have a dim_order argument like spacing?
translation[dim] = cuimage.origin[idx]
spacing = cuimage.spacing(spatial_dims_str)
scale = {d: 1.0 for d in spatial_dims}
for idx, dim in enumerate(spatial_dims):
scale[dim] = spacing[idx]
return to_ngff_image(data, dims=dims, translation=translation, scale=scale)
10 changes: 10 additions & 0 deletions ngff_zarr/detect_cli_io_backend.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import importlib.util
import sys
from enum import Enum
from pathlib import Path
Expand All @@ -8,6 +9,7 @@
("ZARR_ARRAY", "zarr"),
("ITKWASM", "itkwasm_image_io"),
("ITK", "itk"),
("CUCIM", "cucim"),
("TIFFFILE", "tifffile"),
("IMAGEIO", "imageio"),
]
Expand Down Expand Up @@ -103,6 +105,14 @@ def detect_cli_io_backend(input: List[str]) -> ConversionBackend:
if extension in itk_supported_extensions:
return ConversionBackend.ITK

extension = Path(input[0]).suffixes[-1].lower()

if importlib.util.find_spec("cucim") is not None:
cucim_supported_extensions = (".svs", ".tif", ".tiff")

if extension in cucim_supported_extensions:
return ConversionBackend.CUCIM

try:
import tifffile

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ classifiers = [
]
dependencies = [
"dask[array]",
"itkwasm >= 1.0b168",
"itkwasm >= 1.0b169",
"itkwasm-downsample >= 1.1.0",
"numpy",
"platformdirs",
Expand Down
16 changes: 16 additions & 0 deletions test/test_cli_input_to_ngff_image.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import pytest
from ngff_zarr import ConversionBackend, cli_input_to_ngff_image

from ._data import test_data_dir
Expand Down Expand Up @@ -37,6 +38,21 @@ def test_cli_input_to_ngff_image_tifffile(input_images): # noqa: ARG001
assert image.dims == ("z", "y", "x")


def test_cli_input_to_ngff_image_cucim(input_images): # noqa: ARG001
pytest.importorskip("cucim")
input = [
test_data_dir
/ "input"
/ "TCGA-C8-A26W-01A-01-TSA.5870b3d4-a81e-423e-bb93-5897ebc922a3.svs"
]
image = cli_input_to_ngff_image(ConversionBackend.CUCIM, input)
assert image.dims == ("y", "x", "c")
assert image.translation["x"] == 0.0
assert image.translation["y"] == 0.0
assert image.scale["x"] == 1.0
assert image.scale["y"] == 1.0


def test_cli_input_to_ngff_image_imageio(input_images): # noqa: ARG001
input = [
test_data_dir / "input" / "cthead1.png",
Expand Down
7 changes: 6 additions & 1 deletion test/test_detect_cli_input_backend.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import importlib.util

from ngff_zarr import ConversionBackend, detect_cli_io_backend


Expand All @@ -18,7 +20,10 @@ def test_detect_tifffile_input_backend():
f"file{extension}",
]
)
assert backend == ConversionBackend.TIFFFILE
if importlib.util.find_spec("cucim") is not None:
assert backend == ConversionBackend.CUCIM
else:
assert backend == ConversionBackend.TIFFFILE


def test_detect_imageio_input_backend():
Expand Down
Loading