Skip to content

Commit

Permalink
Merge pull request #83 from erikogabrielsson/icc-profile
Browse files Browse the repository at this point in the history
Add color_profile property and profile to Pil images if available
  • Loading branch information
ap-- authored Feb 13, 2024
2 parents 7dc3fce + 9e3c7f9 commit da29a48
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 5 deletions.
34 changes: 33 additions & 1 deletion tiffslide/tests/test_compatibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,14 @@ def ts_slide(file_name):

@pytest.fixture()
def os_slide(file_name):
from openslide import OpenSlide
import os
if hasattr(os, 'add_dll_directory'):
openslide_path = os.getenv("OPENSLIDE_PATH")
if openslide_path is not None:
with os.add_dll_directory(openslide_path):
from openslide import OpenSlide
else:
from openslide import OpenSlide

yield OpenSlide(file_name)

Expand Down Expand Up @@ -233,3 +240,28 @@ def test_read_region_equality_level_common_max(ts_slide, os_slide, file_name):

max_difference = np.max(np.abs(ts_arr.astype(int) - os_arr.astype(int)))
assert max_difference <= (0 if exact else 1)

def test_color_profile_property(ts_slide, os_slide):
if os_slide.color_profile is None:
assert ts_slide.color_profile is None
else:
assert ts_slide.color_profile.tobytes() == os_slide.color_profile.tobytes()

def test_icc_profile_in_thumbnail(ts_slide, os_slide):
ts_slide_thumbnail = ts_slide.get_thumbnail((200, 200))
os_slide_thumbnail = os_slide.get_thumbnail((200, 200))

if os_slide_thumbnail.info.get("icc_profile") is None:
assert ts_slide_thumbnail.info.get("icc_profile") is None
else:
assert os_slide_thumbnail.info["icc_profile"] == ts_slide_thumbnail.info["icc_profile"]


def test_icc_profile_in_region(ts_slide, os_slide):
ts_slide_region = ts_slide.read_region((0, 0), 0, (200, 200))
os_slide_region = os_slide.read_region((0, 0), 0, (200, 200))

if os_slide_region.info.get("icc_profile") is None:
assert ts_slide_region.info.get("icc_profile") is None
else:
assert os_slide_region.info["icc_profile"] == ts_slide_region.info["icc_profile"]
42 changes: 38 additions & 4 deletions tiffslide/tiffslide.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from __future__ import annotations
import io

import math
import os.path
Expand All @@ -13,7 +14,6 @@
from typing import Iterator
from typing import Literal
from typing import Mapping
from typing import Optional
from typing import TypeVar
from typing import overload
from warnings import warn
Expand All @@ -25,7 +25,7 @@
from fsspec.core import url_to_fs
from fsspec.implementations.local import LocalFileSystem
from fsspec.implementations.reference import ReferenceFileSystem
from PIL import Image
from PIL import Image, ImageCms
from tifffile import TiffFile
from tifffile import TiffFileError as TiffFileError
from tifffile import TiffPage
Expand Down Expand Up @@ -240,6 +240,13 @@ def associated_images(self) -> _LazyAssociatedImagesDict:
series = self.ts_tifffile.series[idx + 1 :]
return _LazyAssociatedImagesDict(series)

@cached_property
def color_profile(self) -> ImageCms.ImageCmsProfile | None:
"""return the color profile of the image if present"""
if self._profile is None:
return None
return ImageCms.getOpenProfile(io.BytesIO(self._profile))

def get_best_level_for_downsample(self, downsample: float) -> int:
"""return the best level for a given downsampling factor"""
if downsample <= 1.0:
Expand Down Expand Up @@ -411,9 +418,12 @@ def read_region(
if as_array:
return arr
elif axes == "YX":
return Image.fromarray(arr[..., 0])
image = Image.fromarray(arr[..., 0])
else:
return Image.fromarray(arr)
image = Image.fromarray(arr)
if self._profile is not None:
image.info['icc_profile'] = self._profile
return image

def _read_region_loc_transform(
self, location: tuple[int, int], level: int
Expand Down Expand Up @@ -484,8 +494,16 @@ def get_thumbnail(
except ValueError:
# see: https://github.com/python-pillow/Pillow/blob/95cff6e959/src/libImaging/Resample.c#L559-L588
thumb.thumbnail(size, _NEAREST)
if self._profile is not None:
thumb.info['icc_profile'] = self._profile
return thumb

@cached_property
def _profile(self) -> bytes | None:
"""return the color profile of the image if present"""
parser = _IccParser(self._tifffile)
return parser.parse()


class NotTiffSlide(TiffSlide):
# noinspection PyMissingConstructor
Expand Down Expand Up @@ -771,6 +789,7 @@ def parse_aperio(self) -> dict[str, Any]:

# collect level info
md.update(self.collect_level_info(series0))

return md

def parse_leica(self) -> dict[str, Any]:
Expand Down Expand Up @@ -1031,6 +1050,21 @@ def _parse_metadata_leica(image_description: str) -> dict[str, Any]:

return md

class _IccParser:
"""parse ICC profile from tiff tags"""

def __init__(self, tf: TiffFile) -> None:
self._tf = tf

def parse(self) -> bytes | None:
"""return the ICC profile if present"""
page = self._tf.pages[0]
if isinstance(page, TiffPage) and "InterColorProfile" in page.tags:
icc_profile = page.tags["InterColorProfile"].value
if isinstance(icc_profile, bytes):
return icc_profile
return None


# --- helper functions --------------------------------------------------------

Expand Down

0 comments on commit da29a48

Please sign in to comment.