Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/GispoCoding/eis_toolkit i…
Browse files Browse the repository at this point in the history
…nto 439-add-proximity-to-anomaly-tool
  • Loading branch information
nmaarnio committed Oct 23, 2024
2 parents aedd87f + 384f829 commit 3499c66
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 27 deletions.
28 changes: 20 additions & 8 deletions eis_toolkit/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1275,7 +1275,9 @@ def distance_to_anomaly_cli(
Uses only the first band of the raster.
"""
from eis_toolkit.raster_processing.distance_to_anomaly import distance_to_anomaly
from sys import platform

from eis_toolkit.raster_processing.distance_to_anomaly import distance_to_anomaly, distance_to_anomaly_gdal

typer.echo("Progress: 10%")

Expand All @@ -1286,13 +1288,23 @@ def distance_to_anomaly_cli(

with rasterio.open(input_raster) as raster:
typer.echo("Progress: 25%")
out_image, out_meta = distance_to_anomaly(
anomaly_raster_profile=raster.profile,
anomaly_raster_data=raster.read(1),
threshold_criteria_value=threshold_criteria_value,
threshold_criteria=get_enum_values(threshold_criteria),
max_distance=max_distance,
)
# Use optimized version if Windows
if platform == "win32":
out_image, out_meta = distance_to_anomaly_gdal(
anomaly_raster_profile=raster.profile,
anomaly_raster_data=raster.read(1),
threshold_criteria_value=threshold_criteria_value,
threshold_criteria=get_enum_values(threshold_criteria),
max_distance=max_distance,
)
else:
out_image, out_meta = distance_to_anomaly(
anomaly_raster_profile=raster.profile,
anomaly_raster_data=raster.read(1),
threshold_criteria_value=threshold_criteria_value,
threshold_criteria=get_enum_values(threshold_criteria),
max_distance=max_distance,
)

typer.echo("Progress: 75%")

Expand Down
25 changes: 15 additions & 10 deletions eis_toolkit/raster_processing/distance_to_anomaly.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from osgeo import gdal
from rasterio import profiles

from eis_toolkit.exceptions import EmptyDataException, InvalidParameterValueException
from eis_toolkit.exceptions import EmptyDataException, InvalidParameterValueException, NumericValueSignException
from eis_toolkit.utilities.checks.raster import check_raster_profile
from eis_toolkit.utilities.miscellaneous import row_points, toggle_gdal_exceptions
from eis_toolkit.vector_processing.distance_computation import distance_computation
Expand Down Expand Up @@ -84,26 +84,24 @@ def distance_to_anomaly(


@beartype
def distance_to_anomaly_gdal_compute_proximity(
def distance_to_anomaly_gdal(
anomaly_raster_profile: Union[profiles.Profile, dict],
anomaly_raster_data: np.ndarray,
threshold_criteria_value: Union[Tuple[Number, Number], Number],
threshold_criteria: Literal["lower", "higher", "in_between", "outside"],
max_distance: Optional[Number] = None,
) -> Tuple[np.ndarray, profiles.Profile]:
"""Calculate distance from raster cell to nearest anomaly.
This tool is much faster than `distance_to_anomaly` but only available on
Windows.
The criteria for what is anomalous can be defined as a single number and
criteria text of "higher" or "lower". Alternatively, the definition can be
a range where values inside (criteria text of "within") or outside are
marked as anomalous (criteria text of "outside"). If anomaly_raster_profile does
contain "nodata" key, np.nan is assumed to correspond to nodata values.
This function demonstrates superior performance compared to the distance_to_anomaly
and distance_to_anomaly_gdal functions, as it uses a low-level, C++-based API
within the GDAL library. By directly computing the proximity map from the
source dataset, it benefits from the core-level optimizations inherent to GDAL,
ensuring enhanced efficiency and speed.
Args:
anomaly_raster_profile: The raster profile in which the distances
to the nearest anomalous value are determined.
Expand All @@ -114,6 +112,7 @@ def distance_to_anomaly_gdal_compute_proximity(
the first value should be the minimum and the second
the maximum value.
threshold_criteria: Method to define anomalous.
max_distance: The maximum distance in the output array.
Returns:
A 2D numpy array with the distances to anomalies computed
Expand All @@ -124,12 +123,15 @@ def distance_to_anomaly_gdal_compute_proximity(
_check_threshold_criteria_and_value(
threshold_criteria=threshold_criteria, threshold_criteria_value=threshold_criteria_value
)
if max_distance is not None and max_distance <= 0:
raise NumericValueSignException("Expected max distance to be a positive number.")

out_array, out_meta = _distance_to_anomaly_gdal_compute_proximity(
out_array, out_meta = _distance_to_anomaly_gdal(
anomaly_raster_profile=anomaly_raster_profile,
anomaly_raster_data=anomaly_raster_data,
threshold_criteria=threshold_criteria,
threshold_criteria_value=threshold_criteria_value,
max_distance=max_distance,
)

return out_array, out_meta
Expand Down Expand Up @@ -216,11 +218,12 @@ def _distance_to_anomaly(
return distance_array


def _distance_to_anomaly_gdal_compute_proximity(
def _distance_to_anomaly_gdal(
anomaly_raster_profile: Union[profiles.Profile, dict],
anomaly_raster_data: np.ndarray,
threshold_criteria_value: Union[Tuple[Number, Number], Number],
threshold_criteria: Literal["lower", "higher", "in_between", "outside"],
max_distance: Optional[Number],
) -> Tuple[np.ndarray, profiles.Profile]:

data_fits_criteria = _validate_threshold_criteria(
Expand Down Expand Up @@ -259,6 +262,8 @@ def _distance_to_anomaly_gdal_compute_proximity(

# Create outputs
out_array = out_band.ReadAsArray()
if max_distance is not None:
out_array[out_array > max_distance] = max_distance
out_meta = anomaly_raster_profile.copy()

# Update metadata
Expand Down
18 changes: 9 additions & 9 deletions tests/raster_processing/test_distance_to_anomaly.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,18 +276,18 @@ def test_distance_to_anomaly_nodata_handling(
EXPECTED_PYTESTPARAMS,
)
@pytest.mark.xfail(sys.platform != "win32", reason="gdal_array available only on Windows.", raises=ModuleNotFoundError)
def test_distance_to_anomaly_gdal_compute_proximity_expected(
def test_distance_to_anomaly_gdal_expected(
anomaly_raster_profile,
anomaly_raster_data,
threshold_criteria_value,
threshold_criteria,
expected_shape,
expected_mean,
):
"""Test distance_to_anomaly_gdal_compute_proximity_expected with expected result."""
"""Test distance_to_anomaly_gdal with expected result."""

assert not np.any(np.isnan(anomaly_raster_data))
out_image, out_profile = distance_to_anomaly.distance_to_anomaly_gdal_compute_proximity(
out_image, out_profile = distance_to_anomaly.distance_to_anomaly_gdal(
anomaly_raster_profile=anomaly_raster_profile,
anomaly_raster_data=anomaly_raster_data,
threshold_criteria_value=threshold_criteria_value,
Expand Down Expand Up @@ -330,7 +330,7 @@ def test_distance_to_anomaly_gdal_compute_proximity_expected(
],
)
@pytest.mark.xfail(sys.platform != "win32", reason="gdal_array available only on Windows.", raises=ModuleNotFoundError)
def test_distance_to_anomaly_gdal_compute_proximity_nodata_handling(
def test_distance_to_anomaly_gdal_nodata_handling(
anomaly_raster_profile,
anomaly_raster_data,
threshold_criteria_value,
Expand All @@ -339,12 +339,12 @@ def test_distance_to_anomaly_gdal_compute_proximity_nodata_handling(
expected_mean_without_nodata,
nodata_mask_value,
):
"""Test distance_to_anomaly_gdal_compute_proximity with expected result."""
"""Test distance_to_anomaly_gdal with expected result."""

anomaly_raster_data_with_nodata = np.where(anomaly_raster_data > nodata_mask_value, np.nan, anomaly_raster_data)
assert np.any(np.isnan(anomaly_raster_data_with_nodata))

out_image, out_profile = distance_to_anomaly.distance_to_anomaly_gdal_compute_proximity(
out_image, out_profile = distance_to_anomaly.distance_to_anomaly_gdal(
anomaly_raster_profile=anomaly_raster_profile,
anomaly_raster_data=anomaly_raster_data_with_nodata,
threshold_criteria_value=threshold_criteria_value,
Expand Down Expand Up @@ -437,20 +437,20 @@ def test_distance_to_anomaly_gdal_compute_proximity_nodata_handling(
],
)
@pytest.mark.xfail(sys.platform != "win32", reason="gdal_array available only on Windows.", raises=ModuleNotFoundError)
def test_distance_to_anomaly_gdal_compute_proximity_expected_check(
def test_distance_to_anomaly_gdal_expected_check(
anomaly_raster_profile,
anomaly_raster_data,
threshold_criteria_value,
threshold_criteria,
profile_additions,
raises,
):
"""Test distance_to_anomaly_gdal_compute_proximity checks."""
"""Test distance_to_anomaly_gdal checks."""

anomaly_raster_profile.update(profile_additions())
anomaly_raster_profile_with_additions = anomaly_raster_profile
with raises() as exc_info:
out_image, out_profile = distance_to_anomaly.distance_to_anomaly_gdal_compute_proximity(
out_image, out_profile = distance_to_anomaly.distance_to_anomaly_gdal(
anomaly_raster_profile=anomaly_raster_profile_with_additions,
anomaly_raster_data=anomaly_raster_data,
threshold_criteria_value=threshold_criteria_value,
Expand Down

0 comments on commit 3499c66

Please sign in to comment.