Skip to content

Commit b345546

Browse files
Add rgb_range percentile support
1 parent 1a60e00 commit b345546

File tree

3 files changed

+85
-10
lines changed

3 files changed

+85
-10
lines changed

terracotta/handlers/rgb.py

+21-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
from terracotta.profile import trace
1212

1313
Number = TypeVar("Number", int, float)
14-
ListOfRanges = Sequence[Optional[Tuple[Optional[Number], Optional[Number]]]]
14+
NumberOrString = TypeVar("NumberOrString", int, float, str)
15+
ListOfRanges = Sequence[Optional[Tuple[Optional[NumberOrString], Optional[NumberOrString]]]]
1516

1617

1718
@trace("rgb_handler")
@@ -90,10 +91,10 @@ def get_band_future(band_key: str) -> Future:
9091
scale_min, scale_max = band_stretch_override
9192

9293
if scale_min is not None:
93-
band_stretch_range[0] = scale_min
94+
band_stretch_range[0] = get_scale(scale_min, metadata)
9495

9596
if scale_max is not None:
96-
band_stretch_range[1] = scale_max
97+
band_stretch_range[1] = get_scale(scale_max, metadata)
9798

9899
if band_stretch_range[1] < band_stretch_range[0]:
99100
raise exceptions.InvalidArgumentsError(
@@ -105,3 +106,20 @@ def get_band_future(band_key: str) -> Future:
105106

106107
out = np.ma.stack(out_arrays, axis=-1)
107108
return image.array_to_png(out)
109+
110+
111+
def get_scale(scale: NumberOrString, metadata) -> Number:
112+
if isinstance(scale, (int, float)):
113+
return scale
114+
if isinstance(scale, str):
115+
# can be a percentile
116+
if scale.startswith("p"):
117+
# TODO check if percentile is in range
118+
percentile = int(scale[1:]) - 1
119+
return metadata["percentiles"][percentile]
120+
121+
# can be a number
122+
return float(scale)
123+
raise exceptions.InvalidArgumentsError(
124+
"Invalid scale value: %s" % scale
125+
)

terracotta/server/rgb.py

+15-6
Original file line numberDiff line numberDiff line change
@@ -29,25 +29,34 @@ class Meta:
2929
g = fields.String(required=True, description="Key value for green band")
3030
b = fields.String(required=True, description="Key value for blue band")
3131
r_range = fields.List(
32-
fields.Number(allow_none=True),
32+
fields.String(allow_none=True, validate=validate.Regexp("^p?(\d*\.)?\d+$")),
3333
validate=validate.Length(equal=2),
3434
example="[0,1]",
3535
missing=None,
36-
description="Stretch range [min, max] to use for red band as JSON array",
36+
description=(
37+
"Stretch range [min, max] to use for red band as JSON array, "
38+
"prefix with `p` for percentile"
39+
),
3740
)
3841
g_range = fields.List(
39-
fields.Number(allow_none=True),
42+
fields.String(allow_none=True, validate=validate.Regexp("^p?(\d*\.)?\d+$")),
4043
validate=validate.Length(equal=2),
4144
example="[0,1]",
4245
missing=None,
43-
description="Stretch range [min, max] to use for green band as JSON array",
46+
description=(
47+
"Stretch range [min, max] to use for red band as JSON array, "
48+
"prefix with `p` for percentile"
49+
),
4450
)
4551
b_range = fields.List(
46-
fields.Number(allow_none=True),
52+
fields.String(allow_none=True, validate=validate.Regexp("^p?(\d*\.)?\d+$")),
4753
validate=validate.Length(equal=2),
4854
example="[0,1]",
4955
missing=None,
50-
description="Stretch range [min, max] to use for blue band as JSON array",
56+
description=(
57+
"Stretch range [min, max] to use for red band as JSON array, "
58+
"prefix with `p` for percentile"
59+
),
5160
)
5261
tile_size = fields.List(
5362
fields.Integer(),

tests/handlers/test_rgb.py

+49-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,10 @@ def test_rgb_lowzoom(use_testdb, raster_file, raster_file_xyz_lowzoom):
7474

7575

7676
@pytest.mark.parametrize(
77-
"stretch_range", [[0, 20000], [10000, 20000], [-50000, 50000], [100, 100]]
77+
"stretch_range", [
78+
[0, 20000], [10000, 20000], [-50000, 50000], [100, 100],
79+
["0", "20000"], ["10000", "20000"], ["-50000", "50000"], ["100", "100"],
80+
]
7881
)
7982
def test_rgb_stretch(stretch_range, use_testdb, testdb, raster_file_xyz):
8083
import terracotta
@@ -106,6 +109,7 @@ def test_rgb_stretch(stretch_range, use_testdb, testdb, raster_file_xyz):
106109
valid_img = img_data[valid_mask]
107110
valid_data = tile_data.compressed()
108111

112+
stretch_range = [float(stretch_range[0]), float(stretch_range[1])]
109113
assert np.all(valid_img[valid_data < stretch_range[0]] == 1)
110114
stretch_range_mask = (valid_data > stretch_range[0]) & (
111115
valid_data < stretch_range[1]
@@ -131,6 +135,50 @@ def test_rgb_invalid_stretch(use_testdb, raster_file_xyz):
131135
)
132136

133137

138+
def test_rgb_percentile_stretch(use_testdb, testdb, raster_file_xyz):
139+
import terracotta
140+
from terracotta.xyz import get_tile_data
141+
from terracotta.handlers import rgb
142+
143+
ds_keys = ["val21", "x", "val22"]
144+
bands = ["val22", "val23", "val24"]
145+
pct_stretch_range = ["p2", "p98"]
146+
147+
raw_img = rgb.rgb(
148+
ds_keys[:2],
149+
bands,
150+
raster_file_xyz,
151+
stretch_ranges=[pct_stretch_range] * 3,
152+
)
153+
img_data = np.asarray(Image.open(raw_img))[..., 0]
154+
155+
# get unstretched data to compare to
156+
driver = terracotta.get_driver(testdb)
157+
158+
with driver.connect():
159+
tile_data = get_tile_data(
160+
driver, ds_keys, tile_xyz=raster_file_xyz, tile_size=img_data.shape
161+
)
162+
band_metadata = driver.get_metadata(ds_keys)
163+
164+
stretch_range = [band_metadata["percentiles"][1], band_metadata["percentiles"][97]]
165+
166+
# filter transparent values
167+
valid_mask = ~tile_data.mask
168+
assert np.all(img_data[~valid_mask] == 0)
169+
170+
valid_img = img_data[valid_mask]
171+
valid_data = tile_data.compressed()
172+
173+
assert np.all(valid_img[valid_data < stretch_range[0]] == 1)
174+
stretch_range_mask = (valid_data > stretch_range[0]) & (
175+
valid_data < stretch_range[1]
176+
)
177+
assert np.all(valid_img[stretch_range_mask] >= 1)
178+
assert np.all(valid_img[stretch_range_mask] <= 255)
179+
assert np.all(valid_img[valid_data > stretch_range[1]] == 255)
180+
181+
134182
def test_rgb_preview(use_testdb):
135183
import terracotta
136184
from terracotta.handlers import rgb

0 commit comments

Comments
 (0)