Coverage for src/rok4/Raster.py: 98%

126 statements  

« prev     ^ index     » next       coverage.py v7.4.1, created at 2024-01-29 10:29 +0100

1"""Provide functions to read information on raster data 

2 

3The module contains the following class : 

4 

5 - Raster - Structure describing raster data. 

6 - RasterSet - Structure describing a set of raster data. 

7""" 

8 

9import copy 

10import json 

11import re 

12from enum import Enum 

13from typing import Tuple, Dict 

14 

15from osgeo import ogr, gdal 

16 

17from rok4.Storage import exists, get_osgeo_path, put_data_str 

18from rok4.Utils import ColorFormat, compute_bbox, compute_format 

19 

20# Enable GDAL/OGR exceptions 

21ogr.UseExceptions() 

22gdal.UseExceptions() 

23 

24 

25class Raster: 

26 """A structure describing raster data 

27 

28 Attributes : 

29 path (str): path to the file/object (ex: 

30 file:///path/to/image.tif or s3://bucket/path/to/image.tif) 

31 bbox (Tuple[float, float, float, float]): bounding rectange 

32 in the data projection 

33 bands (int): number of color bands (or channels) 

34 format (ColorFormat): numeric variable format for color values. 

35 Bit depth, as bits per channel, can be derived from it. 

36 mask (str): path to the associated mask file or object, if any, 

37 or None (same path as the image, but with a ".msk" extension 

38 and TIFF format. ex: 

39 file:///path/to/image.msk or s3://bucket/path/to/image.msk) 

40 dimensions (Tuple[int, int]): image width and height, in pixels 

41 """ 

42 

43 def __init__(self) -> None: 

44 self.bands = None 

45 self.bbox = (None, None, None, None) 

46 self.dimensions = (None, None) 

47 self.format = None 

48 self.mask = None 

49 self.path = None 

50 

51 @classmethod 

52 def from_file(cls, path: str) -> "Raster": 

53 """Creates a Raster object from an image 

54 

55 Args: 

56 path (str): path to the image file/object 

57 

58 Examples: 

59 

60 Loading informations from a file stored raster TIFF image 

61 

62 from rok4.Raster import Raster 

63 

64 try: 

65 raster = Raster.from_file( 

66 "file:///data/SC1000/0040_6150_L93.tif" 

67 ) 

68 

69 except Exception as e: 

70 print(f"Cannot load information from image : {e}") 

71 

72 Raises: 

73 RuntimeError: raised by OGR/GDAL if anything goes wrong 

74 NotImplementedError: Storage type not handled 

75 

76 Returns: 

77 Raster: a Raster instance 

78 """ 

79 if not exists(path): 

80 raise Exception(f"No file or object found at path '{path}'.") 

81 

82 self = cls() 

83 

84 work_image_path = get_osgeo_path(path) 

85 

86 image_datasource = gdal.Open(work_image_path) 

87 self.path = path 

88 

89 path_pattern = re.compile("(/[^/]+?)[.][a-zA-Z0-9_-]+$") 

90 mask_path = path_pattern.sub("\\1.msk", path) 

91 

92 if exists(mask_path): 

93 work_mask_path = get_osgeo_path(mask_path) 

94 mask_driver = gdal.IdentifyDriver(work_mask_path).ShortName 

95 if "GTiff" != mask_driver: 

96 message = ( 

97 f"Mask file '{mask_path}' is not a TIFF image." 

98 + f" (GDAL driver : '{mask_driver}'" 

99 ) 

100 raise Exception(message) 

101 self.mask = mask_path 

102 else: 

103 self.mask = None 

104 

105 self.bbox = compute_bbox(image_datasource) 

106 self.bands = image_datasource.RasterCount 

107 self.format = compute_format(image_datasource, path) 

108 self.dimensions = (image_datasource.RasterXSize, image_datasource.RasterYSize) 

109 

110 return self 

111 

112 @classmethod 

113 def from_parameters( 

114 cls, 

115 path: str, 

116 bands: int, 

117 bbox: Tuple[float, float, float, float], 

118 dimensions: Tuple[int, int], 

119 format: ColorFormat, 

120 mask: str = None, 

121 ) -> "Raster": 

122 """Creates a Raster object from parameters 

123 

124 Args: 

125 path (str): path to the file/object (ex: 

126 file:///path/to/image.tif or s3://bucket/image.tif) 

127 bands (int): number of color bands (or channels) 

128 bbox (Tuple[float, float, float, float]): bounding rectange 

129 in the data projection 

130 dimensions (Tuple[int, int]): image width and height 

131 expressed in pixels 

132 format (ColorFormat): numeric format for color values. 

133 Bit depth, as bits per channel, can be derived from it. 

134 mask (str, optionnal): path to the associated mask, if any, 

135 or None (same path as the image, but with a 

136 ".msk" extension and TIFF format. ex: 

137 file:///path/to/image.msk or s3://bucket/image.msk) 

138 

139 Examples: 

140 

141 Loading informations from parameters, related to 

142 a TIFF main image coupled to a TIFF mask image 

143 

144 from rok4.Raster import Raster 

145 

146 try: 

147 raster = Raster.from_parameters( 

148 path="file:///data/SC1000/_0040_6150_L93.tif", 

149 mask="file:///data/SC1000/0040_6150_L93.msk", 

150 bands=3, 

151 format=ColorFormat.UINT8, 

152 dimensions=(2000, 2000), 

153 bbox=(40000.000, 5950000.000, 

154 240000.000, 6150000.000) 

155 ) 

156 

157 except Exception as e: 

158 print( 

159 f"Cannot load information from parameters : {e}" 

160 ) 

161 

162 Raises: 

163 KeyError: a mandatory argument is missing 

164 

165 Returns: 

166 Raster: a Raster instance 

167 """ 

168 self = cls() 

169 

170 self.path = path 

171 self.bands = bands 

172 self.bbox = bbox 

173 self.dimensions = dimensions 

174 self.format = format 

175 self.mask = mask 

176 return self 

177 

178 

179class RasterSet: 

180 """A structure describing a set of raster data 

181 

182 Attributes : 

183 raster_list (List[Raster]): List of Raster instances in the set 

184 colors (List[Dict]): List of color properties for each raster 

185 instance. Contains only one element if 

186 the set is homogenous. 

187 Element properties: 

188 bands (int): number of color bands (or channels) 

189 format (ColorFormat): numeric variable format for 

190 color values. Bit depth, as bits per channel, 

191 can be derived from it. 

192 srs (str): Name of the set's spatial reference system 

193 bbox (Tuple[float, float, float, float]): bounding rectange 

194 in the data projection, enclosing the whole set 

195 """ 

196 

197 def __init__(self) -> None: 

198 self.bbox = (None, None, None, None) 

199 self.colors = [] 

200 self.raster_list = [] 

201 self.srs = None 

202 

203 @classmethod 

204 def from_list(cls, path: str, srs: str) -> "RasterSet": 

205 """Instanciate a RasterSet from an images list path and a srs 

206 

207 Args: 

208 path (str): path to the images list file or object 

209 (each line in this list contains the path to 

210 an image file or object in the set) 

211 

212 Examples: 

213 

214 Loading informations from a file stored list 

215 

216 from rok4.Raster import RasterSet 

217 

218 try: 

219 raster_set = RasterSet.from_list( 

220 path="file:///data/SC1000.list", 

221 srs="EPSG:3857" 

222 ) 

223 

224 except Exception as e: 

225 print( 

226 f"Cannot load information from list file : {e}" 

227 ) 

228 

229 Raises: 

230 RuntimeError: raised by OGR/GDAL if anything goes wrong 

231 NotImplementedError: Storage type not handled 

232 

233 Returns: 

234 RasterSet: a RasterSet instance 

235 """ 

236 self = cls() 

237 self.srs = srs 

238 

239 local_list_path = get_osgeo_path(path) 

240 image_list = [] 

241 with open(file=local_list_path, mode="r") as list_file: 

242 for line in list_file: 

243 image_path = line.strip(" \t\n\r") 

244 image_list.append(image_path) 

245 

246 temp_bbox = [None, None, None, None] 

247 for image_path in image_list: 

248 raster = Raster.from_file(image_path) 

249 self.raster_list.append(raster) 

250 if temp_bbox == [None, None, None, None]: 

251 for i in range(0, 4, 1): 

252 temp_bbox[i] = raster.bbox[i] 

253 else: 

254 if temp_bbox[0] > raster.bbox[0]: 

255 temp_bbox[0] = raster.bbox[0] 

256 if temp_bbox[1] > raster.bbox[1]: 

257 temp_bbox[1] = raster.bbox[1] 

258 if temp_bbox[2] < raster.bbox[2]: 

259 temp_bbox[2] = raster.bbox[2] 

260 if temp_bbox[3] < raster.bbox[3]: 

261 temp_bbox[3] = raster.bbox[3] 

262 color_dict = {"bands": raster.bands, "format": raster.format} 

263 if color_dict not in self.colors: 

264 self.colors.append(color_dict) 

265 self.bbox = tuple(temp_bbox) 

266 return self 

267 

268 @classmethod 

269 def from_descriptor(cls, path: str) -> "RasterSet": 

270 """Creates a RasterSet object from a descriptor file or object 

271 

272 Args: 

273 path (str): path to the descriptor file or object 

274 

275 Examples: 

276 

277 Loading informations from a file stored descriptor 

278 

279 from rok4.Raster import RasterSet 

280 

281 try: 

282 raster_set = RasterSet.from_descriptor( 

283 "file:///data/images/descriptor.json" 

284 ) 

285 

286 except Exception as e: 

287 message = ("Cannot load information from " 

288 + f"descriptor file : {e}") 

289 print(message) 

290 

291 Raises: 

292 RuntimeError: raised by OGR/GDAL if anything goes wrong 

293 NotImplementedError: Storage type not handled 

294 

295 Returns: 

296 RasterSet: a RasterSet instance 

297 """ 

298 self = cls() 

299 descriptor_path = get_osgeo_path(path) 

300 with open(file=descriptor_path, mode="r") as file_handle: 

301 raw_content = file_handle.read() 

302 serialization = json.loads(raw_content) 

303 self.srs = serialization["srs"] 

304 self.raster_list = [] 

305 for raster_dict in serialization["raster_list"]: 

306 parameters = copy.deepcopy(raster_dict) 

307 parameters["bbox"] = tuple(raster_dict["bbox"]) 

308 parameters["dimensions"] = tuple(raster_dict["dimensions"]) 

309 parameters["format"] = ColorFormat[raster_dict["format"]] 

310 self.raster_list.append(Raster.from_parameters(**parameters)) 

311 self.bbox = tuple(serialization["bbox"]) 

312 self.colors = [] 

313 for color_dict in serialization["colors"]: 

314 color_item = copy.deepcopy(color_dict) 

315 color_item["format"] = ColorFormat[color_dict["format"]] 

316 self.colors.append(color_item) 

317 return self 

318 

319 @property 

320 def serializable(self) -> Dict: 

321 """Get the dict version of the raster set, descriptor compliant 

322 

323 Returns: 

324 Dict: descriptor structured object description 

325 """ 

326 serialization = {"bbox": list(self.bbox), "srs": self.srs, "colors": [], "raster_list": []} 

327 for color in self.colors: 

328 color_serial = {"bands": color["bands"], "format": color["format"].name} 

329 serialization["colors"].append(color_serial) 

330 for raster in self.raster_list: 

331 raster_dict = { 

332 "path": raster.path, 

333 "dimensions": list(raster.dimensions), 

334 "bbox": list(raster.bbox), 

335 "bands": raster.bands, 

336 "format": raster.format.name, 

337 } 

338 if raster.mask is not None: 

339 raster_dict["mask"] = raster.mask 

340 serialization["raster_list"].append(raster_dict) 

341 return serialization 

342 

343 def write_descriptor(self, path: str = None) -> None: 

344 """Print raster set's descriptor as JSON 

345 

346 Args: 

347 path (str, optional): Complete path (file or object) 

348 where to print the raster set's JSON. Defaults to None, 

349 JSON is printed to standard output. 

350 """ 

351 content = json.dumps(self.serializable, sort_keys=True) 

352 if path is None: 

353 print(content) 

354 else: 

355 put_data_str(content, path)