Skip to content

Commit

Permalink
Isyntax (#111)
Browse files Browse the repository at this point in the history
* Support for isyntax file using pyisyntax

* Read barcode from isyntax

* Update changelog

* Update readme

* Skip isyntax test on mac

* Do not include icc profile from isyntax as it is not useable
  • Loading branch information
erikogabrielsson authored Jan 29, 2025
1 parent 2d59212 commit 74da149
Show file tree
Hide file tree
Showing 26 changed files with 985 additions and 321 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pytest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ jobs:
python -m pip install --upgrade pip
python -m pip install poetry
- name: Install Application
run: python -m poetry install
run: python -m poetry install --all-extras

- name: Lint with flake8
run: |
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Support for reading isyntax files using pyisyntax.

### Fixed

- Reading correct image size in pyramid series in Bioformats reader.
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
- Aperio svs (lossless)
- Hamamatsu ndpi (lossless)
- Philips tiff (lossless)
- Zeiss czi (lossy, only base level)
- Zeiss czi (lossy)
- Optional: Formats supported by Bioformats (lossy)

With the `openslide` extra the following formats are also supported:
Expand All @@ -28,6 +28,10 @@ With the `openslide` extra the following formats are also supported:

The `bioformats` extra by default enables lossy support for the [BSD-licensed Bioformat formats](https://docs.openmicroscopy.org/bio-formats/6.12.0/supported-formats.html).

The `isyntax` extra enables lossy single-thread support for isynax files.

For czi and isyntax only the base level is read from file. To produce a conversion with full levels, use `add_missing_levels` in the `save()` method.

## Installation

***Install wsidicomizer from pypi***
Expand Down
258 changes: 185 additions & 73 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ tiffslide = "^2.4.0"
openslide-python = { version = "^1.3.0", optional = true }
scyjava = { version = "^1.8.1", optional = true }
ome-types = {version = "^0.5.0", optional = true }
pyisyntax = {version = "^0.1.2", optional = true }
imagecodecs = { version = "^2024.12.30", optional = true }

[tool.poetry.extras]
openslide = ["openslide-python"]
bioformats = ["scyjava", "ome-types"]
isyntax = ["pyisyntax"]

[tool.poetry.group.dev.dependencies]
pytest = "^7.2.0"
Expand Down
41 changes: 41 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"svs": {
"CMU-1/CMU-1.svs": {
"convert": True,
"openslide": True,
"include_levels": [0, 1, 2],
"lowest_included_pyramid_level": 0,
"photometric_interpretation": "RGB",
Expand Down Expand Up @@ -73,6 +74,7 @@
},
"svs1/input.svs": {
"convert": True,
"openslide": True,
"include_levels": [0, 1, 2],
"lowest_included_pyramid_level": 0,
"photometric_interpretation": "RGB",
Expand Down Expand Up @@ -117,6 +119,7 @@
"czi": {
"czi1/input.czi": {
"convert": False,
"openslide": False,
"include_levels": [0],
"lowest_included_pyramid_level": 0,
"tile_size": 512,
Expand Down Expand Up @@ -144,6 +147,7 @@
"mirax": {
"CMU-1/CMU-1.mrxs": {
"convert": True,
"openslide": True,
"include_levels": [4, 6],
"lowest_included_pyramid_level": 4,
"tile_size": 1024,
Expand Down Expand Up @@ -197,6 +201,7 @@
"ndpi": {
"CMU-1/CMU-1.ndpi": {
"convert": True,
"openslide": True,
"include_levels": [2, 3],
"lowest_included_pyramid_level": 4,
"tile_size": 1024,
Expand Down Expand Up @@ -244,6 +249,7 @@
},
"ndpi1/input.ndpi": {
"convert": True,
"openslide": True,
"include_levels": [2, 3],
"lowest_included_pyramid_level": 4,
"tile_size": 1024,
Expand Down Expand Up @@ -358,6 +364,7 @@
"philips_tiff": {
"philips1/input.tif": {
"convert": True,
"openslide": True,
"include_levels": [4, 5, 6],
"lowest_included_pyramid_level": 4,
"photometric_interpretation": "YBR_FULL_422",
Expand Down Expand Up @@ -408,6 +415,40 @@
],
}
},
"isyntax": {
"isyntax1/testslide.isyntax": {
"convert": False,
"openslide": False,
"include_levels": [0],
"lowest_included_pyramid_level": 0,
"photometric_interpretation": "YBR_FULL_422",
"image_coordinate_system": {"x": 0.0, "y": 0.0},
"icc_profile": False,
"read_region": [
{
"location": {"x": 18000, "y": 39000},
"level": 0,
"size": {"width": 200, "height": 200},
"md5": "47d0eea527c6203817350a5b38c34a85",
},
{
"location": {"x": 20000, "y": 40000},
"level": 0,
"size": {"width": 200, "height": 200},
"md5": "3690f9decbd78ef83ed0b5c949050a15",
},
{
"location": {"x": 20500, "y": 40300},
"level": 0,
"size": {"width": 200, "height": 200},
"md5": "abb552d51b81feaf0768067708b1c169",
},
],
"read_thumbnail": [],
"read_region_openslide": [],
"skip_hash_test_platforms": ["Darwin"],
}
},
}


Expand Down
4 changes: 4 additions & 0 deletions tests/download_test_images.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@
},
"zip": True,
},
"isyntax/isyntax1/testslide.isyntax": {
"url": "https://zenodo.org/record/5037046/files/testslide.isyntax", # NOQA
"md5": {"testslide.isyntax": "d762ed9e13d4c47549672a54777f40e3"},
},
}

DEFAULT_SLIDE_FOLDER = "tests/testdata/slides"
Expand Down
48 changes: 42 additions & 6 deletions tests/test_convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# limitations under the License.

import os
import platform
from hashlib import md5
from pathlib import Path
from tempfile import TemporaryDirectory
Expand Down Expand Up @@ -136,13 +137,20 @@ def test_focal_plane_not_found(self, wsi: WsiDicom):
wsi.read_tile(0, (0, 0), z=1.0)

@pytest.mark.parametrize(
["file_format", "file", "region", "lowest_included_level"],
[
"file_format",
"file",
"region",
"lowest_included_level",
"skip_hash_test_platforms",
],
[
(
file_format,
file,
region,
file_parameters["lowest_included_pyramid_level"],
file_parameters.get("skip_hash_test_platforms", []),
)
for file_format, format_files in test_parameters.items()
for file, file_parameters in format_files.items()
Expand All @@ -156,9 +164,12 @@ def test_read_region_from_converted_file_should_match_hash(
file: str,
region: Dict[str, Any],
lowest_included_level: int,
skip_hash_test_platforms: List[str],
wsi: WsiDicom,
):
# Arrange
if platform.system() in skip_hash_test_platforms:
pytest.skip(f"Skipping hash test for {platform.system()}.")
level = region["level"] - lowest_included_level

# Act
Expand All @@ -174,19 +185,31 @@ def test_read_region_from_converted_file_should_match_hash(
), f"{file_format}: {file} lowest level {lowest_included_level} {region}"

@pytest.mark.parametrize(
["file_format", "file", "thumbnail"],
["file_format", "file", "thumbnail", "skip_hash_test_platforms"],
[
(file_format, file, thumbnail)
(
file_format,
file,
thumbnail,
file_parameters.get("skip_hash_test_platforms", []),
)
for file_format, format_files in test_parameters.items()
for file, file_parameters in format_files.items()
for thumbnail in file_parameters["read_thumbnail"]
],
scope="module",
)
def test_read_thumbnail_from_converted_file_should_match_hash(
self, file_format: str, file: str, thumbnail: Dict[str, Any], wsi: WsiDicom
self,
file_format: str,
file: str,
thumbnail: Dict[str, Any],
wsi: WsiDicom,
skip_hash_test_platforms: List[str],
):
# Arrange
if platform.system() in skip_hash_test_platforms:
pytest.skip(f"Skipping hash test for {platform.system()}.")

# Act
im = wsi.read_thumbnail(
Expand All @@ -199,17 +222,25 @@ def test_read_thumbnail_from_converted_file_should_match_hash(
), f"{file_format}: {file} {thumbnail}"

@pytest.mark.parametrize(
["file_format", "file", "region", "lowest_included_level"],
[
"file_format",
"file",
"region",
"lowest_included_level",
"skip_hash_test_platforms",
],
[
(
file_format,
file,
region,
file_parameters["lowest_included_pyramid_level"],
file_parameters.get("skip_hash_test_platforms", []),
)
for file_format, format_files in test_parameters.items()
for file, file_parameters in format_files.items()
for region in file_parameters["read_region_openslide"]
if file_parameters["openslide"]
],
scope="module",
)
Expand All @@ -219,10 +250,13 @@ def test_read_region_from_converted_file_should_match_openslide(
file: str,
region: Dict[str, Any],
lowest_included_level: int,
skip_hash_test_platforms: List[str],
wsi_file: Path,
wsi: WsiDicom,
):
# Arrange
if platform.system() in skip_hash_test_platforms:
pytest.skip(f"Skipping hash test for {platform.system()}.")
level = region["level"] - lowest_included_level
with OpenSlide(wsi_file) as openslide_wsi:
scale: float = openslide_wsi.level_downsamples[region["level"]]
Expand Down Expand Up @@ -269,10 +303,11 @@ def test_read_region_from_converted_file_should_match_openslide(
for file_format, format_files in test_parameters.items()
for file, file_parameters in format_files.items()
for thumbnail in file_parameters["read_thumbnail"]
if file_parameters["openslide"]
],
scope="module",
)
def test_read_thumbnail_from_converted_file_should_almost_match_thumbnail(
def test_read_thumbnail_from_converted_file_should_almost_match_openslide_thumbnail(
self,
file_format: str,
file: str,
Expand Down Expand Up @@ -324,6 +359,7 @@ def test_photometric_interpretation(
(file_format, file)
for file_format, format_files in test_parameters.items()
for file in format_files.keys()
if format_files[file]["convert"]
],
scope="module",
)
Expand Down
10 changes: 10 additions & 0 deletions wsidicomizer/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,19 @@ class Settings:
global variable settings."""

def __init__(self) -> None:
self._default_tile_size = 512
self._czi_block_cache_size = 8
self._insert_icc_profile_if_missing = True

@property
def default_tile_size(self) -> int:
"""Default tile size to use."""
return self._default_tile_size

@default_tile_size.setter
def default_tile_size(self, value: int) -> None:
self._default_tile_size = value

@property
def czi_block_cache_size(self) -> int:
"""Size of block cache to use for czi files."""
Expand Down
Loading

0 comments on commit 74da149

Please sign in to comment.