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

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 

10 

11 

12class IccProfileServer(WrappedImageServer): 

13 """ 

14 Wrap an ImageServer and apply an ICC Profile to the pixels, if possible. 

15 

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. 

19 

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. 

22  

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 """ 

26 

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) 

40 

41 if icc_profile is None: 

42 icc_profile = IccProfileServer._get_icc_bytes_from_path(base_server.metadata.path) 

43 

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") 

50 

51 if self._icc is None: 

52 warnings.warn(f'No ICC Profile found for {base_server.metadata.path}. Returning original pixels.') 

53 

54 @property 

55 def icc_transform(self) -> ImageCms.ImageCmsTransform: 

56 """ 

57 Get the transform used to apply the ICC profile. 

58 

59 If this is None, then the server simply returns the original pixels unchanged. 

60 """ 

61 return self._icc 

62 

63 def _read_block(self, level: int, region: Region2D) -> np.ndarray: 

64 image = self.base_server._read_block(level, region) 

65 

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 

70 

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