asp-plot 1.6.4__tar.gz → 1.7.0__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.
- {asp_plot-1.6.4 → asp_plot-1.7.0}/.gitignore +1 -0
- {asp_plot-1.6.4 → asp_plot-1.7.0}/CHANGELOG.md +18 -0
- {asp_plot-1.6.4 → asp_plot-1.7.0}/PKG-INFO +4 -3
- {asp_plot-1.6.4 → asp_plot-1.7.0}/README.md +3 -2
- asp_plot-1.7.0/asp_plot/__init__.py +6 -0
- {asp_plot-1.6.4 → asp_plot-1.7.0}/asp_plot/processing_parameters.py +32 -0
- {asp_plot-1.6.4 → asp_plot-1.7.0}/asp_plot/report.py +21 -0
- {asp_plot-1.6.4 → asp_plot-1.7.0}/asp_plot/scenes.py +14 -1
- {asp_plot-1.6.4 → asp_plot-1.7.0}/asp_plot/stereo.py +24 -9
- {asp_plot-1.6.4 → asp_plot-1.7.0}/asp_plot/utils.py +118 -47
- {asp_plot-1.6.4 → asp_plot-1.7.0}/pyproject.toml +1 -1
- asp_plot-1.6.4/asp_plot/cli/__init__.py +0 -0
- {asp_plot-1.6.4 → asp_plot-1.7.0}/.flake8 +0 -0
- {asp_plot-1.6.4 → asp_plot-1.7.0}/.github/workflows/release.yml +0 -0
- {asp_plot-1.6.4 → asp_plot-1.7.0}/.github/workflows/run-tests.yml +0 -0
- {asp_plot-1.6.4 → asp_plot-1.7.0}/.pre-commit-config.yaml +0 -0
- {asp_plot-1.6.4 → asp_plot-1.7.0}/LICENSE +0 -0
- {asp_plot-1.6.4 → asp_plot-1.7.0}/asp_plot/alignment.py +0 -0
- {asp_plot-1.6.4 → asp_plot-1.7.0}/asp_plot/altimetry.py +0 -0
- {asp_plot-1.6.4 → asp_plot-1.7.0}/asp_plot/bundle_adjust.py +0 -0
- {asp_plot-1.6.4/asp_plot → asp_plot-1.7.0/asp_plot/cli}/__init__.py +0 -0
- {asp_plot-1.6.4 → asp_plot-1.7.0}/asp_plot/cli/asp_plot.py +0 -0
- {asp_plot-1.6.4 → asp_plot-1.7.0}/asp_plot/cli/csm_camera_plot.py +0 -0
- {asp_plot-1.6.4 → asp_plot-1.7.0}/asp_plot/cli/stereo_geom.py +0 -0
- {asp_plot-1.6.4 → asp_plot-1.7.0}/asp_plot/csm_camera.py +0 -0
- {asp_plot-1.6.4 → asp_plot-1.7.0}/asp_plot/stereo_geometry.py +0 -0
- {asp_plot-1.6.4 → asp_plot-1.7.0}/asp_plot/stereopair_metadata_parser.py +0 -0
- {asp_plot-1.6.4 → asp_plot-1.7.0}/conda-forge-recipe/meta.yaml +0 -0
- {asp_plot-1.6.4 → asp_plot-1.7.0}/environment.yml +0 -0
|
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.7.0] - 2026-02-24
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- ASP version and asp_plot version displayed on report title page
|
|
12
|
+
- Copyright overlay ("© Vantor {year}") on WorldView satellite imagery in scene, match point, and detailed hillshade plots
|
|
13
|
+
- `detect_vantor_satellite()` utility to identify WorldView imagery from XML SATID tags
|
|
14
|
+
- `add_copyright_overlay()` utility for matplotlib axes
|
|
15
|
+
- `ProcessingParameters.get_asp_version()` method to extract ASP version from log files
|
|
16
|
+
- `Raster._mask_nodata()` private helper to consolidate nodata/invalid value masking
|
|
17
|
+
- `Raster._load_and_diff_rasters_da()` private static method returning xarray DataArray for raster differencing
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- `Raster.get_bounds()` now uses `self.ds.bounds` (rasterio) instead of opening a redundant rioxarray dataset
|
|
21
|
+
- `Raster.compute_difference()` uses `rio.to_raster()` for saving when `save=True`, avoiding manual profile construction
|
|
22
|
+
- `StereoPlotter.plot_detailed_hillshade()` reuses existing `raster.ds.transform` instead of reopening the DEM file
|
|
23
|
+
- Consolidated duplicated nodata masking logic into `Raster._mask_nodata()`
|
|
24
|
+
- Updated ASTER and WorldView example notebooks
|
|
25
|
+
|
|
8
26
|
## [1.6.4] - 2026-02-17
|
|
9
27
|
|
|
10
28
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: asp_plot
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.0
|
|
4
4
|
Summary: Package for plotting outputs Ames Stereo Pipeline processing
|
|
5
5
|
Project-URL: Homepage, https://github.com/uw-cryo/asp_plot
|
|
6
6
|
Project-URL: Issues, https://github.com/uw-cryo/asp_plot/issues
|
|
@@ -31,12 +31,13 @@ Description-Content-Type: text/markdown
|
|
|
31
31
|
|
|
32
32
|
# asp_plot
|
|
33
33
|
|
|
34
|
-
[](https://doi.org/10.5281/zenodo.14263121)
|
|
35
35
|
|
|
36
36
|
Scripts and notebooks to visualize output from the [NASA Ames Stereo Pipeline (ASP)](https://github.com/NeoGeographyToolkit/StereoPipeline).
|
|
37
37
|
|
|
38
38
|
> [!IMPORTANT]
|
|
39
|
-
> [View an example WorldView report here](notebooks/WorldView/asp_plot_report_atlanta_tile.pdf).
|
|
39
|
+
> [View an example WorldView report here](https://github.com/uw-cryo/asp_plot/blob/main/notebooks/WorldView/asp_plot_report_atlanta_tile.pdf).
|
|
40
|
+
> [View example modular plotting usage here](https://github.com/uw-cryo/asp_plot/tree/main/notebooks)
|
|
40
41
|
>
|
|
41
42
|
|
|
42
43
|
## Motivation
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# asp_plot
|
|
2
2
|
|
|
3
|
-
[](https://doi.org/10.5281/zenodo.14263121)
|
|
4
4
|
|
|
5
5
|
Scripts and notebooks to visualize output from the [NASA Ames Stereo Pipeline (ASP)](https://github.com/NeoGeographyToolkit/StereoPipeline).
|
|
6
6
|
|
|
7
7
|
> [!IMPORTANT]
|
|
8
|
-
> [View an example WorldView report here](notebooks/WorldView/asp_plot_report_atlanta_tile.pdf).
|
|
8
|
+
> [View an example WorldView report here](https://github.com/uw-cryo/asp_plot/blob/main/notebooks/WorldView/asp_plot_report_atlanta_tile.pdf).
|
|
9
|
+
> [View example modular plotting usage here](https://github.com/uw-cryo/asp_plot/tree/main/notebooks)
|
|
9
10
|
>
|
|
10
11
|
|
|
11
12
|
## Motivation
|
|
@@ -103,6 +103,37 @@ class ProcessingParameters:
|
|
|
103
103
|
except:
|
|
104
104
|
self.point2dem_log = None
|
|
105
105
|
|
|
106
|
+
def get_asp_version(self):
|
|
107
|
+
"""Extract the ASP version string from the first available log file.
|
|
108
|
+
|
|
109
|
+
Returns
|
|
110
|
+
-------
|
|
111
|
+
str
|
|
112
|
+
ASP version string (e.g., "3.4.0-alpha"), or "N/A" if not found.
|
|
113
|
+
"""
|
|
114
|
+
log_candidates = []
|
|
115
|
+
if self.bundle_adjust_log:
|
|
116
|
+
log_candidates.append(self.bundle_adjust_log)
|
|
117
|
+
if self.stereo_logs:
|
|
118
|
+
pprc = next(
|
|
119
|
+
(log for log in self.stereo_logs if "log-stereo_pprc" in log),
|
|
120
|
+
None,
|
|
121
|
+
)
|
|
122
|
+
if pprc:
|
|
123
|
+
log_candidates.append(pprc)
|
|
124
|
+
if self.point2dem_log:
|
|
125
|
+
log_candidates.append(self.point2dem_log)
|
|
126
|
+
|
|
127
|
+
for log_file in log_candidates:
|
|
128
|
+
try:
|
|
129
|
+
with open(log_file, "r") as f:
|
|
130
|
+
first_line = f.readline().strip()
|
|
131
|
+
if first_line.startswith("ASP "):
|
|
132
|
+
return first_line[4:]
|
|
133
|
+
except Exception:
|
|
134
|
+
continue
|
|
135
|
+
return "N/A"
|
|
136
|
+
|
|
106
137
|
def from_log_files(self):
|
|
107
138
|
"""
|
|
108
139
|
Extract processing parameters from log files.
|
|
@@ -157,6 +188,7 @@ class ProcessingParameters:
|
|
|
157
188
|
point2dem_params = "point2dem " + point2dem_params.split(maxsplit=1)[1]
|
|
158
189
|
|
|
159
190
|
self.processing_parameters_dict = {
|
|
191
|
+
"asp_version": self.get_asp_version(),
|
|
160
192
|
"processing_timestamp": processing_timestamp,
|
|
161
193
|
"reference_dem": reference_dem,
|
|
162
194
|
"bundle_adjust": bundle_adjust_params,
|
|
@@ -152,6 +152,27 @@ def compile_report(
|
|
|
152
152
|
new_x="LMARGIN",
|
|
153
153
|
new_y="NEXT",
|
|
154
154
|
)
|
|
155
|
+
asp_version = processing_parameters_dict.get("asp_version", "")
|
|
156
|
+
if asp_version:
|
|
157
|
+
pdf.cell(
|
|
158
|
+
0,
|
|
159
|
+
8,
|
|
160
|
+
f"ASP version: {asp_version}",
|
|
161
|
+
align="C",
|
|
162
|
+
new_x="LMARGIN",
|
|
163
|
+
new_y="NEXT",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
from asp_plot import __version__
|
|
167
|
+
|
|
168
|
+
pdf.cell(
|
|
169
|
+
0,
|
|
170
|
+
8,
|
|
171
|
+
f"asp_plot version: {__version__}",
|
|
172
|
+
align="C",
|
|
173
|
+
new_x="LMARGIN",
|
|
174
|
+
new_y="NEXT",
|
|
175
|
+
)
|
|
155
176
|
pdf.ln(10)
|
|
156
177
|
|
|
157
178
|
if report_metadata is not None:
|
|
@@ -3,7 +3,14 @@ import os
|
|
|
3
3
|
|
|
4
4
|
import matplotlib.pyplot as plt
|
|
5
5
|
|
|
6
|
-
from asp_plot.utils import
|
|
6
|
+
from asp_plot.utils import (
|
|
7
|
+
Plotter,
|
|
8
|
+
Raster,
|
|
9
|
+
add_copyright_overlay,
|
|
10
|
+
detect_vantor_satellite,
|
|
11
|
+
glob_file,
|
|
12
|
+
save_figure,
|
|
13
|
+
)
|
|
7
14
|
|
|
8
15
|
logging.basicConfig(level=logging.WARNING)
|
|
9
16
|
logger = logging.getLogger(__name__)
|
|
@@ -64,6 +71,8 @@ class ScenePlotter(Plotter):
|
|
|
64
71
|
self.stereo_directory = stereo_directory
|
|
65
72
|
self.full_stereo_directory = os.path.join(directory, stereo_directory)
|
|
66
73
|
|
|
74
|
+
self.is_vantor = detect_vantor_satellite(self.directory)
|
|
75
|
+
|
|
67
76
|
self.left_scene_sub_fn = glob_file(self.full_stereo_directory, "*-L_sub.tif")
|
|
68
77
|
self.right_scene_sub_fn = glob_file(self.full_stereo_directory, "*-R_sub.tif")
|
|
69
78
|
|
|
@@ -110,10 +119,14 @@ class ScenePlotter(Plotter):
|
|
|
110
119
|
left_scene_ma = left_scene.read_array()
|
|
111
120
|
self.plot_array(ax=axa[0], array=left_scene_ma, cmap="gray", add_cbar=False)
|
|
112
121
|
axa[0].set_title(f"Left\n{os.path.basename(self.left_scene_sub_fn)}", size=8)
|
|
122
|
+
if self.is_vantor:
|
|
123
|
+
add_copyright_overlay(axa[0])
|
|
113
124
|
|
|
114
125
|
right_scene_ma = Raster(self.right_scene_sub_fn).read_array()
|
|
115
126
|
self.plot_array(ax=axa[1], array=right_scene_ma, cmap="gray", add_cbar=False)
|
|
116
127
|
axa[1].set_title(f"Right\n{os.path.basename(self.right_scene_sub_fn)}", size=8)
|
|
128
|
+
if self.is_vantor:
|
|
129
|
+
add_copyright_overlay(axa[1])
|
|
117
130
|
|
|
118
131
|
fig.tight_layout()
|
|
119
132
|
if save_dir and fig_fn:
|
|
@@ -9,7 +9,15 @@ import rasterio as rio
|
|
|
9
9
|
from matplotlib_scalebar.scalebar import ScaleBar
|
|
10
10
|
|
|
11
11
|
from asp_plot.processing_parameters import ProcessingParameters
|
|
12
|
-
from asp_plot.utils import
|
|
12
|
+
from asp_plot.utils import (
|
|
13
|
+
ColorBar,
|
|
14
|
+
Plotter,
|
|
15
|
+
Raster,
|
|
16
|
+
add_copyright_overlay,
|
|
17
|
+
detect_vantor_satellite,
|
|
18
|
+
glob_file,
|
|
19
|
+
save_figure,
|
|
20
|
+
)
|
|
13
21
|
|
|
14
22
|
logging.basicConfig(level=logging.WARNING)
|
|
15
23
|
logger = logging.getLogger(__name__)
|
|
@@ -90,6 +98,7 @@ class StereoPlotter(Plotter):
|
|
|
90
98
|
super().__init__(**kwargs)
|
|
91
99
|
self.directory = directory
|
|
92
100
|
self.stereo_directory = stereo_directory
|
|
101
|
+
self.is_vantor = detect_vantor_satellite(self.directory)
|
|
93
102
|
|
|
94
103
|
if reference_dem:
|
|
95
104
|
self.reference_dem = reference_dem
|
|
@@ -331,6 +340,10 @@ class StereoPlotter(Plotter):
|
|
|
331
340
|
s=1,
|
|
332
341
|
)
|
|
333
342
|
axa[1].set_aspect("equal")
|
|
343
|
+
|
|
344
|
+
if self.is_vantor:
|
|
345
|
+
add_copyright_overlay(axa[0])
|
|
346
|
+
add_copyright_overlay(axa[1])
|
|
334
347
|
else:
|
|
335
348
|
axa[0].text(
|
|
336
349
|
0.5,
|
|
@@ -647,14 +660,14 @@ class StereoPlotter(Plotter):
|
|
|
647
660
|
]
|
|
648
661
|
self._plot_hillshade_with_overlay(ax_hs, dem_subset, hs_subset, gsd)
|
|
649
662
|
|
|
650
|
-
|
|
651
|
-
transform
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
663
|
+
ul_x, ul_y = rio.transform.xy(
|
|
664
|
+
raster.ds.transform, idx[0] * subset_size, idx[1] * subset_size
|
|
665
|
+
)
|
|
666
|
+
lr_x, lr_y = rio.transform.xy(
|
|
667
|
+
raster.ds.transform,
|
|
668
|
+
(idx[0] + 1) * subset_size,
|
|
669
|
+
(idx[1] + 1) * subset_size,
|
|
670
|
+
)
|
|
658
671
|
|
|
659
672
|
# We only show the corresponding image if it is mapprojected
|
|
660
673
|
if self.orthos:
|
|
@@ -671,6 +684,8 @@ class StereoPlotter(Plotter):
|
|
|
671
684
|
cmap="gray",
|
|
672
685
|
add_cbar=False,
|
|
673
686
|
)
|
|
687
|
+
if self.is_vantor:
|
|
688
|
+
add_copyright_overlay(ax_img)
|
|
674
689
|
axes_to_modify = [ax_hs, ax_img]
|
|
675
690
|
else:
|
|
676
691
|
plt.delaxes(ax_img)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import glob
|
|
2
2
|
import logging
|
|
3
3
|
import os
|
|
4
|
+
import re
|
|
4
5
|
import subprocess
|
|
5
6
|
import warnings
|
|
6
7
|
|
|
@@ -529,6 +530,27 @@ class Raster:
|
|
|
529
530
|
|
|
530
531
|
return (height, width)
|
|
531
532
|
|
|
533
|
+
def _mask_nodata(self, array):
|
|
534
|
+
"""
|
|
535
|
+
Apply nodata and invalid value masking to a raster array.
|
|
536
|
+
|
|
537
|
+
Handles two cases that rasterio's masked=True alone does not:
|
|
538
|
+
1. Undeclared nodata values (inferred by get_ndv() from pixel data)
|
|
539
|
+
2. Invalid values (inf, nan) that may exist in ASP outputs
|
|
540
|
+
|
|
541
|
+
Parameters
|
|
542
|
+
----------
|
|
543
|
+
array : numpy.ndarray
|
|
544
|
+
Raw raster array (may already be partially masked from rasterio)
|
|
545
|
+
|
|
546
|
+
Returns
|
|
547
|
+
-------
|
|
548
|
+
numpy.ma.MaskedArray
|
|
549
|
+
Array with nodata and invalid values masked
|
|
550
|
+
"""
|
|
551
|
+
ndv = self.get_ndv()
|
|
552
|
+
return np.ma.fix_invalid(np.ma.masked_equal(array, ndv))
|
|
553
|
+
|
|
532
554
|
def read_array(self, b=1, extent=False):
|
|
533
555
|
"""
|
|
534
556
|
Read raster data as a numpy masked array.
|
|
@@ -559,8 +581,7 @@ class Raster:
|
|
|
559
581
|
else:
|
|
560
582
|
a = self.ds.read(b, masked=True)
|
|
561
583
|
|
|
562
|
-
|
|
563
|
-
ma = np.ma.fix_invalid(np.ma.masked_equal(a, ndv))
|
|
584
|
+
ma = self._mask_nodata(a)
|
|
564
585
|
out = ma
|
|
565
586
|
if extent:
|
|
566
587
|
extent = rio.plot.plotting_extent(self.ds)
|
|
@@ -590,9 +611,7 @@ class Raster:
|
|
|
590
611
|
"""
|
|
591
612
|
window = from_bounds(*bbox, self.ds.transform)
|
|
592
613
|
subset = self.ds.read(b, window=window, masked=True)
|
|
593
|
-
|
|
594
|
-
ma = np.ma.fix_invalid(np.ma.masked_equal(subset, ndv))
|
|
595
|
-
return ma
|
|
614
|
+
return self._mask_nodata(subset)
|
|
596
615
|
|
|
597
616
|
def get_ndv(self):
|
|
598
617
|
"""
|
|
@@ -672,8 +691,7 @@ class Raster:
|
|
|
672
691
|
If json_format=True: list of corner coordinates as dictionaries
|
|
673
692
|
If json_format=False: tuple of (min_x, min_y, max_x, max_y)
|
|
674
693
|
"""
|
|
675
|
-
|
|
676
|
-
bounds = ds.rio.bounds()
|
|
694
|
+
bounds = self.ds.bounds
|
|
677
695
|
if latlon:
|
|
678
696
|
epsg = self.get_epsg_code()
|
|
679
697
|
bounds = rio.warp.transform_bounds(f"EPSG:{epsg}", "EPSG:4326", *bounds)
|
|
@@ -736,13 +754,9 @@ class Raster:
|
|
|
736
754
|
Aligns rasters to the grid of the second raster before differencing.
|
|
737
755
|
If save=True, saves the difference raster with "_diff.tif" suffix.
|
|
738
756
|
"""
|
|
739
|
-
# Load and align rasters, with second raster as reference (cropped to intersection)
|
|
740
|
-
diff_data, dst_transform, dst_crs, nodata = self.load_and_diff_rasters(
|
|
741
|
-
self.fn, second_fn
|
|
742
|
-
)
|
|
743
|
-
|
|
744
|
-
# Optionally save difference raster
|
|
745
757
|
if save:
|
|
758
|
+
diff_da, nodata = self._load_and_diff_rasters_da(self.fn, second_fn)
|
|
759
|
+
|
|
746
760
|
outdir = os.path.dirname(os.path.abspath(self.fn))
|
|
747
761
|
outprefix = (
|
|
748
762
|
os.path.splitext(os.path.split(self.fn)[1])[0]
|
|
@@ -751,24 +765,13 @@ class Raster:
|
|
|
751
765
|
)
|
|
752
766
|
dst_fn = os.path.join(outdir, outprefix + "_diff.tif")
|
|
753
767
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
"count": 1,
|
|
762
|
-
"crs": dst_crs,
|
|
763
|
-
"transform": dst_transform,
|
|
764
|
-
"nodata": nodata,
|
|
765
|
-
"compress": "lzw",
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
with rio.open(dst_fn, "w", **profile) as dst:
|
|
769
|
-
dst.write(diff_data.filled(nodata).astype(rio.float32), 1)
|
|
770
|
-
|
|
771
|
-
return diff_data
|
|
768
|
+
diff_da.rio.write_nodata(nodata, inplace=True)
|
|
769
|
+
diff_da.rio.to_raster(dst_fn, dtype="float32", compress="lzw")
|
|
770
|
+
|
|
771
|
+
return np.ma.masked_invalid(diff_da.values)
|
|
772
|
+
else:
|
|
773
|
+
diff_data, _, _, _ = self.load_and_diff_rasters(self.fn, second_fn)
|
|
774
|
+
return diff_data
|
|
772
775
|
|
|
773
776
|
@staticmethod
|
|
774
777
|
def save_raster(data, output_fn, reference_fn, dtype=None, nodata=None):
|
|
@@ -819,9 +822,9 @@ class Raster:
|
|
|
819
822
|
dst.write(data.astype(profile["dtype"]), 1)
|
|
820
823
|
|
|
821
824
|
@staticmethod
|
|
822
|
-
def
|
|
825
|
+
def _load_and_diff_rasters_da(first_fn, second_fn):
|
|
823
826
|
"""
|
|
824
|
-
Load two rasters, align them, and compute their difference.
|
|
827
|
+
Load two rasters, align them, and compute their difference as a DataArray.
|
|
825
828
|
|
|
826
829
|
Parameters
|
|
827
830
|
----------
|
|
@@ -833,7 +836,7 @@ class Raster:
|
|
|
833
836
|
Returns
|
|
834
837
|
-------
|
|
835
838
|
tuple
|
|
836
|
-
(difference
|
|
839
|
+
(difference xarray.DataArray, nodata value)
|
|
837
840
|
|
|
838
841
|
Notes
|
|
839
842
|
-----
|
|
@@ -841,35 +844,49 @@ class Raster:
|
|
|
841
844
|
grid before differencing. Both rasters are cropped to their intersection first
|
|
842
845
|
(matching geoutils behavior). Uses rioxarray for efficient reprojection.
|
|
843
846
|
"""
|
|
844
|
-
# Load rasters with rioxarray
|
|
845
847
|
first = rioxarray.open_rasterio(first_fn, masked=True).squeeze()
|
|
846
848
|
second = rioxarray.open_rasterio(second_fn, masked=True).squeeze()
|
|
847
849
|
|
|
848
|
-
# Get first raster's bounds in second raster's CRS
|
|
849
850
|
first_bounds_in_ref_crs = first.rio.transform_bounds(second.rio.crs)
|
|
850
|
-
|
|
851
|
-
# Clip second raster to first raster's bounds (intersection)
|
|
852
851
|
second_clipped = second.rio.clip_box(*first_bounds_in_ref_crs)
|
|
853
|
-
|
|
854
|
-
# Reproject first raster to match clipped second raster's grid
|
|
855
852
|
first_reproj = first.rio.reproject_match(second_clipped)
|
|
856
853
|
|
|
857
|
-
# Compute difference: second - first
|
|
858
854
|
diff = second_clipped - first_reproj
|
|
859
855
|
|
|
860
|
-
# Extract metadata
|
|
861
|
-
dst_transform = second_clipped.rio.transform()
|
|
862
|
-
dst_crs = second_clipped.rio.crs
|
|
863
856
|
nodata = (
|
|
864
857
|
second.rio.encoded_nodata
|
|
865
858
|
if second.rio.encoded_nodata is not None
|
|
866
859
|
else -9999
|
|
867
860
|
)
|
|
868
861
|
|
|
869
|
-
|
|
870
|
-
diff_array = np.ma.masked_invalid(diff.values)
|
|
862
|
+
return diff, nodata
|
|
871
863
|
|
|
872
|
-
|
|
864
|
+
@staticmethod
|
|
865
|
+
def load_and_diff_rasters(first_fn, second_fn):
|
|
866
|
+
"""
|
|
867
|
+
Load two rasters, align them, and compute their difference.
|
|
868
|
+
|
|
869
|
+
Parameters
|
|
870
|
+
----------
|
|
871
|
+
first_fn : str
|
|
872
|
+
Path to the first raster file
|
|
873
|
+
second_fn : str
|
|
874
|
+
Path to the second raster file (used as reference grid)
|
|
875
|
+
|
|
876
|
+
Returns
|
|
877
|
+
-------
|
|
878
|
+
tuple
|
|
879
|
+
(difference array (second_raster - first_raster), transform, CRS, nodata)
|
|
880
|
+
|
|
881
|
+
Notes
|
|
882
|
+
-----
|
|
883
|
+
The first raster is reprojected and resampled to match the second raster's
|
|
884
|
+
grid before differencing. Both rasters are cropped to their intersection first
|
|
885
|
+
(matching geoutils behavior). Uses rioxarray for efficient reprojection.
|
|
886
|
+
"""
|
|
887
|
+
diff, nodata = Raster._load_and_diff_rasters_da(first_fn, second_fn)
|
|
888
|
+
diff_array = np.ma.masked_invalid(diff.values)
|
|
889
|
+
return diff_array, diff.rio.transform(), diff.rio.crs, nodata
|
|
873
890
|
|
|
874
891
|
|
|
875
892
|
class Plotter:
|
|
@@ -1045,3 +1062,57 @@ class Plotter:
|
|
|
1045
1062
|
|
|
1046
1063
|
if ctx_kwargs:
|
|
1047
1064
|
ctx.add_basemap(ax=ax, **ctx_kwargs)
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
def detect_vantor_satellite(directory):
|
|
1068
|
+
"""Check if XML files in directory indicate a Vantor (WorldView) satellite.
|
|
1069
|
+
|
|
1070
|
+
Parameters
|
|
1071
|
+
----------
|
|
1072
|
+
directory : str
|
|
1073
|
+
Path to directory containing XML camera model files.
|
|
1074
|
+
|
|
1075
|
+
Returns
|
|
1076
|
+
-------
|
|
1077
|
+
bool
|
|
1078
|
+
True if any XML file contains a WorldView SATID (WV01, WV02, WV03, etc.).
|
|
1079
|
+
"""
|
|
1080
|
+
try:
|
|
1081
|
+
xml_files = glob_file(directory, "*.[Xx][Mm][Ll]", all_files=True)
|
|
1082
|
+
except Exception:
|
|
1083
|
+
return False
|
|
1084
|
+
if not xml_files:
|
|
1085
|
+
return False
|
|
1086
|
+
xml_files = [f for f in xml_files if not re.search(r".*ortho.*\.xml", f)]
|
|
1087
|
+
for xml_file in xml_files:
|
|
1088
|
+
try:
|
|
1089
|
+
satid = get_xml_tag(xml_file, "SATID")
|
|
1090
|
+
if satid.startswith("WV"):
|
|
1091
|
+
return True
|
|
1092
|
+
except (ValueError, Exception):
|
|
1093
|
+
continue
|
|
1094
|
+
return False
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
def add_copyright_overlay(ax):
|
|
1098
|
+
"""Add Vantor copyright text overlay to the bottom-right of a matplotlib axes.
|
|
1099
|
+
|
|
1100
|
+
Parameters
|
|
1101
|
+
----------
|
|
1102
|
+
ax : matplotlib.axes.Axes
|
|
1103
|
+
The axes to add the copyright overlay to.
|
|
1104
|
+
"""
|
|
1105
|
+
from datetime import datetime
|
|
1106
|
+
|
|
1107
|
+
year = datetime.now().year
|
|
1108
|
+
ax.text(
|
|
1109
|
+
0.98,
|
|
1110
|
+
0.02,
|
|
1111
|
+
f"\u00a9 Vantor {year}",
|
|
1112
|
+
transform=ax.transAxes,
|
|
1113
|
+
fontsize=7,
|
|
1114
|
+
color="white",
|
|
1115
|
+
ha="right",
|
|
1116
|
+
va="bottom",
|
|
1117
|
+
bbox=dict(boxstyle="round,pad=0.3", facecolor="black", alpha=0.5),
|
|
1118
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|