Coverage for src/rok4/raster.py: 97%

126 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-10-01 15:35 +0000

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 

9# -- IMPORTS -- 

10 

11import json 

12import re 

13import tempfile 

14 

15# standard library 

16from copy import deepcopy 

17from json.decoder import JSONDecodeError 

18from typing import Dict, Tuple 

19 

20# 3rd party 

21from osgeo import gdal, ogr 

22 

23# package 

24from rok4.enums import ColorFormat 

25from rok4.storage import ( 

26 copy, 

27 exists, 

28 get_data_str, 

29 get_osgeo_path, 

30 put_data_str, 

31 remove, 

32) 

33from rok4.utils import compute_bbox, compute_format 

34 

35# -- GLOBALS -- 

36 

37# Enable GDAL/OGR exceptions 

38ogr.UseExceptions() 

39gdal.UseExceptions() 

40 

41 

42class Raster: 

43 """A structure describing raster data 

44 

45 Attributes: 

46 path (str): path to the file/object (ex: file:///path/to/image.tif or s3://bucket/path/to/image.tif) 

47 bbox (Tuple[float, float, float, float]): bounding rectange in the data projection 

48 bands (int): number of color bands (or channels) format (ColorFormat). Numeric variable format for color values. Bit depth, as bits per channel, 

49 can be derived from it. 

50 mask (str): path to the associated mask file or object, if any, or None (same path as the image, but with a ".msk" extension and TIFF format. 

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

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

53 """ 

54 

55 def __init__(self) -> None: 

56 self.bands = None 

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

58 self.dimensions = (None, None) 

59 self.format = None 

60 self.mask = None 

61 self.path = None 

62 

63 @classmethod 

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

65 """Creates a Raster object from an image 

66 

67 Args: 

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

69 

70 Examples: 

71 

72 Loading informations from a file stored raster TIFF image 

73 

74 from rok4.raster import Raster 

75 

76 try: 

77 raster = Raster.from_file( 

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

79 ) 

80 

81 except Exception as e: 

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

83 

84 Raises: 

85 FormatError: MASK file is not a TIFF 

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

87 NotImplementedError: Storage type not handled 

88 FileNotFoundError: File or object does not exists 

89 

90 Returns: 

91 Raster: a Raster instance 

92 """ 

93 if not exists(path): 

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

95 

96 self = cls() 

97 

98 work_image_path = get_osgeo_path(path) 

99 

100 image_datasource = gdal.Open(work_image_path) 

101 self.path = path 

102 

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

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

105 

106 if exists(mask_path): 

107 work_mask_path = get_osgeo_path(mask_path) 

108 mask_driver = gdal.IdentifyDriver(work_mask_path).ShortName 

109 if "GTiff" != mask_driver: 

110 message = f"Mask file '{mask_path}' use GDAL driver : '{mask_driver}'" 

111 raise FormatError("TIFF", mask_path, message) 

112 self.mask = mask_path 

113 else: 

114 self.mask = None 

115 

116 self.bbox = compute_bbox(image_datasource) 

117 self.bands = image_datasource.RasterCount 

118 self.format = compute_format(image_datasource, path) 

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

120 

121 return self 

122 

123 @classmethod 

124 def from_parameters( 

125 cls, 

126 path: str, 

127 bands: int, 

128 bbox: Tuple[float, float, float, float], 

129 dimensions: Tuple[int, int], 

130 format: ColorFormat, 

131 mask: str = None, 

132 ) -> "Raster": 

133 """Creates a Raster object from parameters 

134 

135 Args: 

136 path (str): path to the file/object (ex: file:///path/to/image.tif or s3://bucket/image.tif) 

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

138 bbox (Tuple[float, float, float, float]): bounding rectange in the data projection 

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

140 format (ColorFormat): numeric format for color values. Bit depth, as bits per channel, can be derived from it. 

141 mask (str, optionnal): path to the associated mask, if any, or None (same path as the image, but with a ".msk" 

142 extension and TIFF format. ex: file:///path/to/image.msk or s3://bucket/image.msk) 

143 

144 Examples: 

145 

146 Loading informations from parameters, related to 

147 a TIFF main image coupled to a TIFF mask image 

148 

149 from rok4.raster import Raster 

150 

151 try: 

152 raster = Raster.from_parameters( 

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

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

155 bands=3, 

156 format=ColorFormat.UINT8, 

157 dimensions=(2000, 2000), 

158 bbox=(40000.000, 5950000.000, 240000.000, 6150000.000) 

159 ) 

160 

161 except Exception as e: 

162 print( 

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

164 ) 

165 

166 Raises: 

167 KeyError: a mandatory argument is missing 

168 

169 Returns: 

170 Raster: a Raster instance 

171 """ 

172 self = cls() 

173 

174 self.path = path 

175 self.bands = bands 

176 self.bbox = bbox 

177 self.dimensions = dimensions 

178 self.format = format 

179 self.mask = mask 

180 return self 

181 

182 

183class RasterSet: 

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

185 

186 Attributes: 

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

188 colors (Set[Tuple[int, ColorFormat]]): Set (distinct values) of color properties (bands and format) found in the raster set. 

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

190 bbox (Tuple[float, float, float, float]): bounding rectange in the data projection, enclosing the whole set 

191 """ 

192 

193 def __init__(self) -> None: 

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

195 self.colors = set() 

196 self.raster_list = [] 

197 self.srs = None 

198 

199 @classmethod 

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

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

202 

203 Args: 

204 path (str): path to the images list file or object (each line in this list contains the path to an image file or object in the set) 

205 srs (str): images' coordinates system 

206 

207 Examples: 

208 

209 Loading informations from a file stored list 

210 

211 from rok4.raster import RasterSet 

212 

213 try: 

214 raster_set = RasterSet.from_list( 

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

216 srs="EPSG:3857" 

217 ) 

218 

219 except Exception as e: 

220 print( 

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

222 ) 

223 

224 Raises: 

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

226 NotImplementedError: Storage type not handled 

227 

228 Returns: 

229 RasterSet: a RasterSet instance 

230 """ 

231 self = cls() 

232 self.srs = srs 

233 

234 # Chargement de la liste des images (la liste peut être un fichier ou un objet) 

235 list_obj = tempfile.NamedTemporaryFile(mode="r", delete=False) 

236 list_file = list_obj.name 

237 copy(path, f"file://{list_file}") 

238 list_obj.close() 

239 image_list = [] 

240 with open(list_file) as listin: 

241 for line in listin: 

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

243 image_list.append(image_path) 

244 

245 remove(f"file://{list_file}") 

246 

247 bbox = [None, None, None, None] 

248 for image_path in image_list: 

249 raster = Raster.from_file(image_path) 

250 self.raster_list.append(raster) 

251 

252 # Mise à jour de la bbox globale 

253 if bbox == [None, None, None, None]: 

254 bbox = list(raster.bbox) 

255 else: 

256 if bbox[0] > raster.bbox[0]: 

257 bbox[0] = raster.bbox[0] 

258 if bbox[1] > raster.bbox[1]: 

259 bbox[1] = raster.bbox[1] 

260 if bbox[2] < raster.bbox[2]: 

261 bbox[2] = raster.bbox[2] 

262 if bbox[3] < raster.bbox[3]: 

263 bbox[3] = raster.bbox[3] 

264 

265 # Inventaire des colors distinctes 

266 self.colors.add((raster.bands, raster.format)) 

267 

268 self.bbox = tuple(bbox) 

269 

270 return self 

271 

272 @classmethod 

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

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

275 

276 Args: 

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

278 

279 Examples: 

280 

281 Loading informations from a file stored descriptor 

282 

283 from rok4.raster import RasterSet 

284 

285 try: 

286 raster_set = RasterSet.from_descriptor( 

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

288 ) 

289 

290 except Exception as e: 

291 message = ("Cannot load information from descriptor file : {e}") 

292 print(message) 

293 

294 Raises: 

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

296 NotImplementedError: Storage type not handled 

297 

298 Returns: 

299 RasterSet: a RasterSet instance 

300 """ 

301 self = cls() 

302 

303 try: 

304 serialization = json.loads(get_data_str(path)) 

305 

306 except JSONDecodeError as e: 

307 raise FormatError("JSON", path, e) 

308 

309 self.srs = serialization["srs"] 

310 self.raster_list = [] 

311 for raster_dict in serialization["raster_list"]: 

312 parameters = deepcopy(raster_dict) 

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

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

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

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

317 

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

319 for color_dict in serialization["colors"]: 

320 self.colors.add((color_dict["bands"], ColorFormat[color_dict["format"]])) 

321 

322 return self 

323 

324 @property 

325 def serializable(self) -> Dict: 

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

327 

328 Returns: 

329 Dict: descriptor structured object description 

330 """ 

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

332 for color in self.colors: 

333 color_serial = {"bands": color[0], "format": color[1].name} 

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

335 for raster in self.raster_list: 

336 raster_dict = { 

337 "path": raster.path, 

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

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

340 "bands": raster.bands, 

341 "format": raster.format.name, 

342 } 

343 if raster.mask is not None: 

344 raster_dict["mask"] = raster.mask 

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

346 

347 return serialization 

348 

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

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

351 

352 Args: 

353 path (str, optional): Complete path (file or object) where to print the raster set's JSON. Defaults to None, JSON is printed to standard output. 

354 """ 

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

356 if path is None: 

357 print(content) 

358 else: 

359 put_data_str(content, path)