Skip to content

Commit d8884e2

Browse files
mipesonmaarnio
authored andcommitted
feat: Add tool to replace pixel values with nodata
1 parent f51a363 commit d8884e2

File tree

3 files changed

+161
-1
lines changed

3 files changed

+161
-1
lines changed

eis_toolkit/cli.py

+35
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,16 @@ class WeightsType(str, Enum):
344344
descending = "descending"
345345

346346

347+
class ReplaceCondition(str, Enum):
348+
"""Replace conditions for replace with nodata."""
349+
350+
equal = "equal"
351+
less_than = "less_than"
352+
greater_than = "greater_than"
353+
less_than_or_equal = "less_than_or_equal"
354+
greater_than_or_equal = "greater_than_or_equal"
355+
356+
347357
INPUT_FILE_OPTION = Annotated[
348358
Path,
349359
typer.Option(
@@ -4077,6 +4087,31 @@ def convert_raster_nodata_cli(
40774087
typer.echo(f"Converting nodata completed, writing raster to {output_raster}.")
40784088

40794089

4090+
@app.command()
4091+
def replace_with_nodata_cli(
4092+
input_raster: INPUT_FILE_OPTION,
4093+
output_raster: OUTPUT_FILE_OPTION,
4094+
target_value: Annotated[float, typer.Option()],
4095+
nodata_value: float = None,
4096+
replace_condition: Annotated[ReplaceCondition, typer.Option(case_sensitive=False)] = ReplaceCondition.equal,
4097+
):
4098+
"""Replace raster pixel values with nodata."""
4099+
from eis_toolkit.utilities.nodata import replace_with_nodata
4100+
4101+
typer.echo("Progress: 10%")
4102+
4103+
with rasterio.open(input_raster) as raster:
4104+
typer.echo("Progress: 25%")
4105+
out_image, out_meta = replace_with_nodata(raster, target_value, nodata_value, replace_condition)
4106+
typer.echo("Progress: 70%")
4107+
4108+
with rasterio.open(output_raster, "w", **out_meta) as dst:
4109+
dst.write(out_image)
4110+
typer.echo("Progres: 100%")
4111+
4112+
typer.echo(f"Raster pixel values replaced with nodata, writing raster to {output_raster}.")
4113+
4114+
40804115
@app.command()
40814116
def set_raster_nodata_cli(
40824117
input_raster: INPUT_FILE_OPTION, output_raster: OUTPUT_FILE_OPTION, new_nodata: float = typer.Option()

eis_toolkit/utilities/nodata.py

+55-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import numpy as np
55
import rasterio
66
from beartype import beartype
7-
from beartype.typing import Any, Callable, Dict, Optional, Sequence, Tuple, Union
7+
from beartype.typing import Any, Callable, Dict, Literal, Optional, Sequence, Tuple, Union
88
from rasterio import profiles
99

1010
from eis_toolkit.exceptions import InvalidParameterValueException
@@ -101,6 +101,60 @@ def convert_raster_nodata(
101101
return out_image, out_meta
102102

103103

104+
@beartype
105+
def replace_with_nodata(
106+
input_raster: rasterio.io.DatasetReader,
107+
target_value: Number,
108+
nodata_value: Optional[Number] = None,
109+
replace_condition: Literal[
110+
"equal", "less_than", "greater_than", "less_than_or_equal", "greater_than_or_equal"
111+
] = "equal",
112+
) -> Tuple[np.ndarray, dict]:
113+
"""
114+
Replace pixels values with nodata for a raster.
115+
116+
Can be used either for replacing all pixels with certain value with nodata, or for replacing all pixels with
117+
values less than, greater than, less than or equal to, or greater than or equal to the target value with nodata.
118+
119+
Args:
120+
input_raster: Input raster dataset.
121+
target_value: Value to be replaced with nodata.
122+
nodata_value: Value that will be used as nodata. If not provided, nodata is determined from input raster.
123+
replace_condition: Whether to replace pixels with certain value or values less than, greater than, less than or
124+
equal, or greater than or equal to target_value with nodata.
125+
126+
Returns:
127+
The input raster data with specified pixels replaced with nodata, and updated metadata.
128+
129+
Raises:
130+
InvalidParameterValueException: Nodata is provided and not found in the input raster.
131+
"""
132+
133+
if nodata_value is None:
134+
nodata_value = input_raster.nodata
135+
if nodata_value is None:
136+
raise InvalidParameterValueException("Nodata not provided and not found in the input raster.")
137+
138+
raster_arr = input_raster.read()
139+
140+
if replace_condition == "equal":
141+
values_to_replace = target_value
142+
elif replace_condition == "less_than":
143+
values_to_replace = raster_arr[raster_arr < target_value].tolist()
144+
elif replace_condition == "greater_than":
145+
values_to_replace = raster_arr[raster_arr > target_value].tolist()
146+
elif replace_condition == "less_than_or_equal":
147+
values_to_replace = raster_arr[raster_arr <= target_value].tolist()
148+
elif replace_condition == "greater_than_or_equal":
149+
values_to_replace = raster_arr[raster_arr >= target_value].tolist()
150+
151+
out_image = replace_values(raster_arr, values_to_replace, nodata_value)
152+
out_meta = input_raster.meta.copy()
153+
out_meta["nodata"] = nodata_value
154+
155+
return out_image, out_meta
156+
157+
104158
@beartype
105159
def nodata_to_nan(data: np.ndarray, nodata_value: Number) -> np.ndarray:
106160
"""Convert specified nodata_value to np.nan.

tests/utilities/nodata_test.py

+71
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
handle_nodata_as_nan,
99
nan_to_nodata,
1010
nodata_to_nan,
11+
replace_with_nodata,
1112
set_raster_nodata,
1213
unify_raster_nodata,
1314
)
@@ -38,6 +39,76 @@ def test_convert_raster_nodata():
3839
assert out_meta["nodata"] == -999
3940

4041

42+
def test_replace_with_nodata():
43+
"""Test that replacing raster pixel values with nodata works as expected."""
44+
target_value = 2.705
45+
nodata_value = -999
46+
47+
with rasterio.open(SMALL_RASTER_PATH) as raster:
48+
raster_data = raster.read()
49+
nr_of_pixels = np.count_nonzero(raster_data == target_value)
50+
assert nr_of_pixels > 0
51+
assert np.count_nonzero(raster_data == nodata_value) == 0
52+
53+
replace_condition = "equal"
54+
out_image, out_meta = replace_with_nodata(raster, target_value, nodata_value, replace_condition)
55+
assert np.count_nonzero(out_image == nodata_value) > 0
56+
assert np.count_nonzero(out_image == target_value) == 0
57+
assert out_meta["nodata"] == -999
58+
59+
with rasterio.open(SMALL_RASTER_PATH) as raster:
60+
raster_data = raster.read()
61+
# Ensure some pixels exist that are less than the target value
62+
nr_of_pixels_less_than_target = np.count_nonzero(raster_data < target_value)
63+
assert nr_of_pixels_less_than_target > 0
64+
65+
replace_condition = "less_than"
66+
out_image, out_meta = replace_with_nodata(raster, target_value, nodata_value, replace_condition)
67+
68+
assert np.count_nonzero(out_image == nodata_value) == nr_of_pixels_less_than_target
69+
assert np.count_nonzero((out_image < target_value) & (out_image != nodata_value)) == 0
70+
assert out_meta["nodata"] == -999
71+
72+
with rasterio.open(SMALL_RASTER_PATH) as raster:
73+
raster_data = raster.read()
74+
# Ensure some pixels exist that are greater than the target value
75+
nr_of_pixels_greater_than_target = np.count_nonzero(raster_data > target_value)
76+
assert nr_of_pixels_greater_than_target > 0
77+
78+
replace_condition = "greater_than"
79+
out_image, out_meta = replace_with_nodata(raster, target_value, nodata_value, replace_condition)
80+
81+
assert np.count_nonzero(out_image == nodata_value) == nr_of_pixels_greater_than_target
82+
assert np.count_nonzero(out_image > target_value) == 0
83+
assert out_meta["nodata"] == -999
84+
85+
with rasterio.open(SMALL_RASTER_PATH) as raster:
86+
raster_data = raster.read()
87+
# Ensure some pixels exist that are less than or equal to the target value
88+
nr_of_pixels_less_than_or_equal = np.count_nonzero(raster_data <= target_value)
89+
assert nr_of_pixels_less_than_or_equal > 0
90+
91+
replace_condition = "less_than_or_equal"
92+
out_image, out_meta = replace_with_nodata(raster, target_value, nodata_value, replace_condition)
93+
94+
assert np.count_nonzero(out_image == nodata_value) == nr_of_pixels_less_than_or_equal
95+
assert np.count_nonzero((out_image <= target_value) & (out_image != nodata_value)) == 0
96+
assert out_meta["nodata"] == -999
97+
98+
with rasterio.open(SMALL_RASTER_PATH) as raster:
99+
raster_data = raster.read()
100+
# Ensure some pixels exist that are greater than or equal to the target value
101+
nr_of_pixels_greater_than_or_equal = np.count_nonzero(raster_data >= target_value)
102+
assert nr_of_pixels_greater_than_or_equal > 0
103+
104+
replace_condition = "greater_than_or_equal"
105+
out_image, out_meta = replace_with_nodata(raster, target_value, nodata_value, replace_condition)
106+
107+
assert np.count_nonzero(out_image == nodata_value) == nr_of_pixels_greater_than_or_equal
108+
assert np.count_nonzero(out_image >= target_value) == 0
109+
assert out_meta["nodata"] == -999
110+
111+
41112
def test_unify_raster_nodata():
42113
"""Test that unifying raster nodata for multiple rasters works as expected."""
43114
with rasterio.open(SMALL_RASTER_PATH) as raster:

0 commit comments

Comments
 (0)