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
« 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
3The module contains the following class :
5 - Raster - Structure describing raster data.
6 - RasterSet - Structure describing a set of raster data.
7"""
9import copy
10import json
11import re
12from enum import Enum
13from typing import Tuple, Dict
15from osgeo import ogr, gdal
17from rok4.Storage import exists, get_osgeo_path, put_data_str
18from rok4.Utils import ColorFormat, compute_bbox, compute_format
20# Enable GDAL/OGR exceptions
21ogr.UseExceptions()
22gdal.UseExceptions()
25class Raster:
26 """A structure describing raster data
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 """
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
51 @classmethod
52 def from_file(cls, path: str) -> "Raster":
53 """Creates a Raster object from an image
55 Args:
56 path (str): path to the image file/object
58 Examples:
60 Loading informations from a file stored raster TIFF image
62 from rok4.Raster import Raster
64 try:
65 raster = Raster.from_file(
66 "file:///data/SC1000/0040_6150_L93.tif"
67 )
69 except Exception as e:
70 print(f"Cannot load information from image : {e}")
72 Raises:
73 RuntimeError: raised by OGR/GDAL if anything goes wrong
74 NotImplementedError: Storage type not handled
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}'.")
82 self = cls()
84 work_image_path = get_osgeo_path(path)
86 image_datasource = gdal.Open(work_image_path)
87 self.path = path
89 path_pattern = re.compile("(/[^/]+?)[.][a-zA-Z0-9_-]+$")
90 mask_path = path_pattern.sub("\\1.msk", path)
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
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)
110 return self
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
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)
139 Examples:
141 Loading informations from parameters, related to
142 a TIFF main image coupled to a TIFF mask image
144 from rok4.Raster import Raster
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 )
157 except Exception as e:
158 print(
159 f"Cannot load information from parameters : {e}"
160 )
162 Raises:
163 KeyError: a mandatory argument is missing
165 Returns:
166 Raster: a Raster instance
167 """
168 self = cls()
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
179class RasterSet:
180 """A structure describing a set of raster data
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 """
197 def __init__(self) -> None:
198 self.bbox = (None, None, None, None)
199 self.colors = []
200 self.raster_list = []
201 self.srs = None
203 @classmethod
204 def from_list(cls, path: str, srs: str) -> "RasterSet":
205 """Instanciate a RasterSet from an images list path and a srs
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)
212 Examples:
214 Loading informations from a file stored list
216 from rok4.Raster import RasterSet
218 try:
219 raster_set = RasterSet.from_list(
220 path="file:///data/SC1000.list",
221 srs="EPSG:3857"
222 )
224 except Exception as e:
225 print(
226 f"Cannot load information from list file : {e}"
227 )
229 Raises:
230 RuntimeError: raised by OGR/GDAL if anything goes wrong
231 NotImplementedError: Storage type not handled
233 Returns:
234 RasterSet: a RasterSet instance
235 """
236 self = cls()
237 self.srs = srs
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)
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
268 @classmethod
269 def from_descriptor(cls, path: str) -> "RasterSet":
270 """Creates a RasterSet object from a descriptor file or object
272 Args:
273 path (str): path to the descriptor file or object
275 Examples:
277 Loading informations from a file stored descriptor
279 from rok4.Raster import RasterSet
281 try:
282 raster_set = RasterSet.from_descriptor(
283 "file:///data/images/descriptor.json"
284 )
286 except Exception as e:
287 message = ("Cannot load information from "
288 + f"descriptor file : {e}")
289 print(message)
291 Raises:
292 RuntimeError: raised by OGR/GDAL if anything goes wrong
293 NotImplementedError: Storage type not handled
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
319 @property
320 def serializable(self) -> Dict:
321 """Get the dict version of the raster set, descriptor compliant
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
343 def write_descriptor(self, path: str = None) -> None:
344 """Print raster set's descriptor as JSON
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)