cubio 0.1.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.
- cubio-0.1.0/LICENSE +21 -0
- cubio-0.1.0/PKG-INFO +30 -0
- cubio-0.1.0/README.md +2 -0
- cubio-0.1.0/pyproject.toml +29 -0
- cubio-0.1.0/src/cubio/__init__.py +11 -0
- cubio-0.1.0/src/cubio/conevnience_functions/__init__.py +9 -0
- cubio-0.1.0/src/cubio/conevnience_functions/envi_file_interface.py +144 -0
- cubio-0.1.0/src/cubio/cube_context.py +355 -0
- cubio-0.1.0/src/cubio/cube_data.py +193 -0
- cubio-0.1.0/src/cubio/cube_reader.py +24 -0
- cubio-0.1.0/src/cubio/cube_size_tools.py +72 -0
- cubio-0.1.0/src/cubio/cube_writer.py +40 -0
- cubio-0.1.0/src/cubio/data/__init__.py +0 -0
- cubio-0.1.0/src/cubio/data/crs_wkt_strings.py +8 -0
- cubio-0.1.0/src/cubio/envi_hdr_tools.py +104 -0
- cubio-0.1.0/src/cubio/geotools/__init__.py +0 -0
- cubio-0.1.0/src/cubio/geotools/generate_geoloc_backplane.py +51 -0
- cubio-0.1.0/src/cubio/geotools/georeference_from_gcps.py +115 -0
- cubio-0.1.0/src/cubio/geotools/georeference_satellite_swath.py +130 -0
- cubio-0.1.0/src/cubio/geotools/models/__init__.py +13 -0
- cubio-0.1.0/src/cubio/geotools/models/bounding_box_model.py +190 -0
- cubio-0.1.0/src/cubio/geotools/models/gcp_model.py +186 -0
- cubio-0.1.0/src/cubio/geotools/models/geotransform_model.py +224 -0
- cubio-0.1.0/src/cubio/py.typed +0 -0
- cubio-0.1.0/src/cubio/types.py +107 -0
cubio-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zach
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
cubio-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cubio
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A fully typed and pydantic-compatible framework for reading and writing geospatial data cubes.
|
|
5
|
+
License: LICENSE
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: Z.M. Vig
|
|
8
|
+
Author-email: zvig@umd.edu
|
|
9
|
+
Requires-Python: >=3.13,<4
|
|
10
|
+
Classifier: License :: Other/Proprietary License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Requires-Dist: affine (>=2.4.0,<3.0.0)
|
|
15
|
+
Requires-Dist: numpy (>=2.4.2,<3.0.0)
|
|
16
|
+
Requires-Dist: pydantic (>=2.12.5,<3.0.0)
|
|
17
|
+
Requires-Dist: pyresample (>=1.35.0,<2.0.0)
|
|
18
|
+
Requires-Dist: rasterio (>=1.5.0,<2.0.0)
|
|
19
|
+
Requires-Dist: scipy (>=1.17.1,<2.0.0)
|
|
20
|
+
Requires-Dist: scipy-stubs (>=1.17.1.0,<2.0.0.0)
|
|
21
|
+
Requires-Dist: spectralio (>=0.1.11,<0.2.0)
|
|
22
|
+
Requires-Dist: tqdm (>=4.67.3,<5.0.0)
|
|
23
|
+
Requires-Dist: tqdm-stubs (>=0.2.1,<0.3.0)
|
|
24
|
+
Requires-Dist: xarray (>=2026.2.0,<2027.0.0)
|
|
25
|
+
Requires-Dist: zarr (>=3.1.5,<4.0.0)
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# cubio
|
|
29
|
+
A fully typed and pydantic-compatible framework for reading and writing geospatial data cubes.
|
|
30
|
+
|
cubio-0.1.0/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "cubio"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A fully typed and pydantic-compatible framework for reading and writing geospatial data cubes."
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "Z.M. Vig",email = "zvig@umd.edu"}
|
|
7
|
+
]
|
|
8
|
+
license = {text = "LICENSE"}
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.13,<4"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"rasterio (>=1.5.0,<2.0.0)",
|
|
13
|
+
"pydantic (>=2.12.5,<3.0.0)",
|
|
14
|
+
"affine (>=2.4.0,<3.0.0)",
|
|
15
|
+
"numpy (>=2.4.2,<3.0.0)",
|
|
16
|
+
"spectralio (>=0.1.11,<0.2.0)",
|
|
17
|
+
"scipy (>=1.17.1,<2.0.0)",
|
|
18
|
+
"scipy-stubs (>=1.17.1.0,<2.0.0.0)",
|
|
19
|
+
"xarray (>=2026.2.0,<2027.0.0)",
|
|
20
|
+
"tqdm (>=4.67.3,<5.0.0)",
|
|
21
|
+
"tqdm-stubs (>=0.2.1,<0.3.0)",
|
|
22
|
+
"pyresample (>=1.35.0,<2.0.0)",
|
|
23
|
+
"zarr (>=3.1.5,<4.0.0)"
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
29
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Welcome to `cubio`!
|
|
3
|
+
---
|
|
4
|
+
### Available Classes
|
|
5
|
+
- CubeContext // Metadata for reading in geospatial cubes.
|
|
6
|
+
- CubeData // Memory-mapped pointer for performing processing with `xarray`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .cube_context import CubeContext
|
|
10
|
+
|
|
11
|
+
__all__ = ["CubeContext"]
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Built-Ins
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
|
|
5
|
+
# Dependencies
|
|
6
|
+
import rasterio as rio # type: ignore
|
|
7
|
+
|
|
8
|
+
# Local Imports
|
|
9
|
+
from cubio.types import RasterioProfile, NumpyDType
|
|
10
|
+
from cubio.cube_context import CubeContext, ContextBuilder
|
|
11
|
+
from cubio.geotools.models import GeotransformModel
|
|
12
|
+
from cubio.data.crs_wkt_strings import GeographicCRS
|
|
13
|
+
from cubio.envi_hdr_tools import (
|
|
14
|
+
extract_hdr_wavelengths,
|
|
15
|
+
extract_hdr_bbl,
|
|
16
|
+
extract_hdr_band_names,
|
|
17
|
+
extract_hdr_desc,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def read_spectral_envi_file_context(fp: Path | str, name: str) -> CubeContext:
|
|
22
|
+
"""
|
|
23
|
+
Reads the context data from an ENVI cube file representing spectral data.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
fp : Path | str
|
|
28
|
+
File path to ENVI cube.
|
|
29
|
+
name : str
|
|
30
|
+
Name of the CubeContext object.
|
|
31
|
+
|
|
32
|
+
Returns
|
|
33
|
+
-------
|
|
34
|
+
CubeContext
|
|
35
|
+
Relevant context data about the cube.
|
|
36
|
+
|
|
37
|
+
Raises
|
|
38
|
+
------
|
|
39
|
+
ValueError
|
|
40
|
+
If the .hdr file does not contain a wavelength field.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
with rio.open(fp, "r") as f:
|
|
44
|
+
prf: RasterioProfile = f.profile
|
|
45
|
+
|
|
46
|
+
hdr_fp = Path(fp).with_suffix(".hdr")
|
|
47
|
+
wvls = extract_hdr_wavelengths(hdr_fp)
|
|
48
|
+
desc = extract_hdr_desc(hdr_fp)
|
|
49
|
+
bbl = extract_hdr_bbl(hdr_fp)
|
|
50
|
+
if wvls == "Wavelengths not found.":
|
|
51
|
+
raise ValueError(
|
|
52
|
+
"File is not a spectral envi file. Try"
|
|
53
|
+
"`read_measurement_envi_file_context()`"
|
|
54
|
+
)
|
|
55
|
+
if bbl == "No BBL Found":
|
|
56
|
+
bbl = [1] * len(wvls)
|
|
57
|
+
|
|
58
|
+
if prf["crs"] is None:
|
|
59
|
+
crs = GeographicCRS.GCS_MOON_2000
|
|
60
|
+
else:
|
|
61
|
+
crs = prf["crs"]
|
|
62
|
+
|
|
63
|
+
context_dict: ContextBuilder = {
|
|
64
|
+
"name": name,
|
|
65
|
+
"description": desc,
|
|
66
|
+
"data_filename": Path(Path(fp).name),
|
|
67
|
+
"nrows": prf["height"],
|
|
68
|
+
"ncols": prf["width"],
|
|
69
|
+
"nbands": prf["count"],
|
|
70
|
+
"crs": crs,
|
|
71
|
+
"geotransform": GeotransformModel.fromaffine(prf["transform"]),
|
|
72
|
+
"hdr_off": 0,
|
|
73
|
+
"data_type": NumpyDType.FLOAT32,
|
|
74
|
+
"interleave": "BIL",
|
|
75
|
+
"nodata": -999,
|
|
76
|
+
"band_names": [f"Band{n+1}({i}nm)" for n, i in enumerate(wvls)],
|
|
77
|
+
"measurement_name": "Wavelength",
|
|
78
|
+
"measurement_units": "nm",
|
|
79
|
+
"measurement_values": wvls,
|
|
80
|
+
"bad_bands": bbl,
|
|
81
|
+
"id": uuid4(),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return CubeContext.from_builder(context_dict)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def read_measurement_envi_file_context(
|
|
88
|
+
fp: Path | str, name: str
|
|
89
|
+
) -> CubeContext:
|
|
90
|
+
"""
|
|
91
|
+
Reads the context data from an envi file that does not represent a spectral
|
|
92
|
+
dataset, but some other measurement (Location, Geometry, Parameters,
|
|
93
|
+
etc...)
|
|
94
|
+
|
|
95
|
+
Parameters
|
|
96
|
+
----------
|
|
97
|
+
fp : Path | str
|
|
98
|
+
File path to the ENVI cube.
|
|
99
|
+
name : str
|
|
100
|
+
Name of the CubeContext object.
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
CubeContext
|
|
105
|
+
Resulting context data from the cube.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
with rio.open(fp, "r") as f:
|
|
109
|
+
prf: RasterioProfile = f.profile
|
|
110
|
+
|
|
111
|
+
hdr_fp = Path(fp).with_suffix(".hdr")
|
|
112
|
+
band_names = extract_hdr_band_names(hdr_fp)
|
|
113
|
+
bbl = extract_hdr_bbl(hdr_fp)
|
|
114
|
+
desc = extract_hdr_desc(hdr_fp)
|
|
115
|
+
if bbl == "No BBL Found":
|
|
116
|
+
bbl = [1] * len(band_names)
|
|
117
|
+
|
|
118
|
+
if prf["crs"] is None:
|
|
119
|
+
crs = GeographicCRS.GCS_MOON_2000
|
|
120
|
+
else:
|
|
121
|
+
crs = prf["crs"]
|
|
122
|
+
|
|
123
|
+
context_dict: ContextBuilder = {
|
|
124
|
+
"name": name,
|
|
125
|
+
"description": desc,
|
|
126
|
+
"data_filename": Path(Path(fp).name),
|
|
127
|
+
"nrows": prf["height"],
|
|
128
|
+
"ncols": prf["width"],
|
|
129
|
+
"nbands": prf["count"],
|
|
130
|
+
"crs": crs,
|
|
131
|
+
"geotransform": GeotransformModel.fromaffine(prf["transform"]),
|
|
132
|
+
"hdr_off": 0,
|
|
133
|
+
"data_type": NumpyDType(prf["dtype"]),
|
|
134
|
+
"interleave": "BIL",
|
|
135
|
+
"nodata": -999,
|
|
136
|
+
"band_names": band_names,
|
|
137
|
+
"measurement_name": name,
|
|
138
|
+
"measurement_units": "unitless",
|
|
139
|
+
"measurement_values": [i for i in range(prf["count"])],
|
|
140
|
+
"bad_bands": bbl,
|
|
141
|
+
"id": uuid4(),
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return CubeContext.from_builder(context_dict)
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# Built-ins
|
|
2
|
+
from typing import TypedDict, Literal
|
|
3
|
+
from typing_extensions import Self
|
|
4
|
+
from tempfile import NamedTemporaryFile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import shutil
|
|
7
|
+
from uuid import UUID, uuid4
|
|
8
|
+
|
|
9
|
+
# Dependencies
|
|
10
|
+
import numpy as np
|
|
11
|
+
from pydantic import (
|
|
12
|
+
BaseModel,
|
|
13
|
+
field_serializer,
|
|
14
|
+
model_validator,
|
|
15
|
+
Field,
|
|
16
|
+
field_validator,
|
|
17
|
+
PrivateAttr,
|
|
18
|
+
)
|
|
19
|
+
import rasterio as rio # type: ignore
|
|
20
|
+
import xarray as xr
|
|
21
|
+
|
|
22
|
+
# Local Imports
|
|
23
|
+
from cubio.types import (
|
|
24
|
+
NumpyDType,
|
|
25
|
+
CubeArrayFormat,
|
|
26
|
+
is_valid_cubearrayformat,
|
|
27
|
+
suffix_to_format_map,
|
|
28
|
+
RasterioProfile,
|
|
29
|
+
ImageSuffix,
|
|
30
|
+
is_valid_image_suffix,
|
|
31
|
+
image_suffix_priority,
|
|
32
|
+
)
|
|
33
|
+
from cubio.geotools.models import GeotransformModel
|
|
34
|
+
from cubio.envi_hdr_tools import (
|
|
35
|
+
replace_hdr_band_names,
|
|
36
|
+
replace_hdr_description,
|
|
37
|
+
replace_shape_fields,
|
|
38
|
+
)
|
|
39
|
+
from cubio.cube_size_tools import CubeSize
|
|
40
|
+
from cubio.cube_data import CubeData
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ContextBuilder(TypedDict):
|
|
44
|
+
name: str
|
|
45
|
+
description: str
|
|
46
|
+
data_filename: Path
|
|
47
|
+
ncols: int
|
|
48
|
+
nrows: int
|
|
49
|
+
nbands: int
|
|
50
|
+
hdr_off: int
|
|
51
|
+
data_type: NumpyDType
|
|
52
|
+
interleave: CubeArrayFormat
|
|
53
|
+
crs: str
|
|
54
|
+
geotransform: GeotransformModel
|
|
55
|
+
band_names: list[str]
|
|
56
|
+
nodata: float | int
|
|
57
|
+
measurement_name: str
|
|
58
|
+
measurement_units: str
|
|
59
|
+
measurement_values: list[float]
|
|
60
|
+
bad_bands: list[int]
|
|
61
|
+
id: UUID
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class CubeContext(BaseModel):
|
|
65
|
+
name: str = Field(..., description="A short name for the cube.")
|
|
66
|
+
description: str = Field(
|
|
67
|
+
..., description="A description of the data cube."
|
|
68
|
+
)
|
|
69
|
+
data_filename: Path = Field(
|
|
70
|
+
...,
|
|
71
|
+
description="Filename of the data that is described by this file. It"
|
|
72
|
+
" should be in the same directory as this file.",
|
|
73
|
+
)
|
|
74
|
+
ncols: int = Field(..., description="Number of columns in the data cube.")
|
|
75
|
+
nrows: int = Field(..., description="Number of rows in the data cube.")
|
|
76
|
+
nbands: int = Field(..., description="Number of bands in the data cube.")
|
|
77
|
+
hdr_off: int = Field(default=0, description="Header offset in bytes.")
|
|
78
|
+
data_type: NumpyDType = Field(
|
|
79
|
+
..., description="The data type of the cube."
|
|
80
|
+
)
|
|
81
|
+
interleave: CubeArrayFormat = Field(
|
|
82
|
+
...,
|
|
83
|
+
description=(
|
|
84
|
+
"The interleave format of the cube. Either BIL, BIP, or BSQ."
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
crs: str = Field(
|
|
88
|
+
..., description="Coordinate reference system of the cube."
|
|
89
|
+
)
|
|
90
|
+
geotransform: GeotransformModel = Field(
|
|
91
|
+
..., description="Geotransform model for the cube."
|
|
92
|
+
)
|
|
93
|
+
band_names: list[str] = Field(default_factory=list)
|
|
94
|
+
nodata: float | int = Field(default=-999, description="The nodata value.")
|
|
95
|
+
measurement_name: str = Field(
|
|
96
|
+
default="Measurement",
|
|
97
|
+
description="Name that describes the nature of the measurement along "
|
|
98
|
+
"the cube z-axis.",
|
|
99
|
+
)
|
|
100
|
+
measurement_units: str = Field(
|
|
101
|
+
default="unitless", description="The measurement units."
|
|
102
|
+
)
|
|
103
|
+
measurement_values: list[float] = Field(
|
|
104
|
+
default_factory=list, description="The measurement values."
|
|
105
|
+
)
|
|
106
|
+
bad_bands: list[int] = Field(
|
|
107
|
+
default_factory=list, description="List of bad band flags."
|
|
108
|
+
)
|
|
109
|
+
id: UUID = Field(
|
|
110
|
+
default_factory=uuid4, description="Unique ID of the cube object."
|
|
111
|
+
)
|
|
112
|
+
_retrieval_path: Path | Literal["NoRetrieval"] = PrivateAttr(
|
|
113
|
+
default="NoRetrieval"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def shape(self) -> CubeSize:
|
|
118
|
+
return CubeSize(
|
|
119
|
+
nrows=self.nrows, ncolumns=self.ncols, nbands=self.nbands
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def shape_tuple(self) -> tuple[int, int, int]:
|
|
124
|
+
return self.shape.as_tuple(self.interleave)
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def builder(self) -> ContextBuilder:
|
|
128
|
+
_builder: ContextBuilder = {
|
|
129
|
+
"name": self.name,
|
|
130
|
+
"description": self.description,
|
|
131
|
+
"data_filename": self.data_filename,
|
|
132
|
+
"ncols": self.ncols,
|
|
133
|
+
"nrows": self.nrows,
|
|
134
|
+
"nbands": self.nbands,
|
|
135
|
+
"hdr_off": self.hdr_off,
|
|
136
|
+
"data_type": self.data_type,
|
|
137
|
+
"interleave": self.interleave,
|
|
138
|
+
"crs": self.crs,
|
|
139
|
+
"geotransform": self.geotransform,
|
|
140
|
+
"band_names": self.band_names,
|
|
141
|
+
"nodata": self.nodata,
|
|
142
|
+
"measurement_name": self.measurement_name,
|
|
143
|
+
"measurement_units": self.measurement_units,
|
|
144
|
+
"measurement_values": self.measurement_values,
|
|
145
|
+
"bad_bands": self.bad_bands,
|
|
146
|
+
"id": self.id,
|
|
147
|
+
}
|
|
148
|
+
return _builder
|
|
149
|
+
|
|
150
|
+
@classmethod
|
|
151
|
+
def from_builder(cls, builder_dict: ContextBuilder) -> Self:
|
|
152
|
+
return cls(**builder_dict)
|
|
153
|
+
|
|
154
|
+
@classmethod
|
|
155
|
+
def from_json(cls, savefp: str | Path) -> Self:
|
|
156
|
+
"""Convenience method for reading in the model from json file."""
|
|
157
|
+
with open(savefp, "r") as f:
|
|
158
|
+
json = f.read()
|
|
159
|
+
valid_model = cls.model_validate_json(json)
|
|
160
|
+
valid_model._retrieval_path = Path(savefp)
|
|
161
|
+
return valid_model
|
|
162
|
+
|
|
163
|
+
@field_serializer("interleave", mode="plain")
|
|
164
|
+
def lowercase(self, value: CubeArrayFormat) -> str:
|
|
165
|
+
return value.lower()
|
|
166
|
+
|
|
167
|
+
@field_validator("interleave", mode="before")
|
|
168
|
+
@classmethod
|
|
169
|
+
def uppercase(cls, value: str) -> CubeArrayFormat:
|
|
170
|
+
ustr = value.upper()
|
|
171
|
+
if is_valid_cubearrayformat(ustr):
|
|
172
|
+
return ustr
|
|
173
|
+
else:
|
|
174
|
+
raise ValueError(f"Invalid interleave: {ustr}")
|
|
175
|
+
|
|
176
|
+
@model_validator(mode="after")
|
|
177
|
+
def set_default_bbl(self) -> Self:
|
|
178
|
+
current_bbl = self.bad_bands
|
|
179
|
+
if len(current_bbl) == 0:
|
|
180
|
+
self.bad_bands = [1] * len(self.measurement_values)
|
|
181
|
+
else:
|
|
182
|
+
self.bad_bands = current_bbl
|
|
183
|
+
|
|
184
|
+
if len(self.bad_bands) != self.nbands:
|
|
185
|
+
print(self.bad_bands)
|
|
186
|
+
raise ValueError(
|
|
187
|
+
"Length of bad band list must match the number of bands."
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return self
|
|
191
|
+
|
|
192
|
+
@model_validator(mode="after")
|
|
193
|
+
def check_measurement_values(self) -> Self:
|
|
194
|
+
if len(self.measurement_values) != self.nbands:
|
|
195
|
+
raise ValueError(
|
|
196
|
+
"Length of measurement values must match the number of bands."
|
|
197
|
+
)
|
|
198
|
+
return self
|
|
199
|
+
|
|
200
|
+
def write_envi_hdr(
|
|
201
|
+
self, dst: Path | str | None = None, use_image_name: bool = True
|
|
202
|
+
) -> None:
|
|
203
|
+
"""
|
|
204
|
+
Writes the model to a properly formatted ENVI header file.
|
|
205
|
+
|
|
206
|
+
Parameters
|
|
207
|
+
----------
|
|
208
|
+
dst : Path | str | None, optional
|
|
209
|
+
Destination file path, by default None.
|
|
210
|
+
|
|
211
|
+
Notes
|
|
212
|
+
-----
|
|
213
|
+
The file suffix is set internally, so the dst file does not need to
|
|
214
|
+
include one.
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
# A temporary file should be used if no path is provided.
|
|
218
|
+
if dst is None:
|
|
219
|
+
tf = NamedTemporaryFile(suffix=".hdr", delete=False)
|
|
220
|
+
savefp = Path(tf.name)
|
|
221
|
+
else:
|
|
222
|
+
savefp = Path(dst)
|
|
223
|
+
|
|
224
|
+
# Adding suffix based on the use_image_name option.
|
|
225
|
+
if use_image_name:
|
|
226
|
+
if savefp.is_dir():
|
|
227
|
+
savefp = Path(savefp, self.data_filename).with_suffix(".hdr")
|
|
228
|
+
else:
|
|
229
|
+
savefp = savefp.with_name(str(self.data_filename)).with_suffix(
|
|
230
|
+
".hdr"
|
|
231
|
+
)
|
|
232
|
+
else:
|
|
233
|
+
savefp = savefp.with_suffix(".hdr")
|
|
234
|
+
|
|
235
|
+
# Need a temporary image and hdr file to write to. These will be empty.
|
|
236
|
+
temp_img = savefp.with_name(f"_{savefp.name}").with_suffix(".img")
|
|
237
|
+
temp_hdr = temp_img.with_suffix(".hdr")
|
|
238
|
+
|
|
239
|
+
# Need a rasterio profile to do a proper write.
|
|
240
|
+
envi_profile: RasterioProfile = {
|
|
241
|
+
"driver": "ENVI",
|
|
242
|
+
"width": 1,
|
|
243
|
+
"height": 1,
|
|
244
|
+
"count": 1,
|
|
245
|
+
"dtype": self.data_type,
|
|
246
|
+
"crs": self.crs,
|
|
247
|
+
"transform": self.geotransform.toaffine(),
|
|
248
|
+
"interleave": self.interleave,
|
|
249
|
+
"nodata": -999,
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
# A blank image write is needed. We just use the .hdr file.
|
|
253
|
+
with rio.open(temp_img, "w", **envi_profile) as f:
|
|
254
|
+
f.write(np.empty((1, 1, 1)))
|
|
255
|
+
|
|
256
|
+
temp_img.unlink() # Discarding temporary image.
|
|
257
|
+
|
|
258
|
+
# Modifying rasterio-written .hdr file.
|
|
259
|
+
# These are existing fields.
|
|
260
|
+
replace_shape_fields(
|
|
261
|
+
temp_hdr,
|
|
262
|
+
samples=self.ncols,
|
|
263
|
+
lines=self.nrows,
|
|
264
|
+
bands=self.nbands,
|
|
265
|
+
)
|
|
266
|
+
replace_hdr_band_names(temp_hdr, self.band_names)
|
|
267
|
+
replace_hdr_description(temp_hdr, self.description)
|
|
268
|
+
|
|
269
|
+
with open(temp_hdr, "a") as f:
|
|
270
|
+
# These are all new fields.
|
|
271
|
+
f.write(f"data ignore value = {self.nodata}\n")
|
|
272
|
+
f.write(f"wavelenth units = {self.measurement_units}\n")
|
|
273
|
+
f.write(
|
|
274
|
+
"wavelength = "
|
|
275
|
+
f"{{{",".join([str(i) for i in self.measurement_values])}}}\n"
|
|
276
|
+
)
|
|
277
|
+
f.write(
|
|
278
|
+
"bbl = " f"{{{",".join([str(i) for i in self.bad_bands])}}}\n"
|
|
279
|
+
)
|
|
280
|
+
# Renaming the temporary hdr that was created with the temp image.
|
|
281
|
+
shutil.move(temp_hdr, temp_hdr.with_name(temp_hdr.name[1:]))
|
|
282
|
+
|
|
283
|
+
# Writing out the pydantic model in json form for easy reading.
|
|
284
|
+
self.write_json(savefp)
|
|
285
|
+
|
|
286
|
+
def write_json(self, savefp: str | Path) -> None:
|
|
287
|
+
"""Convenience function for dumping the model to a json file."""
|
|
288
|
+
json_str = self.model_dump_json(indent=2)
|
|
289
|
+
with open(Path(savefp).with_suffix(".json"), "w") as f:
|
|
290
|
+
f.write(json_str)
|
|
291
|
+
|
|
292
|
+
def lazy_load_data(self, search_dir: str | Path | None = None) -> CubeData:
|
|
293
|
+
load_from: Path
|
|
294
|
+
if (search_dir is not None) and (
|
|
295
|
+
self._retrieval_path == "NoRetrieval"
|
|
296
|
+
):
|
|
297
|
+
load_from = Path(search_dir)
|
|
298
|
+
elif (self._retrieval_path != "NoRetrieval") and (search_dir is None):
|
|
299
|
+
load_from = self._retrieval_path.parent
|
|
300
|
+
else:
|
|
301
|
+
raise ValueError(
|
|
302
|
+
"If `savefp` is specified, the object must not have been "
|
|
303
|
+
"validated from disk."
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
if not load_from.is_dir():
|
|
307
|
+
raise ValueError(f"Search directory does not exist: {load_from}")
|
|
308
|
+
|
|
309
|
+
dat = CubeData(
|
|
310
|
+
self.name,
|
|
311
|
+
self.interleave,
|
|
312
|
+
z_labels=self.measurement_values,
|
|
313
|
+
z_name=self.measurement_name,
|
|
314
|
+
geotransform=self.geotransform,
|
|
315
|
+
)
|
|
316
|
+
candidate_image_data_files: list[tuple[Path, ImageSuffix]] = []
|
|
317
|
+
for i in load_from.iterdir():
|
|
318
|
+
if i.stem == str(self.data_filename.stem):
|
|
319
|
+
suff = i.suffix.lower()
|
|
320
|
+
if is_valid_image_suffix(suff):
|
|
321
|
+
candidate_image_data_files.append((i, suff))
|
|
322
|
+
|
|
323
|
+
candidate_image_data_files.sort(
|
|
324
|
+
key=lambda item: image_suffix_priority[item[1]]
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
image_data_file = candidate_image_data_files[0][0]
|
|
328
|
+
|
|
329
|
+
interleave_test = suffix_to_format_map.get(image_data_file.suffix)
|
|
330
|
+
|
|
331
|
+
if (interleave_test is not None) and (
|
|
332
|
+
interleave_test != self.interleave
|
|
333
|
+
):
|
|
334
|
+
raise ValueError(
|
|
335
|
+
f"Loaded data ({image_data_file}) does not match the"
|
|
336
|
+
f"registered interleave ({self.interleave})"
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
if image_data_file.suffix.lower() in [".img", ".bsq", ".bil", ".bip"]:
|
|
340
|
+
mmap = np.memmap(
|
|
341
|
+
image_data_file,
|
|
342
|
+
dtype=np.dtype(self.data_type),
|
|
343
|
+
shape=self.shape_tuple,
|
|
344
|
+
)
|
|
345
|
+
dat.array = xr.DataArray(mmap)
|
|
346
|
+
elif image_data_file.suffix.lower() == ".hdf5":
|
|
347
|
+
raise NotImplementedError("HDF5 file not implemented yet.")
|
|
348
|
+
elif image_data_file.suffix.lower() == ".zarr":
|
|
349
|
+
arr = xr.open_dataarray(image_data_file, engine="zarr")
|
|
350
|
+
dat.yname = str(arr.dims[0])
|
|
351
|
+
dat.xname = str(arr.dims[1])
|
|
352
|
+
dat.zname = str(arr.dims[2])
|
|
353
|
+
dat.array = arr
|
|
354
|
+
|
|
355
|
+
return dat
|