Coverage for qubalab/qupath/qupath_gateway.py: 23%

90 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-10-07 15:29 +0000

1import numpy as np 

2from py4j.java_gateway import JavaGateway, GatewayParameters, JavaObject 

3from py4j.protocol import Py4JNetworkError 

4from typing import Union, Optional 

5import geojson 

6from ..images import utils 

7from ..objects.image_feature import ImageFeature 

8from ..objects.object_type import ObjectType 

9from ..objects.geojson import geojson_features_from_string 

10 

11 

12_default_gateway = None 

13 

14 

15def create_gateway( 

16 auto_convert=True, auth_token=None, port=25333, set_as_default=True, **kwargs 

17) -> JavaGateway: 

18 """ 

19 Create a new JavaGateway to communicate with QuPath. 

20 

21 This requires launching QuPath and activating Py4J from there first. 

22 

23 :param auto_convert: if True, the gateway will try to automatically convert Python objects like sequences and maps to Java Objects 

24 :param auth_token: the authentication token to use to connect. Can be None if no authentication is used 

25 :param port: the port to use to connect 

26 :param set_as_default: whether to set the created gateway as the default gateway 

27 :param kwargs: additional arguments to give to the JavaGateway constructor 

28 :returns: the created gateway 

29 :raises RuntimeError: if the connection to QuPath couldn't be established 

30 """ 

31 if auth_token is None: 

32 gateway_parameters = GatewayParameters(port=port) 

33 else: 

34 gateway_parameters = GatewayParameters(auth_token=auth_token, port=port) 

35 gateway = JavaGateway( 

36 auto_convert=auto_convert, gateway_parameters=gateway_parameters, **kwargs 

37 ) 

38 

39 try: 

40 # This will fail if QuPath is not running with a Py4J gateway 

41 qupath = gateway.entry_point.getQuPath() 

42 assert qupath is not None 

43 except (Py4JNetworkError, AssertionError) as err: 

44 raise RuntimeError( 

45 "Could not connect to QuPath - is it running, and you have opened a Py4J gateway?" 

46 ) from err 

47 

48 if set_as_default: 

49 set_default_gateway(gateway) 

50 return gateway 

51 

52 

53def set_default_gateway(gateway: JavaGateway): 

54 """ 

55 Set the default JavaGateway to use if one is not otherwise specified. 

56 

57 This default gateway will be used if no gateway is provided in some functions of this file. 

58 

59 :param gateway: the gateway that should be the default gateway 

60 """ 

61 global _default_gateway 

62 _default_gateway = gateway 

63 

64 

65def get_default_gateway() -> JavaGateway: 

66 """ 

67 Return the default gateway. It will be created if it doesn't already exist. 

68 

69 :returns: the default gateway 

70 """ 

71 global _default_gateway 

72 if _default_gateway is None: 

73 _default_gateway = create_gateway() 

74 return _default_gateway 

75 

76 

77def get_current_image_data(gateway: Optional[JavaGateway] = None) -> JavaObject: 

78 """ 

79 Get the current ImageData opened in QuPath through the provided gateway. 

80 

81 :param gateway: the gateway to use. Can be None to use the default gateway 

82 :returns: the current ImageData opened in QuPath 

83 """ 

84 gateway = get_default_gateway() if gateway is None else gateway 

85 return gateway.entry_point.getQuPath().getImageData() 

86 

87 

88def get_project(gateway: Optional[JavaGateway] = None) -> JavaObject: 

89 """ 

90 Return the currently opened QuPath project. 

91 

92 :param gateway: the gateway to use. Can be None to use the default gateway 

93 :return: a Java Object representing the currently opened QuPath project 

94 """ 

95 gateway = get_default_gateway() if gateway is None else gateway 

96 return gateway.entry_point.getProject() 

97 

98 

99def create_snapshot( 

100 gateway: JavaGateway = None, snapshot_type: str = "qupath" 

101) -> np.ndarray: 

102 """ 

103 Create and return a snapshot of QuPath. 

104 

105 :param gateway: the gateway to use. Can be None to use the default gateway 

106 :param snapshot_type: what to include in the snapshot. 'qupath' for the entire qupath window or 

107 'viewer' for only the viewer 

108 :returns: a numpy array with dimensions (y, x, c) representing an RGB image 

109 :raises ValueError: if the snapshot type was not recognized 

110 """ 

111 gateway = get_default_gateway() if gateway is None else gateway 

112 qp = gateway.entry_point 

113 

114 if snapshot_type == "qupath": 

115 image = qp.snapshotBase64(qp.getQuPath()) 

116 elif snapshot_type == "viewer": 

117 image = qp.snapshotBase64(qp.getCurrentViewer()) 

118 else: 

119 raise ValueError(f"Unknown snapshot_type {snapshot_type}") 

120 

121 return utils.base64_to_image(image, True) 

122 

123 

124def get_objects( 

125 image_data: Optional[JavaObject] = None, 

126 gateway: Optional[JavaGateway] = None, 

127 object_type: Optional[ObjectType] = None, 

128 converter: Optional[str] = None, 

129) -> Union[list[geojson.Feature], list[ImageFeature], list[JavaObject]]: 

130 """ 

131 Get the objects (e.g. detections, annotations) of the current or specified image in QuPath. 

132 

133 :param image_data: the image_data to retrieve objects from. Can be None to use the current ImageData opened in QuPath 

134 :param gateway: the gateway to use. Can be None to use the default gateway 

135 :param object_type: the type of object to get. Can be None to get all objects except the root 

136 :param converter: can be 'geojson' to get an extended GeoJSON feature represented as a QuBaLab 'ImageObject', 

137 or 'simple_feature' to get a regular GeoJSON 'Feature'. By default, the Py4J representation of 

138 the QuPath Java object is returned. 

139 :return: the list of objects of the specified image. The type of the objects depends on the converted parameter 

140 """ 

141 gateway = get_default_gateway() if gateway is None else gateway 

142 image_data = get_current_image_data(gateway) if image_data is None else image_data 

143 hierarchy = image_data.getHierarchy() 

144 

145 match object_type: 

146 case None: 

147 path_objects = hierarchy.getAllObjects(False) 

148 case ObjectType.ANNOTATION: 

149 path_objects = hierarchy.getAnnotationObjects() 

150 case ObjectType.DETECTION: 

151 path_objects = hierarchy.getDetectionObjects() 

152 case ObjectType.TILE: 

153 path_objects = hierarchy.getTileObjects() 

154 case ObjectType.CELL: 

155 path_objects = hierarchy.getCellObjects() 

156 case ObjectType.TMA_CORE: 

157 tma_grid = hierarchy.getTMAGrid() 

158 path_objects = [] if tma_grid is None else tma_grid.getTMACoreList() 

159 case _: 

160 path_objects = hierarchy.getAllObjects(False) 

161 

162 if converter == "geojson" or converter == "simple_feature": 

163 # Use toFeatureCollections for performance and to avoid string length troubles 

164 features = [] 

165 for feature_collection in gateway.entry_point.toFeatureCollections( 

166 path_objects, 1000 

167 ): 

168 features.extend( 

169 geojson_features_from_string(feature_collection, parse_constant=None) 

170 ) 

171 

172 if converter == "simple_feature": 

173 return features 

174 else: 

175 return [ImageFeature.create_from_feature(f) for f in features] 

176 else: 

177 return path_objects 

178 

179 

180def count_objects( 

181 image_data: Optional[JavaObject] = None, 

182 gateway: Optional[JavaGateway] = None, 

183 object_type: Optional[ObjectType] = None, 

184) -> int: 

185 """ 

186 Get a count of all objects in the current or specified image. 

187 

188 Since requesting the objects can be slow, this can be used to check if a reasonable 

189 number of objects is available. 

190 

191 :param image_data: the image_data to retrieve objects from. Can be None to use the current ImageData opened in QuPath 

192 :param gateway: the gateway to use. Can be None to use the default gateway 

193 :param object_type: the type of object to get. Can be None to get all objects except the root 

194 :return: the number of such objects in the current or specified image 

195 """ 

196 return len(get_objects(image_data, gateway, object_type)) 

197 

198 

199def add_objects( 

200 features: Union[list[geojson.Feature], geojson.Feature], 

201 image_data: Optional[JavaObject] = None, 

202 gateway: Optional[JavaGateway] = None, 

203): 

204 """ 

205 Add the provided features to the current or provided ImageData. 

206 

207 :param features: the features to add. Can be a list or a single Feature 

208 :param image_data: the image_data to add features to. Can be None to use the current ImageData opened in QuPath 

209 :param gateway: the gateway to use. Can be None to use the default gateway 

210 """ 

211 gateway = get_default_gateway() if gateway is None else gateway 

212 image_data = get_current_image_data(gateway) if image_data is None else image_data 

213 

214 if isinstance(features, geojson.Feature): 

215 features = list(features) 

216 

217 features_json = geojson.dumps(features, allow_nan=True) 

218 image_data.getHierarchy().addObjects( 

219 gateway.entry_point.toPathObjects(features_json) 

220 ) 

221 

222 

223def delete_objects( 

224 image_data: Optional[JavaObject] = None, 

225 gateway: Optional[JavaGateway] = None, 

226 object_type: Optional[ObjectType] = None, 

227): 

228 """ 

229 Delete all specified objects (e.g. annotations, detections) from the current or provided ImageData. 

230 

231 :param image_data: the image_data to remove the objects from. Can be None to use the current ImageData opened in QuPath 

232 :param gateway: the gateway to use. Can be None to use the default gateway 

233 :param object_type: the type of object to remove. Can be None to remove all objects 

234 """ 

235 gateway = get_default_gateway() if gateway is None else gateway 

236 image_data = get_current_image_data(gateway) if image_data is None else image_data 

237 

238 if image_data is not None: 

239 image_data.getHierarchy().removeObjects( 

240 get_objects(image_data, gateway, object_type), True 

241 ) 

242 image_data.getHierarchy().getSelectionModel().clearSelection() 

243 

244 

245def refresh_qupath(gateway: Optional[JavaGateway] = None) -> None: 

246 """ 

247 Update the current QuPath interface. 

248 

249 This is sometimes needed to update the QuPath window when some changes are 

250 made to hierarchy. 

251 

252 :param gateway: the gateway to use. Can be None to use the default gateway 

253 """ 

254 gateway = get_default_gateway() if gateway is None else gateway 

255 gateway.entry_point.fireHierarchyUpdate()