Coverage for qubalab/images/icc_profile_server.py: 90%
39 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-31 11:24 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-31 11:24 +0000
1import numpy as np
2import warnings
3import io
4import tifffile
5from typing import Union
6from PIL import Image, ImageCms
7from .wrapped_image_server import WrappedImageServer
8from .image_server import ImageServer
9from .region_2d import Region2D
12class IccProfileServer(WrappedImageServer):
13 """
14 Wrap an ImageServer and apply an ICC Profile to the pixels, if possible.
16 If no ICC Profile is provided, an attempt is made to read the profile from the image using PIL.
17 This isn't guaranteed to succeed.
18 To find out if it was successful, test whether self.icc_transform is not None.
20 See http://www.andrewjanowczyk.com/application-of-icc-profiles-to-digital-pathology-images/
21 for a blog post describing where this may be useful, and providing further code.
23 Closing this server will close the wrapped server.
24 The metadata of this server is equivalent to the metadata of the wrapped server.
25 """
27 def __init__(
28 self,
29 base_server: ImageServer,
30 icc_profile: Union[bytes, ImageCms.ImageCmsProfile, ImageCms.core.CmsProfile, ImageCms.ImageCmsTransform] = None,
31 **kwargs
32 ):
33 """
34 :param base_server: the server to wrap
35 :param icc_profile: the ICC profile to apply to the wrapped image server. If omitted, an attempt is made to read the profile from the image.
36 If not successful, a warning will be logged.
37 :param resize_method: the resampling method to use when resizing the image for downsampling. Bicubic by default
38 """
39 super().__init__(base_server, **kwargs)
41 if icc_profile is None:
42 icc_profile = IccProfileServer._get_icc_bytes_from_path(base_server.metadata.path)
44 if icc_profile is None:
45 self._icc = None
46 elif isinstance(icc_profile, ImageCms.ImageCmsTransform):
47 self._icc = icc_profile
48 else:
49 self._icc = ImageCms.buildTransformFromOpenProfiles(icc_profile, ImageCms.createProfile("sRGB"), "RGB", "RGB")
51 if self._icc is None:
52 warnings.warn(f'No ICC Profile found for {base_server.metadata.path}. Returning original pixels.')
54 @property
55 def icc_transform(self) -> ImageCms.ImageCmsTransform:
56 """
57 Get the transform used to apply the ICC profile.
59 If this is None, then the server simply returns the original pixels unchanged.
60 """
61 return self._icc
63 def _read_block(self, level: int, region: Region2D) -> np.ndarray:
64 image = self.base_server._read_block(level, region)
66 if self._icc:
67 return np.transpose(np.array(ImageCms.applyTransform(Image.fromarray(np.transpose(image, axes=[2, 1, 0])), self._icc)), axes=[2, 1, 0])
68 else:
69 return image
71 @staticmethod
72 def _get_icc_bytes_from_path(path) -> bytes:
73 try:
74 with tifffile.TiffFile(path) as tif:
75 if 34675 in tif.pages[0].tags:
76 return io.BytesIO(tif.pages[0].tags[34675].value)
77 else:
78 return None
79 except Exception as error:
80 warnings.warn(f"Error while retrieving icc profile from {path}: {error}")
81 return None