eoml 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- eoml/__init__.py +74 -0
- eoml/automation/__init__.py +7 -0
- eoml/automation/configuration.py +105 -0
- eoml/automation/dag.py +233 -0
- eoml/automation/experience.py +618 -0
- eoml/automation/tasks.py +825 -0
- eoml/bin/__init__.py +6 -0
- eoml/bin/clean_checkpoint.py +146 -0
- eoml/bin/land_cover_mapping_toml.py +435 -0
- eoml/bin/mosaic_images.py +137 -0
- eoml/data/__init__.py +7 -0
- eoml/data/basic_geo_data.py +214 -0
- eoml/data/dataset_utils.py +98 -0
- eoml/data/persistence/__init__.py +7 -0
- eoml/data/persistence/generic.py +253 -0
- eoml/data/persistence/lmdb.py +379 -0
- eoml/data/persistence/serializer.py +82 -0
- eoml/raster/__init__.py +7 -0
- eoml/raster/band.py +141 -0
- eoml/raster/dataset/__init__.py +6 -0
- eoml/raster/dataset/extractor.py +604 -0
- eoml/raster/raster_reader.py +602 -0
- eoml/raster/raster_utils.py +116 -0
- eoml/torch/__init__.py +7 -0
- eoml/torch/cnn/__init__.py +7 -0
- eoml/torch/cnn/augmentation.py +150 -0
- eoml/torch/cnn/dataset_evaluator.py +68 -0
- eoml/torch/cnn/db_dataset.py +605 -0
- eoml/torch/cnn/map_dataset.py +579 -0
- eoml/torch/cnn/map_dataset_const_mem.py +135 -0
- eoml/torch/cnn/outputs_transformer.py +130 -0
- eoml/torch/cnn/torch_utils.py +404 -0
- eoml/torch/cnn/training_dataset.py +241 -0
- eoml/torch/cnn/windows_dataset.py +120 -0
- eoml/torch/dataset/__init__.py +6 -0
- eoml/torch/dataset/shade_dataset_tester.py +46 -0
- eoml/torch/dataset/shade_tree_dataset_creators.py +537 -0
- eoml/torch/model_low_use.py +507 -0
- eoml/torch/models.py +282 -0
- eoml/torch/resnet.py +437 -0
- eoml/torch/sample_statistic.py +260 -0
- eoml/torch/trainer.py +782 -0
- eoml/torch/trainer_v2.py +253 -0
- eoml-0.9.0.dist-info/METADATA +93 -0
- eoml-0.9.0.dist-info/RECORD +47 -0
- eoml-0.9.0.dist-info/WHEEL +4 -0
- eoml-0.9.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import rasterio
|
|
5
|
+
from rasterio.transform import TransformMethodsMixin
|
|
6
|
+
from rasterio.windows import WindowMethodsMixin
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RasterInfo(WindowMethodsMixin, TransformMethodsMixin):
|
|
10
|
+
|
|
11
|
+
def __init__(self, transform, height, width, crs, bounds):
|
|
12
|
+
self.transform = transform
|
|
13
|
+
self.height = height
|
|
14
|
+
self.width = width
|
|
15
|
+
self.crs = crs
|
|
16
|
+
self.bounds = bounds
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def from_file(cls, path):
|
|
20
|
+
with rasterio.open(path) as src:
|
|
21
|
+
return cls(src.transform, src.height, src.width, src.crs, src.bounds)
|
|
22
|
+
|
|
23
|
+
def read_gdal_stats(path):
|
|
24
|
+
with open(path) as file:
|
|
25
|
+
# returns JSON object as VN
|
|
26
|
+
|
|
27
|
+
# a dictionary
|
|
28
|
+
data = json.load(file)
|
|
29
|
+
|
|
30
|
+
bands = data["bands"]
|
|
31
|
+
stats = np.zeros((len(bands),2))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
for b in data["bands"]:
|
|
35
|
+
stats[b["band"]-1]= np.array([b["mean"], b["stdDev"]])
|
|
36
|
+
return stats
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def normalize_sigma(data, means, std_devs, n, truncate=False, transform_no_data=None):
|
|
40
|
+
"""
|
|
41
|
+
Normalize in place, the values between mean +- n*sigma are compressed between 0 and 1
|
|
42
|
+
:param data: to normalize in place
|
|
43
|
+
:param means: of the original data
|
|
44
|
+
:param std_devs: of the original data
|
|
45
|
+
:param n: number of sigma to map betweren 0 and 1
|
|
46
|
+
:param truncate: weather to truncat value smaller or bigger than 0 or 1 to 0 or 1
|
|
47
|
+
:return: The array changed in place
|
|
48
|
+
"""
|
|
49
|
+
for b in range(len(data)):
|
|
50
|
+
data[b] = (1 + (data[b] - means[b]) / (n * std_devs[b])) / 2
|
|
51
|
+
|
|
52
|
+
if transform_no_data is not None:
|
|
53
|
+
np.nan_to_num(data, copy=False, nan=transform_no_data, posinf=None, neginf=None)
|
|
54
|
+
|
|
55
|
+
if truncate:
|
|
56
|
+
bigger = data > 1
|
|
57
|
+
data[bigger] = 1
|
|
58
|
+
smaller = data < 0
|
|
59
|
+
data[smaller] = 0
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class NaNToNumber:
|
|
64
|
+
def __init__(self, number):
|
|
65
|
+
"""
|
|
66
|
+
"""
|
|
67
|
+
self.number = number
|
|
68
|
+
def __call__(self, data):
|
|
69
|
+
np.nan_to_num(data, copy=False, nan=self.number, posinf=None, neginf=None)
|
|
70
|
+
return data
|
|
71
|
+
|
|
72
|
+
class SigmaNormalizer:
|
|
73
|
+
def __init__(self, means, std_devs, n, truncate=False, transform_no_data=None):
|
|
74
|
+
"""
|
|
75
|
+
Normalize in place, the values between mean +- n*sigma are compressed between 0 and 1
|
|
76
|
+
Object version of function. Usefull for multi threading usinf the spawn methode
|
|
77
|
+
:param means: of the original data
|
|
78
|
+
:param std_devs: of the original data
|
|
79
|
+
:param n: number of sigma to map betweren 0 and 1
|
|
80
|
+
:param truncate: weather to truncat value smaller or bigger than 0 or 1 to 0 or 1
|
|
81
|
+
:return: The array changed in place
|
|
82
|
+
"""
|
|
83
|
+
self.means = means
|
|
84
|
+
self.std_devs = std_devs
|
|
85
|
+
self.n = n
|
|
86
|
+
self.truncate = truncate
|
|
87
|
+
self.transform_no_data = transform_no_data
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def __call__(self, data):
|
|
91
|
+
normalize_sigma(data, self.means, self.std_devs, self.n, self.truncate, self.transform_no_data)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class CastSigmaNormalizer:
|
|
95
|
+
def __init__(self, means, std_devs, n, truncate=False, transform_no_data=None, dtype=None):
|
|
96
|
+
"""
|
|
97
|
+
Normalize in place, the values between mean +- n*sigma are compressed between 0 and 1
|
|
98
|
+
Object version of function. Usefull for multi threading usinf the spawn methode
|
|
99
|
+
:param means: of the original data
|
|
100
|
+
:param std_devs: of the original data
|
|
101
|
+
:param n: number of sigma to map betweren 0 and 1
|
|
102
|
+
:param truncate: weather to truncat value smaller or bigger than 0 or 1 to 0 or 1
|
|
103
|
+
:return: The array changed in place
|
|
104
|
+
"""
|
|
105
|
+
self.means = means
|
|
106
|
+
self.std_devs = std_devs
|
|
107
|
+
self.n = n
|
|
108
|
+
self.truncate = truncate
|
|
109
|
+
self.transform_no_data = transform_no_data
|
|
110
|
+
self.dtype = dtype
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def __call__(self, data):
|
|
114
|
+
if self.dtype is not None:
|
|
115
|
+
data.astype(self.dtype, copy=False)
|
|
116
|
+
normalize_sigma(data, self.means, self.std_devs, self.n, self.truncate, self.transform_no_data)
|
eoml/torch/__init__.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Data augmentation transformations for PyTorch image datasets.
|
|
2
|
+
|
|
3
|
+
This module provides transformation classes and functions for augmenting image data
|
|
4
|
+
during training. Includes rotation, flipping, cropping, scaling, shearing, and
|
|
5
|
+
blurring operations using torchvision's functional API.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import math
|
|
10
|
+
import random
|
|
11
|
+
|
|
12
|
+
import torchvision.transforms.functional as TF
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def rotate_crop_flip_transform(img, size=13, angle=180, vflip=False):
|
|
18
|
+
"""Apply rotation, crop, and optional flip to an image.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
img: Input image tensor.
|
|
22
|
+
size: Size to crop to after rotation. Defaults to 13.
|
|
23
|
+
angle: Rotation angle in degrees. Defaults to 180.
|
|
24
|
+
vflip: Whether to apply vertical flip. Defaults to False.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Transformed image tensor.
|
|
28
|
+
"""
|
|
29
|
+
img = TF.rotate(img, angle=angle)
|
|
30
|
+
img = TF.center_crop(img, size)
|
|
31
|
+
if vflip:
|
|
32
|
+
img = TF.vflip(img)
|
|
33
|
+
|
|
34
|
+
return img
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Due to memory bug in dataset, the dataset return numpy type that we cast to int Todo need to be fixed in dataset
|
|
38
|
+
def rotate_flip_transform(img, angle=180, vflip=False):
|
|
39
|
+
"""Apply rotation and optional flip to an image.
|
|
40
|
+
|
|
41
|
+
Note:
|
|
42
|
+
Due to dataset memory bug, parameters are cast from numpy types.
|
|
43
|
+
|
|
44
|
+
Todo:
|
|
45
|
+
Fix dataset to avoid numpy type issue.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
img: Input image tensor.
|
|
49
|
+
angle: Rotation angle in degrees. Defaults to 180.
|
|
50
|
+
vflip: Whether to apply vertical flip. Defaults to False.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Transformed image tensor.
|
|
54
|
+
"""
|
|
55
|
+
img = TF.rotate(img, angle=int(angle))
|
|
56
|
+
if bool(vflip):
|
|
57
|
+
img = TF.vflip(img)
|
|
58
|
+
|
|
59
|
+
return img
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class CropTransform:
|
|
63
|
+
"""Crop images to a specified size.
|
|
64
|
+
|
|
65
|
+
Useful for working with databases containing samples larger than needed,
|
|
66
|
+
allowing on-the-fly cropping to the desired size.
|
|
67
|
+
|
|
68
|
+
Attributes:
|
|
69
|
+
width: Target width/size for square crop.
|
|
70
|
+
"""
|
|
71
|
+
def __init__(self, width):
|
|
72
|
+
self.width = width
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def __call__(self, inputs):
|
|
76
|
+
|
|
77
|
+
inputs = TF.center_crop(inputs, self.width)
|
|
78
|
+
return inputs
|
|
79
|
+
|
|
80
|
+
def __repr__(self):
|
|
81
|
+
return f'CropTransform(width: {self.width})'
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class RandomTransform:
|
|
85
|
+
"""Randomly apply augmentation transformations to images.
|
|
86
|
+
|
|
87
|
+
Applies random combinations of rotation, flip, scale, shear, and blur,
|
|
88
|
+
then crops to the specified size. Useful for data augmentation during training.
|
|
89
|
+
|
|
90
|
+
Attributes:
|
|
91
|
+
width: Target width for final crop.
|
|
92
|
+
p_rot: Probability of applying rotation. Defaults to 0.50.
|
|
93
|
+
p_flip: Probability of applying vertical flip. Defaults to 0.50.
|
|
94
|
+
p_scale: Probability of applying scaling. Defaults to 0.2.
|
|
95
|
+
p_shear: Probability of applying shear. Defaults to 0.2.
|
|
96
|
+
p_blur: Probability of applying Gaussian blur. Defaults to 0.2.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __init__(self, width, p_rot=0.50, p_flip=0.50, p_scale=0.2, p_shear= 0.2, p_blur= 0.2):
|
|
100
|
+
self.width = width
|
|
101
|
+
self.p_rot = p_rot
|
|
102
|
+
self.p_flip = p_flip
|
|
103
|
+
self.p_scale = p_scale
|
|
104
|
+
self.p_shear = p_shear
|
|
105
|
+
self.p_blur = p_blur
|
|
106
|
+
|
|
107
|
+
def __repr__(self):
|
|
108
|
+
return f'RandomTransform(width: {self.width}, ' \
|
|
109
|
+
f'p_rot: {self.p_rot} , ' \
|
|
110
|
+
f'p_flip: {self.p_flip} , ' \
|
|
111
|
+
f'p_scale: {self.p_scale} , ' \
|
|
112
|
+
f'p_shear: {self.p_shear} , ' \
|
|
113
|
+
f'p_blur: {self.p_blur})' \
|
|
114
|
+
|
|
115
|
+
# size need to be multiplied by to avoid dark pixel
|
|
116
|
+
# 1.4145
|
|
117
|
+
def __call__(self, inputs):
|
|
118
|
+
c, i_h, i_w = inputs.shape
|
|
119
|
+
|
|
120
|
+
rotation_angle = random.randint(-180, 180) if self.p_rot > random.uniform(0, 1) else 0
|
|
121
|
+
shear = random.randint(-15, 15) if self.p_shear > random.uniform(0, 1) else 0
|
|
122
|
+
scale = random.randint(2, 4) if self.p_scale > random.uniform(0, 1) else 1
|
|
123
|
+
flip = True if self.p_flip > random.uniform(0, 1) else False
|
|
124
|
+
|
|
125
|
+
blur = True if self.p_blur > random.uniform(0, 1) else False
|
|
126
|
+
|
|
127
|
+
# distance to the border to avoid black border du to rotation
|
|
128
|
+
safe_width = math.ceil(1.4143* self.width)
|
|
129
|
+
|
|
130
|
+
if i_h < safe_width:
|
|
131
|
+
logger.warning("Transformation: the width of the input is not big enough and may be truncated.")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
#input = TF.rotate(input, rotation_angle, interpolation=TF.InterpolationMode.BILINEAR)
|
|
135
|
+
# affine
|
|
136
|
+
inputs = TF.affine(inputs, angle=rotation_angle, translate=[0,0], scale=scale, shear=shear,
|
|
137
|
+
interpolation=TF.InterpolationMode.NEAREST)
|
|
138
|
+
|
|
139
|
+
inputs = TF.center_crop(inputs, self.width)
|
|
140
|
+
|
|
141
|
+
# flip
|
|
142
|
+
if flip:
|
|
143
|
+
inputs = TF.vflip(inputs)
|
|
144
|
+
|
|
145
|
+
# gaussian
|
|
146
|
+
if blur:
|
|
147
|
+
inputs = TF.gaussian_blur(inputs, kernel_size=[3, 3], sigma=[0.45, 0.45])
|
|
148
|
+
|
|
149
|
+
return inputs
|
|
150
|
+
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Model evaluation utilities for PyTorch datasets.
|
|
2
|
+
|
|
3
|
+
This module provides classes for evaluating trained neural network models on
|
|
4
|
+
datasets, collecting predictions and reference values for analysis.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import torch
|
|
9
|
+
from tqdm import tqdm
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DatasetEvaluator:
|
|
13
|
+
"""Evaluate a neural network model on a dataset.
|
|
14
|
+
|
|
15
|
+
Runs inference on a complete dataset and collects predictions along with
|
|
16
|
+
reference labels for evaluation metrics.
|
|
17
|
+
|
|
18
|
+
Todo:
|
|
19
|
+
Implement aggressive/optimized version.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
model: PyTorch model or path to JIT-compiled model.
|
|
23
|
+
"""
|
|
24
|
+
def __init__(self, model):
|
|
25
|
+
|
|
26
|
+
if isinstance(model, str):
|
|
27
|
+
self.model = torch.jit.load(model)
|
|
28
|
+
else:
|
|
29
|
+
self.model = model
|
|
30
|
+
|
|
31
|
+
def evaluate(self, loader, device="cpu"):
|
|
32
|
+
"""Evaluate model on dataset and collect predictions.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
loader: PyTorch DataLoader providing test samples.
|
|
36
|
+
device: Device to run inference on ('cpu' or 'cuda'). Defaults to "cpu".
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
tuple: (reference_labels, predictions) as numpy arrays.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
# Make sure gradient tracking is off, and do a pass over the data
|
|
43
|
+
self.model.train(False)
|
|
44
|
+
|
|
45
|
+
results=[]
|
|
46
|
+
reference=[]
|
|
47
|
+
|
|
48
|
+
with torch.inference_mode():
|
|
49
|
+
|
|
50
|
+
with tqdm(total=len(loader),desc="Batch") as pbar:
|
|
51
|
+
for i, data in enumerate(loader):
|
|
52
|
+
# Every data instance is an input + label pair
|
|
53
|
+
|
|
54
|
+
inputs, labels = data
|
|
55
|
+
if device is not None:
|
|
56
|
+
if isinstance(inputs, (list, tuple)):
|
|
57
|
+
inputs = map(lambda x: x.to(device, non_blocking=True), inputs)
|
|
58
|
+
else:
|
|
59
|
+
inputs = inputs.to(device, non_blocking=True)
|
|
60
|
+
|
|
61
|
+
# Make predictions for this batch
|
|
62
|
+
outputs = self.model(*inputs)
|
|
63
|
+
|
|
64
|
+
results.extend(outputs.cpu())
|
|
65
|
+
reference.extend(labels)
|
|
66
|
+
|
|
67
|
+
return np.array(reference), np.array(results)
|
|
68
|
+
|