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
« 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
3The module contains the following class :
5- `Raster` - Structure describing raster data.
6- `RasterSet` - Structure describing a set of raster data.
7"""
9# -- IMPORTS --
11import json
12import re
13import tempfile
15# standard library
16from copy import deepcopy
17from json.decoder import JSONDecodeError
18from typing import Dict, Tuple
20# 3rd party
21from osgeo import gdal, ogr
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
35# -- GLOBALS --
37# Enable GDAL/OGR exceptions
38ogr.UseExceptions()
39gdal.UseExceptions()
42class Raster:
43 """A structure describing raster data
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 """
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
63 @classmethod
64 def from_file(cls, path: str) -> "Raster":
65 """Creates a Raster object from an image
67 Args:
68 path (str): path to the image file/object
70 Examples:
72 Loading informations from a file stored raster TIFF image
74 from rok4.raster import Raster
76 try:
77 raster = Raster.from_file(
78 "file:///data/SC1000/0040_6150_L93.tif"
79 )
81 except Exception as e:
82 print(f"Cannot load information from image : {e}")
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
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}'.")
96 self = cls()
98 work_image_path = get_osgeo_path(path)
100 image_datasource = gdal.Open(work_image_path)
101 self.path = path
103 path_pattern = re.compile("(/[^/]+?)[.][a-zA-Z0-9_-]+$")
104 mask_path = path_pattern.sub("\\1.msk", path)
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
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)
121 return self
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
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)
144 Examples:
146 Loading informations from parameters, related to
147 a TIFF main image coupled to a TIFF mask image
149 from rok4.raster import Raster
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 )
161 except Exception as e:
162 print(
163 f"Cannot load information from parameters : {e}"
164 )
166 Raises:
167 KeyError: a mandatory argument is missing
169 Returns:
170 Raster: a Raster instance
171 """
172 self = cls()
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
183class RasterSet:
184 """A structure describing a set of raster data
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 """
193 def __init__(self) -> None:
194 self.bbox = (None, None, None, None)
195 self.colors = set()
196 self.raster_list = []
197 self.srs = None
199 @classmethod
200 def from_list(cls, path: str, srs: str) -> "RasterSet":
201 """Instanciate a RasterSet from an images list path and a srs
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
207 Examples:
209 Loading informations from a file stored list
211 from rok4.raster import RasterSet
213 try:
214 raster_set = RasterSet.from_list(
215 path="file:///data/SC1000.list",
216 srs="EPSG:3857"
217 )
219 except Exception as e:
220 print(
221 f"Cannot load information from list file : {e}"
222 )
224 Raises:
225 RuntimeError: raised by OGR/GDAL if anything goes wrong
226 NotImplementedError: Storage type not handled
228 Returns:
229 RasterSet: a RasterSet instance
230 """
231 self = cls()
232 self.srs = srs
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)
245 remove(f"file://{list_file}")
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)
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]
265 # Inventaire des colors distinctes
266 self.colors.add((raster.bands, raster.format))
268 self.bbox = tuple(bbox)
270 return self
272 @classmethod
273 def from_descriptor(cls, path: str) -> "RasterSet":
274 """Creates a RasterSet object from a descriptor file or object
276 Args:
277 path (str): path to the descriptor file or object
279 Examples:
281 Loading informations from a file stored descriptor
283 from rok4.raster import RasterSet
285 try:
286 raster_set = RasterSet.from_descriptor(
287 "file:///data/images/descriptor.json"
288 )
290 except Exception as e:
291 message = ("Cannot load information from descriptor file : {e}")
292 print(message)
294 Raises:
295 RuntimeError: raised by OGR/GDAL if anything goes wrong
296 NotImplementedError: Storage type not handled
298 Returns:
299 RasterSet: a RasterSet instance
300 """
301 self = cls()
303 try:
304 serialization = json.loads(get_data_str(path))
306 except JSONDecodeError as e:
307 raise FormatError("JSON", path, e)
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))
318 self.bbox = tuple(serialization["bbox"])
319 for color_dict in serialization["colors"]:
320 self.colors.add((color_dict["bands"], ColorFormat[color_dict["format"]]))
322 return self
324 @property
325 def serializable(self) -> Dict:
326 """Get the dict version of the raster set, descriptor compliant
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)
347 return serialization
349 def write_descriptor(self, path: str = None) -> None:
350 """Print raster set's descriptor as JSON
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)