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
« 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
17class QuPathServer(ImageServer):
18 """
19 An ImageServer that communicates with a QuPath ImageServer through a Gateway.
21 Closing this server won't close the underlying QuPath ImageServer.
22 """
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)
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)}')
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
51 def close(self):
52 pass
54 def _build_metadata(self) -> ImageMetadata:
55 qupath_metadata = self._qupath_server.getMetadata()
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 )
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)
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 )
93 if self._pixel_access == 'temp_files':
94 temp_path = tempfile.mkstemp(prefix='qubalab-', suffix='.tif')[1]
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"
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 )
118 return image
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 """
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()
134 @staticmethod
135 def _find_qupath_server_pixel_calibration(qupath_server: JavaObject) -> PixelCalibration:
136 pixel_calibration = qupath_server.getPixelCalibration()
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()
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