Skip to content

Commit 41338ea

Browse files
authored
Merge pull request #37 from rok4/feature/rasterset
Feature/rasterset
2 parents c54617d + a309738 commit 41338ea

File tree

3 files changed

+752
-197
lines changed

3 files changed

+752
-197
lines changed

src/rok4/Raster.py

Lines changed: 243 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1-
"""Provide functions to read information on raster data from file path or object path
1+
"""Provide functions to read information on raster data
22
33
The module contains the following class :
44
5-
- 'Raster' - Structure describing raster data.
6-
5+
- Raster - Structure describing raster data.
6+
- RasterSet - Structure describing a set of raster data.
77
"""
88

9+
import copy
10+
import json
911
import re
1012
from enum import Enum
13+
from typing import Tuple, Dict
1114

1215
from osgeo import ogr, gdal
13-
from typing import Tuple
1416

15-
from rok4.Storage import exists, get_osgeo_path
17+
from rok4.Storage import exists, get_osgeo_path, put_data_str
1618
from rok4.Utils import ColorFormat, compute_bbox,compute_format
1719

1820
# Enable GDAL/OGR exceptions
@@ -23,12 +25,18 @@ class Raster:
2325
"""A structure describing raster data
2426
2527
Attributes :
26-
path (str): path to the file/object (ex: file:///path/to/image.tif or s3://bucket/path/to/image.tif)
27-
bbox (Tuple[float, float, float, float]): bounding rectange in the data projection
28+
path (str): path to the file/object (ex:
29+
file:///path/to/image.tif or s3://bucket/path/to/image.tif)
30+
bbox (Tuple[float, float, float, float]): bounding rectange
31+
in the data projection
2832
bands (int): number of color bands (or channels)
29-
format (ColorFormat): numeric variable format for color values. Bit depth, as bits per channel, can be derived from it.
30-
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. ex: file:///path/to/image.msk or s3://bucket/path/to/image.msk)
31-
dimensions (Tuple[int, int]): image width and height expressed in pixels
33+
format (ColorFormat): numeric variable format for color values.
34+
Bit depth, as bits per channel, can be derived from it.
35+
mask (str): path to the associated mask file or object, if any,
36+
or None (same path as the image, but with a ".msk" extension
37+
and TIFF format. ex:
38+
file:///path/to/image.msk or s3://bucket/path/to/image.msk)
39+
dimensions (Tuple[int, int]): image width and height, in pixels
3240
"""
3341

3442
def __init__(self) -> None:
@@ -40,7 +48,7 @@ def __init__(self) -> None:
4048
self.path = None
4149

4250
@classmethod
43-
def from_file(cls, path: str) -> 'Raster':
51+
def from_file(cls, path: str) -> "Raster":
4452
"""Creates a Raster object from an image
4553
4654
Args:
@@ -53,7 +61,9 @@ def from_file(cls, path: str) -> 'Raster':
5361
from rok4.Raster import Raster
5462
5563
try:
56-
raster = Raster.from_file("file:///data/images/SC1000_TIFF_LAMB93_FXX/SC1000_0040_6150_L93.tif")
64+
raster = Raster.from_file(
65+
"file:///data/SC1000/0040_6150_L93.tif"
66+
)
5767
5868
except Exception as e:
5969
print(f"Cannot load information from image : {e}")
@@ -65,7 +75,6 @@ def from_file(cls, path: str) -> 'Raster':
6575
Returns:
6676
Raster: a Raster instance
6777
"""
68-
6978
if not exists(path):
7079
raise Exception(f"No file or object found at path '{path}'.")
7180

@@ -76,15 +85,16 @@ def from_file(cls, path: str) -> 'Raster':
7685
image_datasource = gdal.Open(work_image_path)
7786
self.path = path
7887

79-
path_pattern = re.compile('(/[^/]+?)[.][a-zA-Z0-9_-]+$')
80-
mask_path = path_pattern.sub('\\1.msk', path)
88+
path_pattern = re.compile("(/[^/]+?)[.][a-zA-Z0-9_-]+$")
89+
mask_path = path_pattern.sub("\\1.msk", path)
8190

8291
if exists(mask_path):
8392
work_mask_path = get_osgeo_path(mask_path)
8493
mask_driver = gdal.IdentifyDriver(work_mask_path).ShortName
8594
if 'GTiff' != mask_driver:
86-
raise Exception(f"Mask file '{mask_path}' is not a TIFF image. (GDAL driver : '{mask_driver}'")
87-
95+
message = (f"Mask file '{mask_path}' is not a TIFF image."
96+
+ f" (GDAL driver : '{mask_driver}'")
97+
raise Exception(message)
8898
self.mask = mask_path
8999
else:
90100
self.mask = None
@@ -97,36 +107,55 @@ def from_file(cls, path: str) -> 'Raster':
97107
return self
98108

99109
@classmethod
100-
def from_parameters(cls, path: str, bands: int, bbox: Tuple[float, float, float, float], dimensions: Tuple[int, int], format: ColorFormat, mask: str = None) -> 'Raster':
110+
def from_parameters(
111+
cls, path: str, bands: int, bbox: Tuple[float, float, float, float],
112+
dimensions: Tuple[int, int], format: ColorFormat, mask: str = None) -> "Raster":
101113
"""Creates a Raster object from parameters
102114
103115
Args:
104-
path (str): path to the file/object (ex: file:///path/to/image.tif or s3://bucket/path/to/image.tif)
116+
path (str): path to the file/object (ex:
117+
file:///path/to/image.tif or s3://bucket/image.tif)
105118
bands (int): number of color bands (or channels)
106-
bbox (Tuple[float, float, float, float]): bounding rectange in the data projection
107-
dimensions (Tuple[int, int]): image width and height expressed in pixels
108-
format (ColorFormat): numeric variable format for color values. Bit depth, as bits per channel, can be derived from it.
109-
mask (str, optionnal): 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. ex: file:///path/to/image.msk or s3://bucket/path/to/image.msk)
119+
bbox (Tuple[float, float, float, float]): bounding rectange
120+
in the data projection
121+
dimensions (Tuple[int, int]): image width and height
122+
expressed in pixels
123+
format (ColorFormat): numeric format for color values.
124+
Bit depth, as bits per channel, can be derived from it.
125+
mask (str, optionnal): path to the associated mask, if any,
126+
or None (same path as the image, but with a
127+
".msk" extension and TIFF format. ex:
128+
file:///path/to/image.msk or s3://bucket/image.msk)
110129
111130
Examples:
112131
113-
Loading informations from parameters, related to a TIFF main image coupled to a TIFF mask image
132+
Loading informations from parameters, related to
133+
a TIFF main image coupled to a TIFF mask image
114134
115135
from rok4.Raster import Raster
116136
117137
try:
118-
raster = Raster.from_parameters(path="file:///data/images/SC1000_TIFF_LAMB93_FXX/SC1000_0040_6150_L93.tif", mask="file:///data/images/SC1000_TIFF_LAMB93_FXX/SC1000_0040_6150_L93.msk", bands=3, format=ColorFormat.UINT8, dimensions=(2000, 2000), bbox=(40000.000, 5950000.000, 240000.000, 6150000.000))
138+
raster = Raster.from_parameters(
139+
path="file:///data/SC1000/_0040_6150_L93.tif",
140+
mask="file:///data/SC1000/0040_6150_L93.msk",
141+
bands=3,
142+
format=ColorFormat.UINT8,
143+
dimensions=(2000, 2000),
144+
bbox=(40000.000, 5950000.000,
145+
240000.000, 6150000.000)
146+
)
119147
120148
except Exception as e:
121-
print(f"Cannot load information from parameters : {e}")
149+
print(
150+
f"Cannot load information from parameters : {e}"
151+
)
122152
123153
Raises:
124154
KeyError: a mandatory argument is missing
125155
126156
Returns:
127157
Raster: a Raster instance
128158
"""
129-
130159
self = cls()
131160

132161
self.path = path
@@ -135,5 +164,192 @@ def from_parameters(cls, path: str, bands: int, bbox: Tuple[float, float, float,
135164
self.dimensions = dimensions
136165
self.format = format
137166
self.mask = mask
167+
return self
168+
169+
170+
class RasterSet:
171+
"""A structure describing a set of raster data
172+
173+
Attributes :
174+
raster_list (List[Raster]): List of Raster instances in the set
175+
colors (List[Dict]): List of color properties for each raster
176+
instance. Contains only one element if
177+
the set is homogenous.
178+
Element properties:
179+
bands (int): number of color bands (or channels)
180+
format (ColorFormat): numeric variable format for
181+
color values. Bit depth, as bits per channel,
182+
can be derived from it.
183+
srs (str): Name of the set's spatial reference system
184+
bbox (Tuple[float, float, float, float]): bounding rectange
185+
in the data projection, enclosing the whole set
186+
"""
187+
188+
def __init__(self) -> None:
189+
self.bbox = (None, None, None, None)
190+
self.colors = []
191+
self.raster_list = []
192+
self.srs = None
193+
194+
@classmethod
195+
def from_list(cls, path: str, srs: str) -> "RasterSet":
196+
"""Instanciate a RasterSet from an images list path and a srs
197+
198+
Args:
199+
path (str): path to the images list file or object
200+
(each line in this list contains the path to
201+
an image file or object in the set)
202+
203+
Examples:
204+
205+
Loading informations from a file stored list
206+
207+
from rok4.Raster import RasterSet
208+
209+
try:
210+
raster_set = RasterSet.from_list(
211+
path="file:///data/SC1000.list",
212+
srs="EPSG:3857"
213+
)
214+
215+
except Exception as e:
216+
print(
217+
f"Cannot load information from list file : {e}"
218+
)
219+
220+
Raises:
221+
RuntimeError: raised by OGR/GDAL if anything goes wrong
222+
NotImplementedError: Storage type not handled
223+
224+
Returns:
225+
RasterSet: a RasterSet instance
226+
"""
227+
self = cls()
228+
self.srs = srs
229+
230+
local_list_path = get_osgeo_path(path)
231+
image_list = []
232+
with open(file=local_list_path, mode="r") as list_file:
233+
for line in list_file:
234+
image_path = line.strip(" \t\n\r")
235+
image_list.append(image_path)
236+
237+
temp_bbox = [None, None, None, None]
238+
for image_path in image_list:
239+
raster = Raster.from_file(image_path)
240+
self.raster_list.append(raster)
241+
if temp_bbox == [None, None, None, None]:
242+
for i in range(0, 4, 1):
243+
temp_bbox[i] = raster.bbox[i]
244+
else:
245+
if temp_bbox[0] > raster.bbox[0]:
246+
temp_bbox[0] = raster.bbox[0]
247+
if temp_bbox[1] > raster.bbox[1]:
248+
temp_bbox[1] = raster.bbox[1]
249+
if temp_bbox[2] < raster.bbox[2]:
250+
temp_bbox[2] = raster.bbox[2]
251+
if temp_bbox[3] < raster.bbox[3]:
252+
temp_bbox[3] = raster.bbox[3]
253+
color_dict = {"bands": raster.bands, "format": raster.format}
254+
if color_dict not in self.colors:
255+
self.colors.append(color_dict)
256+
self.bbox = tuple(temp_bbox)
257+
return self
258+
259+
@classmethod
260+
def from_descriptor(cls, path: str) -> "RasterSet":
261+
"""Creates a RasterSet object from a descriptor file or object
262+
263+
Args:
264+
path (str): path to the descriptor file or object
265+
266+
Examples:
267+
268+
Loading informations from a file stored descriptor
269+
270+
from rok4.Raster import RasterSet
271+
272+
try:
273+
raster_set = RasterSet.from_descriptor(
274+
"file:///data/images/descriptor.json"
275+
)
276+
277+
except Exception as e:
278+
message = ("Cannot load information from "
279+
+ f"descriptor file : {e}")
280+
print(message)
138281
282+
Raises:
283+
RuntimeError: raised by OGR/GDAL if anything goes wrong
284+
NotImplementedError: Storage type not handled
285+
286+
Returns:
287+
RasterSet: a RasterSet instance
288+
"""
289+
self = cls()
290+
descriptor_path = get_osgeo_path(path)
291+
with open(file=descriptor_path, mode="r") as file_handle:
292+
raw_content = file_handle.read()
293+
serialization = json.loads(raw_content)
294+
self.srs = serialization["srs"]
295+
self.raster_list = []
296+
for raster_dict in serialization["raster_list"]:
297+
parameters = copy.deepcopy(raster_dict)
298+
parameters["bbox"] = tuple(raster_dict["bbox"])
299+
parameters["dimensions"] = tuple(raster_dict["dimensions"])
300+
parameters["format"] = ColorFormat[ raster_dict["format"] ]
301+
self.raster_list.append(Raster.from_parameters(**parameters))
302+
self.bbox = tuple(serialization["bbox"])
303+
self.colors = []
304+
for color_dict in serialization["colors"]:
305+
color_item = copy.deepcopy(color_dict)
306+
color_item["format"] = ColorFormat[ color_dict["format"] ]
307+
self.colors.append(color_item)
139308
return self
309+
310+
@property
311+
def serializable(self) -> Dict:
312+
"""Get the dict version of the raster set, descriptor compliant
313+
314+
Returns:
315+
Dict: descriptor structured object description
316+
"""
317+
serialization = {
318+
"bbox": list(self.bbox),
319+
"srs": self.srs,
320+
"colors": [],
321+
"raster_list" : []
322+
}
323+
for color in self.colors:
324+
color_serial = {
325+
"bands": color["bands"],
326+
"format": color["format"].name
327+
}
328+
serialization["colors"].append(color_serial)
329+
for raster in self.raster_list:
330+
raster_dict = {
331+
"path": raster.path,
332+
"dimensions": list(raster.dimensions),
333+
"bbox": list(raster.bbox),
334+
"bands": raster.bands,
335+
"format": raster.format.name
336+
}
337+
if raster.mask is not None:
338+
raster_dict["mask"] = raster.mask
339+
serialization["raster_list"].append(raster_dict)
340+
return serialization
341+
342+
def write_descriptor(self, path: str = None) -> None:
343+
"""Print raster set's descriptor as JSON
344+
345+
Args:
346+
path (str, optional): Complete path (file or object)
347+
where to print the raster set's JSON. Defaults to None,
348+
JSON is printed to standard output.
349+
"""
350+
content = json.dumps(self.serializable, sort_keys=True)
351+
if path is None:
352+
print(content)
353+
else:
354+
put_data_str(content, path)
355+

0 commit comments

Comments
 (0)