Coverage for qubalab/images/qupath_server.py: 95%

64 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2025-01-31 11:24 +0000

1import numpy as np 

2import tempfile 

3import os 

4from enum import Enum 

5from py4j.java_gateway import JavaGateway, JavaObject 

6from urllib.parse import urlparse, unquote 

7from .image_server import ImageServer 

8from .metadata.image_metadata import ImageMetadata 

9from .metadata.image_shape import ImageShape 

10from .metadata.image_channel import ImageChannel 

11from .metadata.pixel_calibration import PixelCalibration, PixelLength 

12from .region_2d import Region2D 

13from .utils import base64_to_image, bytes_to_image 

14from ..qupath import qupath_gateway 

15 

16 

17class QuPathServer(ImageServer): 

18 """ 

19 An ImageServer that communicates with a QuPath ImageServer through a Gateway. 

20 

21 Closing this server won't close the underlying QuPath ImageServer. 

22 """ 

23 

24 def __init__( 

25 self, 

26 gateway: JavaGateway = None, 

27 qupath_server: JavaObject = None, 

28 pixel_access: str = 'base_64', 

29 **kwargs 

30 ): 

31 """ 

32 :param gateway: the gateway between Python and QuPath. If not specified, the default gateway is used 

33 :param qupath_server: a Java object representing the QuPath ImageServer. If not specified, the currently 

34 opened in QuPath ImageServer is used 

35 :param pixel_access: how to send pixel values from QuPath to Python. Can be 'base_64' to convert pixels to 

36 base 64 encoded text, 'bytes' to to convert pixels to bytes, or 'temp_files' to use 

37 temporary files 

38 :param resize_method: the resampling method to use when resizing the image for downsampling. Bicubic by default 

39 :raises ValueError: when pixel_access has an unexpected value 

40 """ 

41 super().__init__(**kwargs) 

42 

43 available_pixel_access = ['base_64', 'bytes', 'temp_files'] 

44 if pixel_access not in available_pixel_access: 

45 raise ValueError(f'The provided pixel access ({pixel_access}) is not one of {str(available_pixel_access)}') 

46 

47 self._gateway = qupath_gateway.get_default_gateway() if gateway is None else gateway 

48 self._qupath_server = qupath_gateway.get_current_image_data(gateway).getServer() if qupath_server is None else qupath_server 

49 self._pixel_access = pixel_access 

50 

51 def close(self): 

52 pass 

53 

54 def _build_metadata(self) -> ImageMetadata: 

55 qupath_metadata = self._qupath_server.getMetadata() 

56 

57 return ImageMetadata( 

58 path=QuPathServer._find_qupath_server_path(self._qupath_server), 

59 name=qupath_metadata.getName(), 

60 shapes=tuple([ 

61 ImageShape( 

62 x=level.getWidth(), 

63 y=level.getHeight(), 

64 c=self._qupath_server.nChannels(), 

65 z=self._qupath_server.nZSlices(), 

66 t=self._qupath_server.nTimepoints() 

67 ) 

68 for level in qupath_metadata.getLevels() 

69 ]), 

70 pixel_calibration=QuPathServer._find_qupath_server_pixel_calibration(self._qupath_server), 

71 is_rgb=self._qupath_server.isRGB(), 

72 dtype=np.dtype(self._qupath_server.getPixelType().toString().lower()), 

73 channels=tuple([ImageChannel(c.getName(), QuPathServer._unpack_color(c.getColor())) for c in qupath_metadata.getChannels()]), 

74 downsamples=tuple([d for d in self._qupath_server.getPreferredDownsamples()]) 

75 ) 

76 

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

78 if level < 0: 

79 level = len(self.metadata.downsamples) + level 

80 downsample = self._qupath_server.getDownsampleForResolution(level) 

81 

82 request = self._gateway.jvm.qupath.lib.regions.RegionRequest.createInstance( 

83 self._qupath_server.getPath(), 

84 downsample, 

85 int(round(region.x * downsample)), 

86 int(round(region.y * downsample)), 

87 int(round(region.width * downsample)), 

88 int(round(region.height * downsample)), 

89 region.z, 

90 region.t 

91 ) 

92 

93 if self._pixel_access == 'temp_files': 

94 temp_path = tempfile.mkstemp(prefix='qubalab-', suffix='.tif')[1] 

95 

96 self._gateway.entry_point.writeImageRegion(self._qupath_server, request, temp_path) 

97 image = bytes_to_image(temp_path, self.metadata.is_rgb, ImageShape(region.width, region.height, c=self.metadata.n_channels)) 

98 ## on Windows, this fails because the file handle is open elsewhere 

99 ## slightly bad manners to pollute tempfiles but should be insignificant 

100 if not os.name == "nt": 

101 os.remove(temp_path) 

102 else: 

103 format = 'png' if self.metadata.is_rgb else "imagej tiff" 

104 

105 if self._pixel_access == 'bytes': 

106 image = bytes_to_image( 

107 self._gateway.entry_point.getImageBytes(self._qupath_server, request, format), 

108 self.metadata.is_rgb, 

109 ImageShape(region.width, region.height, c=self.metadata.n_channels) 

110 ) 

111 else: 

112 image = base64_to_image( 

113 self._gateway.entry_point.getImageBase64(self._qupath_server, request, format), 

114 self.metadata.is_rgb, 

115 ImageShape(region.width, region.height, c=self.metadata.n_channels) 

116 ) 

117 

118 return image 

119 

120 @staticmethod 

121 def _find_qupath_server_path(qupath_server: JavaObject) -> str: 

122 """ 

123 Try to get the file path for a java object representing an ImageServer. 

124 This can be useful to get direct access to an image file, rather than via QuPath. 

125 """ 

126 

127 uris = tuple(str(u) for u in qupath_server.getURIs()) 

128 if len(uris) == 1: 

129 parsed = urlparse(uris[0]) 

130 if parsed.scheme == 'file': 

131 return unquote(parsed.path) 

132 return qupath_server.getPath() 

133 

134 @staticmethod 

135 def _find_qupath_server_pixel_calibration(qupath_server: JavaObject) -> PixelCalibration: 

136 pixel_calibration = qupath_server.getPixelCalibration() 

137 

138 if pixel_calibration.hasPixelSizeMicrons(): 

139 return PixelCalibration( 

140 PixelLength.create_microns(pixel_calibration.getPixelWidthMicrons()), 

141 PixelLength.create_microns(pixel_calibration.getPixelHeightMicrons()), 

142 PixelLength.create_microns(pixel_calibration.getZSpacingMicrons()) if pixel_calibration.hasZSpacingMicrons() else PixelLength() 

143 ) 

144 else: 

145 return PixelCalibration() 

146 

147 @staticmethod 

148 def _unpack_color(rgb: int) -> tuple[float, float, float]: 

149 r = (rgb >> 16) & 255 

150 g = (rgb >> 8) & 255 

151 b = rgb & 255 

152 return r / 255.0, g / 255.0, b / 255.0