rio-diff 1.0.0a0__tar.gz → 1.0.dev1__tar.gz
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.
- rio_diff-1.0.dev1/PKG-INFO +22 -0
- rio_diff-1.0.dev1/README.md +3 -0
- {rio_diff-1.0.0a0 → rio_diff-1.0.dev1}/rio_diff/__init__.py +1 -1
- rio_diff-1.0.dev1/rio_diff/compare.py +243 -0
- {rio_diff-1.0.0a0 → rio_diff-1.0.dev1}/rio_diff/scripts/cli.py +16 -57
- rio_diff-1.0.0a0/PKG-INFO +0 -145
- rio_diff-1.0.0a0/README.md +0 -126
- rio_diff-1.0.0a0/rio_diff/compare.py +0 -224
- rio_diff-1.0.0a0/rio_diff/models.py +0 -96
- rio_diff-1.0.0a0/rio_diff/render.py +0 -129
- rio_diff-1.0.0a0/rio_diff/utils.py +0 -11
- {rio_diff-1.0.0a0 → rio_diff-1.0.dev1}/.gitignore +0 -0
- {rio_diff-1.0.0a0 → rio_diff-1.0.dev1}/LICENSE +0 -0
- {rio_diff-1.0.0a0 → rio_diff-1.0.dev1}/pyproject.toml +0 -0
- {rio_diff-1.0.0a0 → rio_diff-1.0.dev1}/rio_diff/scripts/__init__.py +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rio-diff
|
|
3
|
+
Version: 1.0.dev1
|
|
4
|
+
Summary: Raster comparison plugin for rasterio
|
|
5
|
+
Project-URL: Homepage, https://github.com/perminovsi/rio-diff
|
|
6
|
+
Author-email: Sergey Perminov <perminovsi@ya.ru>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: raster,rasterio,rio-plugin
|
|
10
|
+
Classifier: Intended Audience :: Information Technology
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Requires-Dist: click>=7.0
|
|
16
|
+
Requires-Dist: numpy>=2.0
|
|
17
|
+
Requires-Dist: rasterio>=1.3
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# rio-diff
|
|
21
|
+
|
|
22
|
+
A rasterio plugin for comparing two raster files and showing differences between them.
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import rasterio
|
|
7
|
+
from affine import Affine
|
|
8
|
+
from rasterio.coords import BoundingBox
|
|
9
|
+
from rasterio.crs import CRS
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class RasterProps:
|
|
14
|
+
width: int
|
|
15
|
+
height: int
|
|
16
|
+
bands: int
|
|
17
|
+
dtype: str
|
|
18
|
+
nodata: float | None
|
|
19
|
+
bbox: BoundingBox
|
|
20
|
+
crs: CRS
|
|
21
|
+
transform: Affine
|
|
22
|
+
metadata: list[dict[str, Any]]
|
|
23
|
+
bands_metadata: list
|
|
24
|
+
stats: list
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def calc_hash(inp_file: str) -> str:
|
|
28
|
+
hash = hashlib.md5()
|
|
29
|
+
|
|
30
|
+
with open(inp_file, "rb") as file:
|
|
31
|
+
for chunk in iter(lambda: file.read(4096), b""):
|
|
32
|
+
hash.update(chunk)
|
|
33
|
+
|
|
34
|
+
return hash.hexdigest()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def read_raster_props(inp_file: str) -> RasterProps:
|
|
38
|
+
with rasterio.open(inp_file) as ds:
|
|
39
|
+
return RasterProps(
|
|
40
|
+
width=ds.profile["width"],
|
|
41
|
+
height=ds.profile["height"],
|
|
42
|
+
bands=ds.profile["count"],
|
|
43
|
+
dtype=ds.profile["dtype"],
|
|
44
|
+
nodata=ds.profile["nodata"], # TODO: проверка для разных каналов
|
|
45
|
+
bbox=ds.bounds,
|
|
46
|
+
crs=ds.profile["crs"],
|
|
47
|
+
transform=ds.profile["transform"],
|
|
48
|
+
metadata=ds.tags(),
|
|
49
|
+
bands_metadata=[ds.tags(bidx=bidx) for bidx in range(1, ds.count + 1)],
|
|
50
|
+
stats=ds.stats(), # TODO: безопаснее будет считать самому по numpy
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def calc_diff(base_raster: str, test_raster: str, *, rtol=0, atol=0, equal_nan=True) -> bool:
|
|
55
|
+
"""Вычитать первый растр из второго для получения diff-a и его последующего анализа
|
|
56
|
+
Сколько пикселей отличается, насколько они отличаются и т.п.
|
|
57
|
+
Опционально выводить график (картинку) и возможность сохранения diff-a на диск
|
|
58
|
+
"""
|
|
59
|
+
with rasterio.open(base_raster) as base, rasterio.open(test_raster) as test:
|
|
60
|
+
if (
|
|
61
|
+
base.count != test.count or
|
|
62
|
+
base.shape != test.shape or
|
|
63
|
+
base.transform != test.transform or
|
|
64
|
+
base.crs != test.crs or
|
|
65
|
+
base.dtypes != test.dtypes
|
|
66
|
+
):
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
for bidx in range(1, base.count + 1):
|
|
70
|
+
nd_base= base.nodatavals[bidx - 1] if base.nodatavals else base.nodata
|
|
71
|
+
nd_test = test.nodatavals[bidx - 1] if test.nodatavals else test.nodata
|
|
72
|
+
|
|
73
|
+
for _, window in base.block_windows(bidx):
|
|
74
|
+
arr_base = base.read(bidx, window=window)
|
|
75
|
+
arr_test = test.read(bidx, window=window)
|
|
76
|
+
|
|
77
|
+
if nd_base is not None:
|
|
78
|
+
mask_base = arr_base == nd_base
|
|
79
|
+
arr_base = arr_base.astype("float64", copy=False)
|
|
80
|
+
arr_base[mask_base] = np.nan
|
|
81
|
+
if nd_test is not None:
|
|
82
|
+
mask_test = arr_test == nd_test
|
|
83
|
+
arr_test = arr_test.astype("float64", copy=False)
|
|
84
|
+
arr_test[mask_test] = np.nan
|
|
85
|
+
|
|
86
|
+
if not np.allclose(arr_base, arr_test, rtol=rtol, atol=atol, equal_nan=equal_nan):
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def compare_rasters(
|
|
93
|
+
base_raster: str,
|
|
94
|
+
test_raster: str,
|
|
95
|
+
*,
|
|
96
|
+
ignore_height: bool = False,
|
|
97
|
+
ignore_width: bool = False,
|
|
98
|
+
ignore_bands: bool = False,
|
|
99
|
+
ignore_dtype: bool = False,
|
|
100
|
+
ignore_nodata: bool = False,
|
|
101
|
+
ignore_bbox: bool = False,
|
|
102
|
+
ignore_crs: bool = False,
|
|
103
|
+
ignore_transform: bool = False,
|
|
104
|
+
ignore_metadata: bool = False,
|
|
105
|
+
ignore_stats: bool = False,
|
|
106
|
+
ignore_pixel_values: bool = False,
|
|
107
|
+
) -> bool:
|
|
108
|
+
is_equal = True
|
|
109
|
+
if calc_hash(base_raster) == calc_hash(test_raster):
|
|
110
|
+
return is_equal
|
|
111
|
+
|
|
112
|
+
base_props = read_raster_props(base_raster)
|
|
113
|
+
test_props = read_raster_props(test_raster)
|
|
114
|
+
|
|
115
|
+
if not ignore_height:
|
|
116
|
+
if base_props.height != test_props.height:
|
|
117
|
+
print(f"< height: {base_props.height}")
|
|
118
|
+
print("---")
|
|
119
|
+
print(f"> height: {test_props.height}")
|
|
120
|
+
print("")
|
|
121
|
+
is_equal = False
|
|
122
|
+
|
|
123
|
+
if not ignore_width:
|
|
124
|
+
if base_props.width != test_props.width:
|
|
125
|
+
print(f"< width: {base_props.width}")
|
|
126
|
+
print("---")
|
|
127
|
+
print(f"> width: {test_props.width}")
|
|
128
|
+
print("")
|
|
129
|
+
is_equal = False
|
|
130
|
+
|
|
131
|
+
if not ignore_bands:
|
|
132
|
+
if base_props.bands != test_props.bands:
|
|
133
|
+
print(f"< bands: {base_props.bands}")
|
|
134
|
+
print("---")
|
|
135
|
+
print(f"> bands: {test_props.bands}")
|
|
136
|
+
print("")
|
|
137
|
+
is_equal = False
|
|
138
|
+
|
|
139
|
+
if not ignore_dtype:
|
|
140
|
+
if base_props.dtype != test_props.dtype:
|
|
141
|
+
print(f"< dtype: {base_props.dtype}")
|
|
142
|
+
print("---")
|
|
143
|
+
print(f"> dtype: {test_props.dtype}")
|
|
144
|
+
print("")
|
|
145
|
+
is_equal = False
|
|
146
|
+
|
|
147
|
+
if not ignore_nodata:
|
|
148
|
+
if base_props.nodata != test_props.nodata:
|
|
149
|
+
print(f"< nodata: {base_props.nodata}")
|
|
150
|
+
print("---")
|
|
151
|
+
print(f"> nodata: {test_props.nodata}")
|
|
152
|
+
print("")
|
|
153
|
+
is_equal = False
|
|
154
|
+
|
|
155
|
+
if not ignore_bbox:
|
|
156
|
+
if base_props.bbox != test_props.bbox:
|
|
157
|
+
print(f"< bbox: {base_props.bbox}")
|
|
158
|
+
print("---")
|
|
159
|
+
print(f"> bbox: {test_props.bbox}")
|
|
160
|
+
print("")
|
|
161
|
+
is_equal = False
|
|
162
|
+
|
|
163
|
+
if not ignore_crs:
|
|
164
|
+
if base_props.crs != test_props.crs:
|
|
165
|
+
print(f"< crs: {base_props.crs}")
|
|
166
|
+
print("---")
|
|
167
|
+
print(f"> crs: {test_props.crs}")
|
|
168
|
+
print("")
|
|
169
|
+
is_equal = False
|
|
170
|
+
|
|
171
|
+
if not ignore_transform:
|
|
172
|
+
if base_props.transform != test_props.transform:
|
|
173
|
+
print(f"< geotransform: {base_props.transform}")
|
|
174
|
+
print("---")
|
|
175
|
+
print(f"> geotransform: {test_props.transform}")
|
|
176
|
+
print("")
|
|
177
|
+
is_equal = False
|
|
178
|
+
|
|
179
|
+
if not ignore_metadata:
|
|
180
|
+
# TODO: выводить конкретно в чем разница и для какого канала
|
|
181
|
+
if base_props.metadata != test_props.metadata:
|
|
182
|
+
print(f"< metadata: {base_props.metadata}")
|
|
183
|
+
print("---")
|
|
184
|
+
print(f"> metadata: {test_props.metadata}")
|
|
185
|
+
print("")
|
|
186
|
+
is_equal = False
|
|
187
|
+
|
|
188
|
+
if base_props.bands_metadata != test_props.bands_metadata:
|
|
189
|
+
print(f"< bands metadata: {base_props.bands_metadata}")
|
|
190
|
+
print("---")
|
|
191
|
+
print(f"> bands metadata: {test_props.bands_metadata}")
|
|
192
|
+
print("")
|
|
193
|
+
is_equal = False
|
|
194
|
+
|
|
195
|
+
if not ignore_stats:
|
|
196
|
+
if base_props.stats != test_props.stats:
|
|
197
|
+
# TODO: выводить детально в каких каналах и в чем различия
|
|
198
|
+
print(f"< statistics: {base_props.stats}")
|
|
199
|
+
print("---")
|
|
200
|
+
print(f"> statistics: {test_props.stats}")
|
|
201
|
+
print("")
|
|
202
|
+
is_equal = False
|
|
203
|
+
|
|
204
|
+
if not ignore_pixel_values:
|
|
205
|
+
if calc_diff(base_raster, test_raster) is False:
|
|
206
|
+
print("< pixel values: ...") # TODO: добавить подробный вывод в чем отличия у пикселей
|
|
207
|
+
print("---")
|
|
208
|
+
print("> pixel values: ...")
|
|
209
|
+
print("")
|
|
210
|
+
is_equal = False
|
|
211
|
+
|
|
212
|
+
return is_equal
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
if __name__ == "__main__":
|
|
216
|
+
# rs = calc_hash("temp/2025110312/gfs.2025110312.003.cape_180-0.tif")
|
|
217
|
+
|
|
218
|
+
# rs = read_raster_props("temp/icon/OLD.icon.2026010200.003.t_2m.tif")
|
|
219
|
+
|
|
220
|
+
# rs = calc_diff(
|
|
221
|
+
# "temp/icon/OLD.icon.2026010200.003.t_2m.tif",
|
|
222
|
+
# # "temp/icon/NEW.icon.2026010200.003.t_2m.tif",
|
|
223
|
+
# "temp/icon/OLD.icon.2026010200.003.td_2m.tif",
|
|
224
|
+
# )
|
|
225
|
+
|
|
226
|
+
rs = compare_rasters(
|
|
227
|
+
"temp/icon/OLD.icon.2026010200.003.t_2m.tif",
|
|
228
|
+
"temp/icon/NEW.icon.2026010200.003.t_2m.tif",
|
|
229
|
+
# "temp/icon/OLD.icon.2026010200.003.td_2m.tif",
|
|
230
|
+
ignore_height=False,
|
|
231
|
+
ignore_width=False,
|
|
232
|
+
ignore_bands=False,
|
|
233
|
+
ignore_dtype=False,
|
|
234
|
+
ignore_nodata=False,
|
|
235
|
+
ignore_bbox=False,
|
|
236
|
+
ignore_crs=False,
|
|
237
|
+
ignore_transform=False,
|
|
238
|
+
ignore_metadata=True,
|
|
239
|
+
ignore_stats=False,
|
|
240
|
+
ignore_pixel_values=False,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
print(rs)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import click
|
|
2
2
|
|
|
3
|
-
from rio_diff import __version__ as plugin_version
|
|
3
|
+
from rio_diff import __version__ as plugin_version
|
|
4
4
|
from rio_diff.compare import compare_rasters
|
|
5
5
|
|
|
6
6
|
|
|
@@ -39,7 +39,7 @@ from rio_diff.compare import compare_rasters
|
|
|
39
39
|
"--ignore-nodata",
|
|
40
40
|
default=False,
|
|
41
41
|
is_flag=True,
|
|
42
|
-
help="
|
|
42
|
+
help="No data values will be ignored.",
|
|
43
43
|
show_default=True,
|
|
44
44
|
)
|
|
45
45
|
@click.option(
|
|
@@ -78,28 +78,12 @@ from rio_diff.compare import compare_rasters
|
|
|
78
78
|
show_default=True,
|
|
79
79
|
)
|
|
80
80
|
@click.option(
|
|
81
|
-
"--ignore-
|
|
82
|
-
"ignore_pixel_values",
|
|
81
|
+
"--ignore-pixel-values",
|
|
83
82
|
default=False,
|
|
84
83
|
is_flag=True,
|
|
85
84
|
help="Pixel values will be ignored.",
|
|
86
85
|
show_default=True,
|
|
87
86
|
)
|
|
88
|
-
@click.option(
|
|
89
|
-
"--checksum",
|
|
90
|
-
"check_checksum",
|
|
91
|
-
default=False,
|
|
92
|
-
is_flag=True,
|
|
93
|
-
help="Also compare the whole-file checksum (strict byte-level equality).",
|
|
94
|
-
show_default=True,
|
|
95
|
-
)
|
|
96
|
-
@click.option(
|
|
97
|
-
"--save-diff",
|
|
98
|
-
"save_diff",
|
|
99
|
-
type=click.Path(),
|
|
100
|
-
default=None,
|
|
101
|
-
help="Save the per-pixel difference raster (base - test) to the given path.",
|
|
102
|
-
)
|
|
103
87
|
@click.version_option(version=plugin_version, message="%(version)s")
|
|
104
88
|
@click.pass_context
|
|
105
89
|
def diff(
|
|
@@ -117,46 +101,21 @@ def diff(
|
|
|
117
101
|
ignore_metadata,
|
|
118
102
|
ignore_stats,
|
|
119
103
|
ignore_pixel_values,
|
|
120
|
-
check_checksum,
|
|
121
|
-
save_diff,
|
|
122
104
|
):
|
|
123
105
|
"""Rasterio diff plugin.
|
|
124
106
|
"""
|
|
125
|
-
|
|
126
|
-
base_raster,
|
|
127
|
-
test_raster,
|
|
128
|
-
|
|
129
|
-
|
|
107
|
+
compare_rasters(
|
|
108
|
+
base_raster=base_raster,
|
|
109
|
+
test_raster=test_raster,
|
|
110
|
+
ignore_height=ignore_height,
|
|
111
|
+
ignore_width=ignore_width,
|
|
112
|
+
ignore_bands=ignore_bands,
|
|
113
|
+
ignore_dtype=ignore_dtype,
|
|
114
|
+
ignore_nodata=ignore_nodata,
|
|
115
|
+
ignore_bbox=ignore_bbox,
|
|
116
|
+
ignore_crs=ignore_crs,
|
|
117
|
+
ignore_transform=ignore_transform,
|
|
118
|
+
ignore_metadata=ignore_metadata,
|
|
130
119
|
ignore_stats=ignore_stats,
|
|
120
|
+
ignore_pixel_values=ignore_pixel_values,
|
|
131
121
|
)
|
|
132
|
-
|
|
133
|
-
if report is None:
|
|
134
|
-
ctx.exit(0)
|
|
135
|
-
|
|
136
|
-
checks: list[tuple[str, bool, object, object]] = []
|
|
137
|
-
|
|
138
|
-
def add(ignore: bool, diff, label: str) -> None:
|
|
139
|
-
if not ignore:
|
|
140
|
-
checks.append((label, diff.equal, diff.base, diff.test))
|
|
141
|
-
|
|
142
|
-
add(not check_checksum, report.checksum, "Checksum")
|
|
143
|
-
add(ignore_width, report.width, "Width")
|
|
144
|
-
add(ignore_height, report.height, "Height")
|
|
145
|
-
add(ignore_bands, report.bands, "Bands")
|
|
146
|
-
add(ignore_dtype, report.dtype, "Data type")
|
|
147
|
-
add(ignore_nodata, report.nodata, "NoData")
|
|
148
|
-
add(ignore_bbox, report.bbox, "BBox")
|
|
149
|
-
add(ignore_crs, report.crs, "CRS")
|
|
150
|
-
add(ignore_transform, report.transform, "Transform")
|
|
151
|
-
add(ignore_metadata, report.metadata, "Metadata")
|
|
152
|
-
# TODO: выводить конкретно в чем разница и для какого канала
|
|
153
|
-
add(ignore_metadata, report.bands_metadata, "Bands metadata")
|
|
154
|
-
# TODO: выводить детально в каких каналах и в чем различия
|
|
155
|
-
add(ignore_stats, report.stats, "Statistics")
|
|
156
|
-
|
|
157
|
-
has_diff = render.print_report(
|
|
158
|
-
checks,
|
|
159
|
-
report.pixel_values,
|
|
160
|
-
show_pixel_values=not ignore_pixel_values,
|
|
161
|
-
)
|
|
162
|
-
ctx.exit(1 if has_diff else 0)
|
rio_diff-1.0.0a0/PKG-INFO
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: rio-diff
|
|
3
|
-
Version: 1.0.0a0
|
|
4
|
-
Summary: Raster comparison plugin for rasterio
|
|
5
|
-
Project-URL: Homepage, https://github.com/perminovsi/rio-diff
|
|
6
|
-
Author-email: Sergey Perminov <perminovsi@ya.ru>
|
|
7
|
-
License-Expression: MIT
|
|
8
|
-
License-File: LICENSE
|
|
9
|
-
Keywords: raster,rasterio,rio-plugin
|
|
10
|
-
Classifier: Intended Audience :: Information Technology
|
|
11
|
-
Classifier: Intended Audience :: Science/Research
|
|
12
|
-
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
14
|
-
Requires-Python: >=3.11
|
|
15
|
-
Requires-Dist: click>=7.0
|
|
16
|
-
Requires-Dist: numpy>=2.0
|
|
17
|
-
Requires-Dist: rasterio>=1.3
|
|
18
|
-
Description-Content-Type: text/markdown
|
|
19
|
-
|
|
20
|
-
# rio-diff
|
|
21
|
-
|
|
22
|
-
A rasterio plugin for comparing two raster files and showing differences between them.
|
|
23
|
-
|
|
24
|
-
## Overview
|
|
25
|
-
|
|
26
|
-
`rio-diff` is a command-line tool that extends the functionality of [Rasterio](https://github.com/rasterio/rasterio) by providing a way to compare two raster files and highlight their differences. Inspired by the classic Unix `diff` command, this plugin enables users to identify differences in raster properties and pixel values.
|
|
27
|
-
|
|
28
|
-
## Features
|
|
29
|
-
|
|
30
|
-
- Compare various raster properties including dimensions, data types, coordinate reference systems, and metadata
|
|
31
|
-
- Calculate pixel-by-pixel differences between compatible rasters
|
|
32
|
-
- Optionally save the per-pixel difference raster (`base - test`) to disk
|
|
33
|
-
- Show statistics on differences including count, percentage, maximum difference, and RMSE
|
|
34
|
-
- Support for ignoring specific properties during comparison
|
|
35
|
-
- Integration with Rasterio's command-line interface
|
|
36
|
-
|
|
37
|
-
## Installation
|
|
38
|
-
|
|
39
|
-
Install the package using pip:
|
|
40
|
-
|
|
41
|
-
```bash
|
|
42
|
-
pip install rio-diff
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
After installation, the plugin will be available as a subcommand of `rio`.
|
|
46
|
-
|
|
47
|
-
## Usage
|
|
48
|
-
|
|
49
|
-
Basic usage to compare two raster files:
|
|
50
|
-
|
|
51
|
-
```bash
|
|
52
|
-
rio diff base_raster.tif test_raster.tif
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
### Options
|
|
56
|
-
|
|
57
|
-
- `--ignore-height`: Ignore the height property during comparison
|
|
58
|
-
- `--ignore-width`: Ignore the width property during comparison
|
|
59
|
-
- `--ignore-bands`: Ignore the number of bands during comparison
|
|
60
|
-
- `--ignore-dtype`: Ignore data type during comparison
|
|
61
|
-
- `--ignore-nodata`: Ignore NoData values during comparison
|
|
62
|
-
- `--ignore-bbox`: Ignore bounding box during comparison
|
|
63
|
-
- `--ignore-crs`: Ignore coordinate reference system during comparison
|
|
64
|
-
- `--ignore-transform`: Ignore affine transform during comparison
|
|
65
|
-
- `--ignore-metadata`: Ignore metadata during comparison
|
|
66
|
-
- `--ignore-stats`: Ignore statistics during comparison
|
|
67
|
-
- `--ignore-pixels`: Ignore pixel values during comparison
|
|
68
|
-
- `--checksum`: Also compare the whole-file checksum (strict byte-level equality; optional, off by default)
|
|
69
|
-
- `--save-diff PATH`: Save the per-pixel difference raster (`base - test`) to the given path. When the rasters are byte-identical, the tool exits early and no diff raster is written.
|
|
70
|
-
- `--version`: Show version information
|
|
71
|
-
|
|
72
|
-
### Examples
|
|
73
|
-
|
|
74
|
-
Compare two raster files with default settings:
|
|
75
|
-
|
|
76
|
-
```bash
|
|
77
|
-
rio diff raster1.tif raster2.tif
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
Ignore metadata differences when comparing:
|
|
81
|
-
|
|
82
|
-
```bash
|
|
83
|
-
rio diff raster1.tif raster2.tif --ignore-metadata
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Compare rasters but ignore differences in pixel values:
|
|
87
|
-
|
|
88
|
-
```bash
|
|
89
|
-
rio diff raster1.tif raster2.tif --ignore-pixels
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
Require strict byte-level equality via the whole-file checksum:
|
|
93
|
-
|
|
94
|
-
```bash
|
|
95
|
-
rio diff raster1.tif raster2.tif --checksum
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
Save the per-pixel difference raster to disk:
|
|
99
|
-
|
|
100
|
-
```bash
|
|
101
|
-
rio diff raster1.tif raster2.tif --save-diff diff.tif
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
If the rasters are byte-identical, the tool exits early and the diff raster is not written.
|
|
105
|
-
|
|
106
|
-
## Comparison Details
|
|
107
|
-
|
|
108
|
-
The tool compares the following raster properties:
|
|
109
|
-
|
|
110
|
-
- **Checksum**: MD5 hash of the file content (only when `--checksum` is passed).
|
|
111
|
-
- **Dimensions**: Width and height in pixels
|
|
112
|
-
- **Bands**: Number of channels/layers
|
|
113
|
-
- **Data Type**: Bit depth and signed/unsigned nature
|
|
114
|
-
- **NoData Value**: Value representing missing or invalid data
|
|
115
|
-
- **Bounding Box**: Spatial extent in coordinate units
|
|
116
|
-
- **CRS**: Coordinate Reference System
|
|
117
|
-
- **Transform**: Affine transformation matrix
|
|
118
|
-
- **Metadata**: Tags and attributes associated with the raster
|
|
119
|
-
- **Statistics**: Basic statistical information about pixel values
|
|
120
|
-
- **Pixel Values**: Actual pixel-by-pixel comparison (when rasters are compatible)
|
|
121
|
-
|
|
122
|
-
For compatible rasters, the tool calculates detailed statistics about pixel differences including:
|
|
123
|
-
- Count of different pixels
|
|
124
|
-
- Percentage of different pixels
|
|
125
|
-
- Maximum difference value
|
|
126
|
-
- Root Mean Square Error (RMSE)
|
|
127
|
-
|
|
128
|
-
## Exit Codes
|
|
129
|
-
|
|
130
|
-
The command sets its exit code so it can be used in scripts and CI:
|
|
131
|
-
|
|
132
|
-
- `0`: No differences were found across the compared properties (also returned early when the files are byte-identical).
|
|
133
|
-
- `1`: At least one difference was found.
|
|
134
|
-
- `2`: Usage error (invalid arguments or missing input files).
|
|
135
|
-
|
|
136
|
-
Ignored properties (`--ignore-*`) do not affect the exit code. The whole-file checksum only affects it when `--checksum` is passed.
|
|
137
|
-
|
|
138
|
-
## Inspiration
|
|
139
|
-
|
|
140
|
-
- [Linux diff command](https://man7.org/linux/man-pages/man1/diff.1.html) - Classic Unix utility for comparing files
|
|
141
|
-
- [ArcGIS Raster Compare](https://pro.arcgis.com/en/pro-app/latest/tool-reference/data-management/raster-compare.htm) - Tool for comparing raster datasets
|
|
142
|
-
|
|
143
|
-
## License
|
|
144
|
-
|
|
145
|
-
This project is licensed under the MIT License - see the LICENSE file for details.
|
rio_diff-1.0.0a0/README.md
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
# rio-diff
|
|
2
|
-
|
|
3
|
-
A rasterio plugin for comparing two raster files and showing differences between them.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
`rio-diff` is a command-line tool that extends the functionality of [Rasterio](https://github.com/rasterio/rasterio) by providing a way to compare two raster files and highlight their differences. Inspired by the classic Unix `diff` command, this plugin enables users to identify differences in raster properties and pixel values.
|
|
8
|
-
|
|
9
|
-
## Features
|
|
10
|
-
|
|
11
|
-
- Compare various raster properties including dimensions, data types, coordinate reference systems, and metadata
|
|
12
|
-
- Calculate pixel-by-pixel differences between compatible rasters
|
|
13
|
-
- Optionally save the per-pixel difference raster (`base - test`) to disk
|
|
14
|
-
- Show statistics on differences including count, percentage, maximum difference, and RMSE
|
|
15
|
-
- Support for ignoring specific properties during comparison
|
|
16
|
-
- Integration with Rasterio's command-line interface
|
|
17
|
-
|
|
18
|
-
## Installation
|
|
19
|
-
|
|
20
|
-
Install the package using pip:
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
pip install rio-diff
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
After installation, the plugin will be available as a subcommand of `rio`.
|
|
27
|
-
|
|
28
|
-
## Usage
|
|
29
|
-
|
|
30
|
-
Basic usage to compare two raster files:
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
rio diff base_raster.tif test_raster.tif
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
### Options
|
|
37
|
-
|
|
38
|
-
- `--ignore-height`: Ignore the height property during comparison
|
|
39
|
-
- `--ignore-width`: Ignore the width property during comparison
|
|
40
|
-
- `--ignore-bands`: Ignore the number of bands during comparison
|
|
41
|
-
- `--ignore-dtype`: Ignore data type during comparison
|
|
42
|
-
- `--ignore-nodata`: Ignore NoData values during comparison
|
|
43
|
-
- `--ignore-bbox`: Ignore bounding box during comparison
|
|
44
|
-
- `--ignore-crs`: Ignore coordinate reference system during comparison
|
|
45
|
-
- `--ignore-transform`: Ignore affine transform during comparison
|
|
46
|
-
- `--ignore-metadata`: Ignore metadata during comparison
|
|
47
|
-
- `--ignore-stats`: Ignore statistics during comparison
|
|
48
|
-
- `--ignore-pixels`: Ignore pixel values during comparison
|
|
49
|
-
- `--checksum`: Also compare the whole-file checksum (strict byte-level equality; optional, off by default)
|
|
50
|
-
- `--save-diff PATH`: Save the per-pixel difference raster (`base - test`) to the given path. When the rasters are byte-identical, the tool exits early and no diff raster is written.
|
|
51
|
-
- `--version`: Show version information
|
|
52
|
-
|
|
53
|
-
### Examples
|
|
54
|
-
|
|
55
|
-
Compare two raster files with default settings:
|
|
56
|
-
|
|
57
|
-
```bash
|
|
58
|
-
rio diff raster1.tif raster2.tif
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
Ignore metadata differences when comparing:
|
|
62
|
-
|
|
63
|
-
```bash
|
|
64
|
-
rio diff raster1.tif raster2.tif --ignore-metadata
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
Compare rasters but ignore differences in pixel values:
|
|
68
|
-
|
|
69
|
-
```bash
|
|
70
|
-
rio diff raster1.tif raster2.tif --ignore-pixels
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
Require strict byte-level equality via the whole-file checksum:
|
|
74
|
-
|
|
75
|
-
```bash
|
|
76
|
-
rio diff raster1.tif raster2.tif --checksum
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
Save the per-pixel difference raster to disk:
|
|
80
|
-
|
|
81
|
-
```bash
|
|
82
|
-
rio diff raster1.tif raster2.tif --save-diff diff.tif
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
If the rasters are byte-identical, the tool exits early and the diff raster is not written.
|
|
86
|
-
|
|
87
|
-
## Comparison Details
|
|
88
|
-
|
|
89
|
-
The tool compares the following raster properties:
|
|
90
|
-
|
|
91
|
-
- **Checksum**: MD5 hash of the file content (only when `--checksum` is passed).
|
|
92
|
-
- **Dimensions**: Width and height in pixels
|
|
93
|
-
- **Bands**: Number of channels/layers
|
|
94
|
-
- **Data Type**: Bit depth and signed/unsigned nature
|
|
95
|
-
- **NoData Value**: Value representing missing or invalid data
|
|
96
|
-
- **Bounding Box**: Spatial extent in coordinate units
|
|
97
|
-
- **CRS**: Coordinate Reference System
|
|
98
|
-
- **Transform**: Affine transformation matrix
|
|
99
|
-
- **Metadata**: Tags and attributes associated with the raster
|
|
100
|
-
- **Statistics**: Basic statistical information about pixel values
|
|
101
|
-
- **Pixel Values**: Actual pixel-by-pixel comparison (when rasters are compatible)
|
|
102
|
-
|
|
103
|
-
For compatible rasters, the tool calculates detailed statistics about pixel differences including:
|
|
104
|
-
- Count of different pixels
|
|
105
|
-
- Percentage of different pixels
|
|
106
|
-
- Maximum difference value
|
|
107
|
-
- Root Mean Square Error (RMSE)
|
|
108
|
-
|
|
109
|
-
## Exit Codes
|
|
110
|
-
|
|
111
|
-
The command sets its exit code so it can be used in scripts and CI:
|
|
112
|
-
|
|
113
|
-
- `0`: No differences were found across the compared properties (also returned early when the files are byte-identical).
|
|
114
|
-
- `1`: At least one difference was found.
|
|
115
|
-
- `2`: Usage error (invalid arguments or missing input files).
|
|
116
|
-
|
|
117
|
-
Ignored properties (`--ignore-*`) do not affect the exit code. The whole-file checksum only affects it when `--checksum` is passed.
|
|
118
|
-
|
|
119
|
-
## Inspiration
|
|
120
|
-
|
|
121
|
-
- [Linux diff command](https://man7.org/linux/man-pages/man1/diff.1.html) - Classic Unix utility for comparing files
|
|
122
|
-
- [ArcGIS Raster Compare](https://pro.arcgis.com/en/pro-app/latest/tool-reference/data-management/raster-compare.htm) - Tool for comparing raster datasets
|
|
123
|
-
|
|
124
|
-
## License
|
|
125
|
-
|
|
126
|
-
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
import warnings
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
import numpy as np
|
|
5
|
-
import rasterio
|
|
6
|
-
|
|
7
|
-
from rio_diff import models, utils
|
|
8
|
-
|
|
9
|
-
# Ограничение блок-кэша GDAL. По умолчанию GDAL отводит под кэш ~5% ОЗУ, из-за
|
|
10
|
-
# чего сквозной обход всех тайлов растра раздувает потребление памяти до
|
|
11
|
-
# нескольких ГБ. Каждый тайл здесь читается ровно один раз, поэтому кэш
|
|
12
|
-
# бесполезен и его можно держать маленьким.
|
|
13
|
-
GDAL_CACHEMAX_BYTES = 256 * 1024 * 1024
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def read_raster_props(inp_file: str, *, read_stats: bool = True) -> models.RasterProps:
|
|
17
|
-
with rasterio.open(inp_file) as ds:
|
|
18
|
-
return models.RasterProps(
|
|
19
|
-
width=ds.profile["width"],
|
|
20
|
-
height=ds.profile["height"],
|
|
21
|
-
bands=ds.profile["count"],
|
|
22
|
-
dtype=ds.profile["dtype"],
|
|
23
|
-
nodata=ds.profile["nodata"], # TODO: проверка для разных каналов
|
|
24
|
-
bbox=ds.bounds,
|
|
25
|
-
crs=ds.profile["crs"],
|
|
26
|
-
transform=ds.profile["transform"],
|
|
27
|
-
metadata=ds.tags(),
|
|
28
|
-
bands_metadata=[ds.tags(bidx=bidx) for bidx in range(1, ds.count + 1)],
|
|
29
|
-
stats=ds.stats() if read_stats else [], # TODO: безопаснее будет считать самому по numpy
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def is_compatible_rasters(base_raster: str, test_raster: str) -> bool:
|
|
34
|
-
"""Проверить, что растры можно сравнить попиксельно.
|
|
35
|
-
|
|
36
|
-
Для попиксельного diff-а массивы должны совпадать по форме, поэтому
|
|
37
|
-
проверяется только то, без чего вычитание невозможно:
|
|
38
|
-
- количество ячеек растра (столбцов и строк);
|
|
39
|
-
- количество каналов.
|
|
40
|
-
|
|
41
|
-
Различия в геопривязке (transform) и системе координат (crs) не мешают
|
|
42
|
-
вычитанию массивов и репортятся отдельно, поэтому здесь не учитываются.
|
|
43
|
-
"""
|
|
44
|
-
with rasterio.open(base_raster) as base_ds, rasterio.open(test_raster) as test_ds:
|
|
45
|
-
return base_ds.shape == test_ds.shape and base_ds.count == test_ds.count
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def calc_diff(
|
|
49
|
-
base_raster: str,
|
|
50
|
-
test_raster: str,
|
|
51
|
-
*,
|
|
52
|
-
rtol=0,
|
|
53
|
-
atol=0,
|
|
54
|
-
equal_nan=True,
|
|
55
|
-
diff_raster_path: str | None = None,
|
|
56
|
-
) -> list[models.PixelDiffStats]:
|
|
57
|
-
"""Вычитать первый растр из второго для получения diff-a и его последующего анализа
|
|
58
|
-
Сколько пикселей отличается, насколько они отличаются и т.п.
|
|
59
|
-
Опционально выводить график (картинку) и возможность сохранения diff-a на диск
|
|
60
|
-
"""
|
|
61
|
-
with rasterio.Env(GDAL_CACHEMAX=GDAL_CACHEMAX_BYTES), \
|
|
62
|
-
rasterio.open(base_raster) as base_ds, \
|
|
63
|
-
rasterio.open(test_raster) as test_ds:
|
|
64
|
-
count = base_ds.count
|
|
65
|
-
total_pixels = base_ds.width * base_ds.height
|
|
66
|
-
|
|
67
|
-
nd_base = base_ds.nodatavals
|
|
68
|
-
nd_test = test_ds.nodatavals
|
|
69
|
-
|
|
70
|
-
diff_count = np.zeros(count, dtype=np.int64)
|
|
71
|
-
valid_count = np.zeros(count, dtype=np.int64)
|
|
72
|
-
max_diff = np.zeros(count, dtype=np.float64)
|
|
73
|
-
sum_squared_diff = np.zeros(count, dtype=np.float64)
|
|
74
|
-
|
|
75
|
-
diff_ds = None
|
|
76
|
-
if diff_raster_path is not None:
|
|
77
|
-
diff_profile = base_ds.profile
|
|
78
|
-
diff_profile.update({
|
|
79
|
-
"dtype": "float32",
|
|
80
|
-
"nodata": float("nan"),
|
|
81
|
-
"compress": "deflate",
|
|
82
|
-
"predictor": 3,
|
|
83
|
-
"zlevel": 6,
|
|
84
|
-
})
|
|
85
|
-
Path(diff_raster_path).parent.mkdir(parents=True, exist_ok=True)
|
|
86
|
-
diff_ds = rasterio.open(diff_raster_path, "w", **diff_profile)
|
|
87
|
-
|
|
88
|
-
try:
|
|
89
|
-
# Обрабатываем растр окно за окном (по всем каналам сразу), чтобы не
|
|
90
|
-
# держать весь diff в памяти и читать каждый блок только один раз.
|
|
91
|
-
for _, window in base_ds.block_windows(1):
|
|
92
|
-
arr_base = base_ds.read(window=window).astype("float32")
|
|
93
|
-
arr_test = test_ds.read(window=window).astype("float32")
|
|
94
|
-
|
|
95
|
-
for b in range(count):
|
|
96
|
-
if nd_base[b] is not None:
|
|
97
|
-
arr_base[b][arr_base[b] == nd_base[b]] = np.nan
|
|
98
|
-
if nd_test[b] is not None:
|
|
99
|
-
arr_test[b][arr_test[b] == nd_test[b]] = np.nan
|
|
100
|
-
|
|
101
|
-
arr_diff = arr_base - arr_test
|
|
102
|
-
finite_mask = np.isfinite(arr_diff)
|
|
103
|
-
valid_count += np.count_nonzero(finite_mask, axis=(1, 2))
|
|
104
|
-
|
|
105
|
-
if diff_ds is not None:
|
|
106
|
-
diff_ds.write(arr_diff, window=window)
|
|
107
|
-
|
|
108
|
-
close_mask = np.isclose(arr_base, arr_test, rtol=rtol, atol=atol, equal_nan=equal_nan)
|
|
109
|
-
diff_count += np.count_nonzero(~close_mask, axis=(1, 2))
|
|
110
|
-
|
|
111
|
-
abs_diff = np.abs(arr_diff)
|
|
112
|
-
with warnings.catch_warnings():
|
|
113
|
-
warnings.simplefilter("ignore", RuntimeWarning) # окно целиком из NaN
|
|
114
|
-
band_max = np.nanmax(abs_diff, axis=(1, 2))
|
|
115
|
-
band_max = np.nan_to_num(band_max, nan=0.0)
|
|
116
|
-
max_diff = np.maximum(max_diff, band_max)
|
|
117
|
-
|
|
118
|
-
sum_squared_diff += np.nansum(arr_diff ** 2, axis=(1, 2))
|
|
119
|
-
finally:
|
|
120
|
-
if diff_ds is not None:
|
|
121
|
-
diff_ds.close()
|
|
122
|
-
|
|
123
|
-
return [
|
|
124
|
-
models.PixelDiffStats(
|
|
125
|
-
diff_count=int(diff_count[b]),
|
|
126
|
-
total_count=total_pixels,
|
|
127
|
-
diff_percent=float((diff_count[b] / total_pixels) * 100),
|
|
128
|
-
max_diff=float(max_diff[b]),
|
|
129
|
-
rmse=float(np.sqrt(sum_squared_diff[b] / valid_count[b])) if valid_count[b] else 0.0,
|
|
130
|
-
)
|
|
131
|
-
for b in range(count)
|
|
132
|
-
]
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def compare_rasters(
|
|
136
|
-
base_raster: str,
|
|
137
|
-
test_raster: str,
|
|
138
|
-
*,
|
|
139
|
-
diff_raster_path: str | None = None,
|
|
140
|
-
ignore_pixel_values: bool = False,
|
|
141
|
-
ignore_stats: bool = False,
|
|
142
|
-
) -> models.RasterDiff | None:
|
|
143
|
-
base_md5 = utils.calc_hash(base_raster)
|
|
144
|
-
test_md5 = utils.calc_hash(test_raster)
|
|
145
|
-
|
|
146
|
-
if base_md5 == test_md5:
|
|
147
|
-
return None
|
|
148
|
-
|
|
149
|
-
# Отключаем GDAL PAM, чтобы чтение статистики (ds.stats()) и запись растров
|
|
150
|
-
# не создавали сайдкар-файлы <растр>.aux.xml рядом с входными данными.
|
|
151
|
-
with rasterio.Env(GDAL_PAM_ENABLED="NO"):
|
|
152
|
-
base_props = read_raster_props(base_raster, read_stats=not ignore_stats)
|
|
153
|
-
test_props = read_raster_props(test_raster, read_stats=not ignore_stats)
|
|
154
|
-
|
|
155
|
-
pixel_values = None
|
|
156
|
-
need_pixel_diff = not ignore_pixel_values or diff_raster_path is not None
|
|
157
|
-
if need_pixel_diff and is_compatible_rasters(base_raster, test_raster):
|
|
158
|
-
pixel_values = calc_diff(
|
|
159
|
-
base_raster, test_raster, diff_raster_path=diff_raster_path
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
return models.RasterDiff(
|
|
163
|
-
checksum=models.DiffStr(
|
|
164
|
-
equal=base_md5 == test_md5,
|
|
165
|
-
base=base_md5,
|
|
166
|
-
test=test_md5,
|
|
167
|
-
),
|
|
168
|
-
bands=models.DiffInt(
|
|
169
|
-
equal=base_props.bands == test_props.bands,
|
|
170
|
-
base=base_props.bands,
|
|
171
|
-
test=test_props.bands,
|
|
172
|
-
),
|
|
173
|
-
width=models.DiffInt(
|
|
174
|
-
equal=base_props.width == test_props.width,
|
|
175
|
-
base=base_props.width,
|
|
176
|
-
test=test_props.width,
|
|
177
|
-
),
|
|
178
|
-
height=models.DiffInt(
|
|
179
|
-
equal=base_props.height == test_props.height,
|
|
180
|
-
base=base_props.height,
|
|
181
|
-
test=test_props.height,
|
|
182
|
-
),
|
|
183
|
-
dtype=models.DiffStr(
|
|
184
|
-
equal=base_props.dtype == test_props.dtype,
|
|
185
|
-
base=base_props.dtype,
|
|
186
|
-
test=test_props.dtype,
|
|
187
|
-
),
|
|
188
|
-
nodata=models.DiffOptionalFloat(
|
|
189
|
-
equal=base_props.nodata == test_props.nodata,
|
|
190
|
-
base=base_props.nodata,
|
|
191
|
-
test=test_props.nodata,
|
|
192
|
-
),
|
|
193
|
-
bbox=models.DiffBbox(
|
|
194
|
-
equal=base_props.bbox == test_props.bbox,
|
|
195
|
-
base=base_props.bbox,
|
|
196
|
-
test=test_props.bbox,
|
|
197
|
-
),
|
|
198
|
-
crs=models.DiffCRS(
|
|
199
|
-
equal=base_props.crs == test_props.crs,
|
|
200
|
-
base=base_props.crs,
|
|
201
|
-
test=test_props.crs,
|
|
202
|
-
),
|
|
203
|
-
transform=models.DiffTransform(
|
|
204
|
-
equal=base_props.transform == test_props.transform,
|
|
205
|
-
base=base_props.transform,
|
|
206
|
-
test=test_props.transform,
|
|
207
|
-
),
|
|
208
|
-
metadata=models.DiffList(
|
|
209
|
-
equal=base_props.metadata == test_props.metadata,
|
|
210
|
-
base=base_props.metadata,
|
|
211
|
-
test=test_props.metadata,
|
|
212
|
-
),
|
|
213
|
-
bands_metadata=models.DiffList(
|
|
214
|
-
equal=base_props.bands_metadata == test_props.bands_metadata,
|
|
215
|
-
base=base_props.bands_metadata,
|
|
216
|
-
test=test_props.bands_metadata,
|
|
217
|
-
),
|
|
218
|
-
stats=models.DiffList(
|
|
219
|
-
equal=base_props.stats == test_props.stats,
|
|
220
|
-
base=base_props.stats,
|
|
221
|
-
test=test_props.stats,
|
|
222
|
-
),
|
|
223
|
-
pixel_values=pixel_values,
|
|
224
|
-
)
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
2
|
-
from typing import Any
|
|
3
|
-
|
|
4
|
-
from affine import Affine
|
|
5
|
-
from rasterio.coords import BoundingBox
|
|
6
|
-
from rasterio.crs import CRS
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@dataclass
|
|
10
|
-
class RasterProps:
|
|
11
|
-
width: int
|
|
12
|
-
height: int
|
|
13
|
-
bands: int
|
|
14
|
-
dtype: str
|
|
15
|
-
nodata: float | None
|
|
16
|
-
bbox: BoundingBox
|
|
17
|
-
crs: CRS
|
|
18
|
-
transform: Affine
|
|
19
|
-
metadata: list[dict[str, Any]]
|
|
20
|
-
bands_metadata: list
|
|
21
|
-
stats: list
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@dataclass
|
|
25
|
-
class DiffStr:
|
|
26
|
-
equal: bool
|
|
27
|
-
base: str
|
|
28
|
-
test: str
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@dataclass
|
|
32
|
-
class DiffInt:
|
|
33
|
-
equal: bool
|
|
34
|
-
base: int
|
|
35
|
-
test: int
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
@dataclass
|
|
39
|
-
class DiffOptionalFloat:
|
|
40
|
-
equal: bool
|
|
41
|
-
base: float | None
|
|
42
|
-
test: float | None
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
@dataclass
|
|
46
|
-
class DiffList:
|
|
47
|
-
equal: bool
|
|
48
|
-
base: list
|
|
49
|
-
test: list
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
@dataclass
|
|
53
|
-
class DiffBbox:
|
|
54
|
-
equal: bool
|
|
55
|
-
base: BoundingBox
|
|
56
|
-
test: BoundingBox
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
@dataclass
|
|
60
|
-
class DiffCRS:
|
|
61
|
-
equal: bool
|
|
62
|
-
base: CRS
|
|
63
|
-
test: CRS
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
@dataclass
|
|
67
|
-
class DiffTransform:
|
|
68
|
-
equal: bool
|
|
69
|
-
base: Affine
|
|
70
|
-
test: Affine
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
@dataclass
|
|
74
|
-
class PixelDiffStats:
|
|
75
|
-
diff_count: int
|
|
76
|
-
total_count: int
|
|
77
|
-
diff_percent: float
|
|
78
|
-
max_diff: float
|
|
79
|
-
rmse: float
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
@dataclass
|
|
83
|
-
class RasterDiff:
|
|
84
|
-
checksum: DiffStr
|
|
85
|
-
bands: DiffInt
|
|
86
|
-
width: DiffInt
|
|
87
|
-
height: DiffInt
|
|
88
|
-
dtype: DiffStr
|
|
89
|
-
nodata: DiffOptionalFloat
|
|
90
|
-
bbox: DiffBbox
|
|
91
|
-
crs: DiffCRS
|
|
92
|
-
transform: DiffTransform
|
|
93
|
-
metadata: DiffList
|
|
94
|
-
bands_metadata: DiffList
|
|
95
|
-
stats: DiffList
|
|
96
|
-
pixel_values: list[PixelDiffStats] | None
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
"""Вывод отчёта сравнения растров в консоль в стиле pytest-diff.
|
|
2
|
-
|
|
3
|
-
Показываются только различия. Каждое поле выводится единым способом — как
|
|
4
|
-
pytest показывает `assert base == test`: unified-diff по ``pprint``-представлению,
|
|
5
|
-
где строки ``-`` относятся к base (красный), ``+`` — к test (зелёный), а
|
|
6
|
-
неизменившиеся строки служат контекстом.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
import difflib
|
|
10
|
-
import pprint
|
|
11
|
-
|
|
12
|
-
import click
|
|
13
|
-
from affine import Affine
|
|
14
|
-
|
|
15
|
-
from rio_diff import models
|
|
16
|
-
|
|
17
|
-
_STAT_ATTRS = ("min", "max", "mean", "std")
|
|
18
|
-
_TRANSFORM_ATTRS = ("a", "b", "c", "d", "e", "f")
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def _has_attrs(value, attrs) -> bool:
|
|
22
|
-
return all(hasattr(value, attr) for attr in attrs)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def _prepare(value):
|
|
26
|
-
"""Разложить объекты по полям, чтобы ``pprint`` печатал их построчно.
|
|
27
|
-
|
|
28
|
-
Statistics/BBox/Transform и подобные объекты в repr-е выглядят одной
|
|
29
|
-
строкой, поэтому diff краснит их целиком. Превращаем их в dict по полям
|
|
30
|
-
(рекурсивно), тогда каждое поле оказывается на своей строке и в diff-е
|
|
31
|
-
подсвечивается только изменившееся. Ключи внешних dict сортируем, чтобы
|
|
32
|
-
base и test были выровнены.
|
|
33
|
-
"""
|
|
34
|
-
if isinstance(value, dict):
|
|
35
|
-
return {key: _prepare(value[key]) for key in sorted(value, key=str)}
|
|
36
|
-
if isinstance(value, Affine):
|
|
37
|
-
return {name: getattr(value, name) for name in _TRANSFORM_ATTRS}
|
|
38
|
-
if _has_attrs(value, _STAT_ATTRS):
|
|
39
|
-
return {name: getattr(value, name) for name in _STAT_ATTRS}
|
|
40
|
-
if hasattr(value, "_fields"): # namedtuple, напр. BoundingBox
|
|
41
|
-
return {name: _prepare(getattr(value, name)) for name in value._fields}
|
|
42
|
-
if isinstance(value, (list, tuple)):
|
|
43
|
-
return [_prepare(item) for item in value]
|
|
44
|
-
return value
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def _lines(value) -> list[str]:
|
|
48
|
-
"""Разбить значение на строки для diff-а.
|
|
49
|
-
|
|
50
|
-
Контейнеры раскладываем через ``pprint`` с узкой шириной, чтобы элементы
|
|
51
|
-
ложились по одному на строку. Скаляры берём через ``str`` — так у объектов
|
|
52
|
-
с коротким представлением (например, CRS → ``EPSG:32637``) не всплывает
|
|
53
|
-
громоздкий repr.
|
|
54
|
-
"""
|
|
55
|
-
prepared = _prepare(value)
|
|
56
|
-
if isinstance(prepared, (dict, list, tuple)):
|
|
57
|
-
text = pprint.pformat(prepared, width=1, sort_dicts=False)
|
|
58
|
-
else:
|
|
59
|
-
text = str(prepared)
|
|
60
|
-
return text.splitlines() or [text]
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def _print_value_diff(base, test) -> None:
|
|
64
|
-
for line in difflib.ndiff(_lines(base), _lines(test)):
|
|
65
|
-
tag = line[:2]
|
|
66
|
-
if tag == "? ": # подсказки ndiff с ^~+ — пропускаем как шум
|
|
67
|
-
continue
|
|
68
|
-
if tag == "- ":
|
|
69
|
-
click.secho(f" {line}", fg="red")
|
|
70
|
-
elif tag == "+ ":
|
|
71
|
-
click.secho(f" {line}", fg="green")
|
|
72
|
-
else: # " " — общий контекст
|
|
73
|
-
click.echo(f" {line}")
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def _print_mismatch(label: str, base, test) -> None:
|
|
77
|
-
click.secho(label, bold=True)
|
|
78
|
-
_print_value_diff(base, test)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def print_report(
|
|
82
|
-
checks: list[tuple[str, bool, object, object]],
|
|
83
|
-
pixel_values: list[models.PixelDiffStats] | None,
|
|
84
|
-
show_pixel_values: bool,
|
|
85
|
-
) -> bool:
|
|
86
|
-
"""Вывести различия. Возвращает True, если найдено хотя бы одно."""
|
|
87
|
-
printed = 0
|
|
88
|
-
|
|
89
|
-
def separate() -> None:
|
|
90
|
-
nonlocal printed
|
|
91
|
-
if printed:
|
|
92
|
-
click.echo()
|
|
93
|
-
printed += 1
|
|
94
|
-
|
|
95
|
-
for label, equal, base, test in checks:
|
|
96
|
-
if not equal:
|
|
97
|
-
separate()
|
|
98
|
-
_print_mismatch(label, base, test)
|
|
99
|
-
|
|
100
|
-
if show_pixel_values:
|
|
101
|
-
if pixel_values is None:
|
|
102
|
-
separate()
|
|
103
|
-
click.secho(
|
|
104
|
-
"Pixel values not compared (incompatible shape or band count)",
|
|
105
|
-
fg="yellow",
|
|
106
|
-
)
|
|
107
|
-
else:
|
|
108
|
-
diffs = [
|
|
109
|
-
(bidx, stat)
|
|
110
|
-
for bidx, stat in enumerate(pixel_values, start=1)
|
|
111
|
-
if stat.diff_count > 0
|
|
112
|
-
]
|
|
113
|
-
if diffs:
|
|
114
|
-
separate()
|
|
115
|
-
click.secho("Pixel values", bold=True)
|
|
116
|
-
rows = [
|
|
117
|
-
{
|
|
118
|
-
"band": bidx,
|
|
119
|
-
"diff_count": stat.diff_count,
|
|
120
|
-
"diff_percent": round(stat.diff_percent, 2),
|
|
121
|
-
"max_diff": stat.max_diff,
|
|
122
|
-
"rmse": stat.rmse,
|
|
123
|
-
}
|
|
124
|
-
for bidx, stat in diffs
|
|
125
|
-
]
|
|
126
|
-
for line in _lines(rows):
|
|
127
|
-
click.secho(f" {line}", fg="red")
|
|
128
|
-
|
|
129
|
-
return printed > 0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|