mirror of
https://github.com/rasterio/rasterio.git
synced 2025-12-08 17:36:12 +00:00
565 lines
18 KiB
Python
565 lines
18 KiB
Python
import logging
|
|
import sys
|
|
|
|
from collections import namedtuple
|
|
import numpy as np
|
|
import pytest
|
|
from affine import Affine
|
|
from hypothesis import given, assume
|
|
from hypothesis.strategies import floats, integers
|
|
|
|
import rasterio
|
|
from rasterio.errors import RasterioDeprecationWarning, WindowError
|
|
from rasterio.windows import (
|
|
crop, from_bounds, bounds, transform, evaluate, window_index, shape,
|
|
Window, intersect, intersection, get_data_window, union,
|
|
round_window_to_full_blocks)
|
|
|
|
EPS = 1.0e-8
|
|
|
|
# hypothesis inputs: col_off, row_off
|
|
F_OFF = floats(min_value=-1.0e+7, max_value=1.0e+7)
|
|
I_OFF = floats(min_value=-10000000, max_value=10000000)
|
|
|
|
# hypothesis inputs: width, height
|
|
F_LEN = floats(min_value=0, max_value=1.0e+7)
|
|
I_LEN = integers(min_value=0, max_value=1.0e+7)
|
|
|
|
|
|
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
|
|
|
|
|
|
def assert_window_almost_equals(a, b):
|
|
assert np.allclose(a.flatten(), b.flatten(), rtol=1e-3, atol=1e-4)
|
|
|
|
|
|
def test_window_repr():
|
|
assert str(Window(0, 1, 4, 2)) == ('Window(col_off=0, row_off=1, width=4, '
|
|
'height=2)')
|
|
|
|
|
|
@given(col_off=F_OFF, row_off=F_OFF, width=F_LEN, height=F_LEN)
|
|
def test_window_class(col_off, row_off, width, height):
|
|
"""Floating point inputs should not be rounded, and 0 values should not
|
|
raise errors"""
|
|
|
|
window = Window(col_off, row_off, width, height)
|
|
|
|
assert np.allclose(window.col_off, col_off)
|
|
assert np.allclose(window.row_off, row_off)
|
|
assert np.allclose(window.width, width)
|
|
assert np.allclose(window.height, height)
|
|
|
|
|
|
def test_window_class_invalid_inputs():
|
|
"""width or height < 0 should raise error"""
|
|
|
|
with pytest.raises(ValueError):
|
|
Window(0, 0, -2, 10)
|
|
|
|
with pytest.raises(ValueError):
|
|
Window(0, 0, 10, -2)
|
|
|
|
|
|
@given(col_off=F_OFF, row_off=F_OFF, width=F_LEN, height=F_LEN)
|
|
def test_window_flatten(col_off, row_off, width, height):
|
|
"""Flattened window should match inputs"""
|
|
|
|
assert np.allclose(
|
|
Window(col_off, row_off, width, height).flatten(),
|
|
(col_off, row_off, width, height))
|
|
|
|
|
|
@given(col_off=F_OFF, row_off=F_OFF, width=F_LEN, height=F_LEN)
|
|
def test_window_todict(col_off, row_off, width, height):
|
|
"""Dictionary of window should match inputs"""
|
|
|
|
d = Window(col_off, row_off, width, height).todict()
|
|
|
|
assert np.allclose(
|
|
(d['col_off'], d['row_off'], d['width'], d['height']),
|
|
(col_off, row_off, width, height))
|
|
|
|
|
|
@given(col_off=F_OFF, row_off=F_OFF, width=F_LEN, height=F_LEN)
|
|
def test_window_toranges(col_off, row_off, width, height):
|
|
"""window.toranges() should match inputs"""
|
|
|
|
assert np.allclose(
|
|
Window(col_off, row_off, width, height).toranges(),
|
|
((row_off, row_off + height), (col_off, col_off + width)))
|
|
|
|
|
|
@given(col_off=F_OFF, row_off=F_OFF, width=F_LEN, height=F_LEN)
|
|
def test_window_toslices(col_off, row_off, width, height):
|
|
"""window.toslices() should match inputs"""
|
|
|
|
expected_slices = (slice(row_off, row_off + height),
|
|
slice(col_off, col_off + width))
|
|
|
|
slices = Window(col_off, row_off, width, height).toslices()
|
|
|
|
assert np.allclose(
|
|
[(s.start, s.stop) for s in slices],
|
|
[(s.start, s.stop) for s in expected_slices]
|
|
)
|
|
|
|
|
|
@given(col_off=F_LEN, row_off=F_LEN, col_stop=F_LEN, row_stop=F_LEN)
|
|
def test_window_fromslices(col_off, row_off, col_stop, row_stop):
|
|
"""Empty and non-empty absolute windows from slices, tuples, or lists
|
|
are valid"""
|
|
|
|
# Constrain windows to >= 0 in each dimension
|
|
assume(col_stop >= col_off)
|
|
assume(row_stop >= row_off)
|
|
|
|
rows = (row_off, row_stop)
|
|
cols = (col_off, col_stop)
|
|
expected = (col_off, row_off, col_stop - col_off, row_stop - row_off)
|
|
|
|
assert np.allclose(
|
|
Window.from_slices(rows=slice(*rows), cols=slice(*cols)).flatten(),
|
|
expected
|
|
)
|
|
|
|
assert np.allclose(
|
|
Window.from_slices(rows=rows, cols=cols).flatten(),
|
|
expected
|
|
)
|
|
|
|
assert np.allclose(
|
|
Window.from_slices(rows=list(rows), cols=list(cols)).flatten(),
|
|
expected
|
|
)
|
|
|
|
|
|
def test_window_fromslices_invalid_rows_cols():
|
|
"""Should raise error if rows or cols are not slices, lists, or tuples
|
|
of length 2"""
|
|
|
|
invalids = (
|
|
np.array([0, 4]), # wrong type, but close
|
|
'04', # clearly the wrong type but right length
|
|
(1, 2, 3) # wrong length
|
|
)
|
|
|
|
for invalid in invalids:
|
|
with pytest.raises(WindowError):
|
|
Window.from_slices(rows=invalid, cols=(0, 4))
|
|
|
|
with pytest.raises(WindowError):
|
|
Window.from_slices(rows=(0, 4), cols=invalid)
|
|
|
|
|
|
def test_window_fromslices_stops_lt_starts():
|
|
"""Should produce empty windows if stop indexes are less than start
|
|
indexes"""
|
|
|
|
assert np.allclose(
|
|
Window.from_slices(rows=(4, 2), cols=(0, 4)).flatten(),
|
|
(0, 4, 4, 0)
|
|
)
|
|
|
|
assert np.allclose(
|
|
Window.from_slices(rows=(0, 4), cols=(4, 2)).flatten(),
|
|
(4, 0, 0, 4)
|
|
)
|
|
|
|
|
|
@given(abs_off=F_LEN, imp_off=F_LEN, stop=F_LEN, dim=F_LEN)
|
|
def test_window_fromslices_implicit(abs_off, imp_off, stop, dim):
|
|
""" providing None for start index will default to 0
|
|
and providing None for stop index will default to width or height """
|
|
|
|
assume(stop >= abs_off)
|
|
assume(dim >= imp_off)
|
|
|
|
absolute = (abs_off, stop)
|
|
implicit_start = (None, stop) # => (0, stop)
|
|
implicit_stop = (imp_off, None) # => (implicit_offset, dim)
|
|
implicit_both = (None, None) # => (implicit_offset, dim)
|
|
|
|
# Implicit start indexes resolve to 0
|
|
assert np.allclose(
|
|
Window.from_slices(rows=implicit_start, cols=absolute).flatten(),
|
|
(abs_off, 0, stop - abs_off, stop)
|
|
)
|
|
|
|
assert np.allclose(
|
|
Window.from_slices(rows=absolute, cols=implicit_start).flatten(),
|
|
(0, abs_off, stop, stop - abs_off)
|
|
)
|
|
|
|
# Implicit stop indexes resolve to dim (height or width)
|
|
assert np.allclose(
|
|
Window.from_slices(
|
|
rows=implicit_stop, cols=absolute, height=dim).flatten(),
|
|
(abs_off, imp_off, stop - abs_off, dim - imp_off)
|
|
)
|
|
|
|
assert np.allclose(
|
|
Window.from_slices(
|
|
rows=absolute, cols=implicit_stop, width=dim).flatten(),
|
|
(imp_off, abs_off, dim - imp_off, stop - abs_off)
|
|
)
|
|
|
|
# Both can be implicit
|
|
assert np.allclose(
|
|
Window.from_slices(
|
|
rows=implicit_both, cols=implicit_both,
|
|
width=dim, height=dim).flatten(),
|
|
(0, 0, dim, dim)
|
|
)
|
|
|
|
|
|
def test_window_fromslices_implicit_err():
|
|
""" height and width are required if stop index is None; failing to
|
|
provide them will result in error"""
|
|
|
|
with pytest.raises(WindowError):
|
|
Window.from_slices(rows=(1, None), cols=(1, 4))
|
|
|
|
with pytest.raises(WindowError):
|
|
Window.from_slices(rows=(1, 4), cols=(1, None))
|
|
|
|
|
|
def test_window_fromslices_negative_start():
|
|
# TODO: if passing negative start, what are valid values for stop?
|
|
assert np.allclose(
|
|
Window.from_slices(rows=(-4, None), cols=(0, 4), height=10).flatten(),
|
|
(0, 6, 4, 4)
|
|
)
|
|
|
|
assert np.allclose(
|
|
Window.from_slices(rows=(0, 4), cols=(-4, None), width=10).flatten(),
|
|
(6, 0, 4, 4)
|
|
)
|
|
|
|
assert np.allclose(
|
|
Window.from_slices(rows=(-6, None), cols=(-4, None),
|
|
height=8, width=10).flatten(),
|
|
(6, 2, 4, 6)
|
|
)
|
|
|
|
|
|
def test_window_fromslices_negative_start_missing_dim_err():
|
|
"""Should raise error if width or height are not provided"""
|
|
|
|
with pytest.raises(WindowError):
|
|
Window.from_slices(rows=(-10, 4), cols=(0, 4))
|
|
|
|
with pytest.raises(WindowError):
|
|
Window.from_slices(rows=(0, 4), cols=(-10, 4))
|
|
|
|
|
|
def test_window_fromslices_negative_stop():
|
|
# TODO: Should negative stops even allowed?? Limited to boundless case?
|
|
assert np.allclose(
|
|
Window.from_slices(rows=(-4, -1), cols=(0, 4), height=10).flatten(),
|
|
(0, 6, 4, 3)
|
|
)
|
|
|
|
assert np.allclose(
|
|
Window.from_slices(rows=(0, 4), cols=(-4, -1), width=10).flatten(),
|
|
(6, 0, 3, 4)
|
|
)
|
|
|
|
|
|
@given(col_off=F_LEN, row_off=F_LEN, col_stop=F_LEN, row_stop=F_LEN)
|
|
def test_window_fromslices_boundless(col_off, row_off, col_stop, row_stop):
|
|
|
|
# Constrain windows to >= 0 in each dimension
|
|
assume(col_stop >= col_off)
|
|
assume(row_stop >= row_off)
|
|
|
|
assert np.allclose(
|
|
Window.from_slices(
|
|
rows=(-row_off, row_stop), cols=(col_off, col_stop),
|
|
boundless=True).flatten(),
|
|
(col_off, -row_off, col_stop - col_off, row_stop + row_off)
|
|
)
|
|
|
|
|
|
@given(col_off=F_OFF, row_off=F_OFF, num_cols=F_LEN, num_rows=F_LEN,
|
|
height=I_LEN, width=I_LEN)
|
|
def test_crop(col_off, row_off, num_cols, num_rows, height, width):
|
|
|
|
window = Window(col_off, row_off, num_cols, num_rows)
|
|
cropped_window = crop(window, height, width)
|
|
|
|
assert 0.0 <= round(cropped_window.col_off, 3) <= width
|
|
assert 0.0 <= round(cropped_window.row_off, 3) <= height
|
|
assert round(cropped_window.width, 3) <= round(width - cropped_window.col_off, 3)
|
|
assert round(cropped_window.height, 3) <= round(height - cropped_window.row_off, 3)
|
|
|
|
|
|
def test_window_from_bounds(path_rgb_byte_tif):
|
|
# TODO: break this test up.
|
|
with rasterio.open(path_rgb_byte_tif) as src:
|
|
left, bottom, right, top = src.bounds
|
|
dx, dy = src.res
|
|
height = src.height
|
|
width = src.width
|
|
|
|
assert_window_almost_equals(from_bounds(
|
|
left + EPS, bottom + EPS, right - EPS, top - EPS, src.transform,
|
|
height, width), Window.from_slices((0, height), (0, width)))
|
|
|
|
assert_window_almost_equals(from_bounds(
|
|
left, top - 2 * dy - EPS, left + 2 * dx - EPS, top, src.transform,
|
|
height, width), Window.from_slices((0, 2), (0, 2)))
|
|
|
|
# boundless
|
|
assert_window_almost_equals(
|
|
from_bounds(left - 2 * dx, top - 2 * dy, left + 2 * dx,
|
|
top + 2 * dy, src.transform, height=height,
|
|
width=width),
|
|
Window.from_slices((-2, 2), (-2, 2), boundless=True, height=height,
|
|
width=width))
|
|
|
|
|
|
def test_window_float(path_rgb_byte_tif):
|
|
"""Test window float values"""
|
|
with rasterio.open(path_rgb_byte_tif) as src:
|
|
left, bottom, right, top = src.bounds
|
|
dx, dy = src.res
|
|
height = src.height
|
|
width = src.width
|
|
|
|
assert_window_almost_equals(from_bounds(
|
|
left, top - 400, left + 400, top, src.transform,
|
|
height, width), Window.from_slices((0, 400 / src.res[1]), (0, 400 / src.res[0])))
|
|
|
|
|
|
def test_window_bounds_south_up():
|
|
identity = Affine.identity()
|
|
assert_window_almost_equals(
|
|
from_bounds(0, 10, 10, 0, identity, 10, 10),
|
|
Window(0, 0, 10, 10))
|
|
|
|
|
|
def test_window_bounds_north_up():
|
|
transform = Affine.translation(0.0, 10.0) * Affine.scale(1.0, -1.0) * Affine.identity()
|
|
assert_window_almost_equals(
|
|
from_bounds(0, 0, 10, 10, transform, 10, 10),
|
|
Window(0, 0, 10, 10))
|
|
|
|
|
|
def test_window_transform_function(path_rgb_byte_tif):
|
|
with rasterio.open(path_rgb_byte_tif) as src:
|
|
assert transform(((0, None), (0, None)), src.transform) == src.transform
|
|
assert transform(((None, None), (None, None)), src.transform) == src.transform
|
|
assert transform(
|
|
((1, None), (1, None)), src.transform).c == src.bounds.left + src.res[0]
|
|
assert transform(
|
|
((1, None), (1, None)), src.transform).f == src.bounds.top - src.res[1]
|
|
assert transform(
|
|
((-1, None), (-1, None)), src.transform).c == src.bounds.left - src.res[0]
|
|
assert transform(
|
|
((-1, None), (-1, None)), src.transform).f == src.bounds.top + src.res[1]
|
|
assert transform(
|
|
Window(-1, -1, src.width + 1, src.height + 1),
|
|
src.transform).f == src.bounds.top + src.res[1]
|
|
|
|
|
|
def test_window_bounds_function(path_rgb_byte_tif):
|
|
with rasterio.open(path_rgb_byte_tif) as src:
|
|
rows = src.height
|
|
cols = src.width
|
|
assert bounds(((0, rows), (0, cols)), src.transform) == src.bounds
|
|
|
|
|
|
bad_type_windows = [
|
|
(1, 2),
|
|
((1, 0), 2)]
|
|
|
|
|
|
@pytest.mark.parametrize("window", bad_type_windows)
|
|
def test_eval_window_bad_type(window):
|
|
with pytest.raises(WindowError):
|
|
evaluate(window, 10, 10)
|
|
|
|
|
|
def test_window_index():
|
|
idx = window_index(((0, 4), (1, 12)))
|
|
assert len(idx) == 2
|
|
r, c = idx
|
|
assert r.start == 0
|
|
assert r.stop == 4
|
|
assert c.start == 1
|
|
assert c.stop == 12
|
|
arr = np.ones((20, 20))
|
|
assert arr[idx].shape == (4, 11)
|
|
|
|
|
|
def test_window_shape_errors():
|
|
# Positive height and width are needed when stop is None.
|
|
with pytest.raises(WindowError):
|
|
assert shape(((10, 20), (10, None)))
|
|
|
|
|
|
def test_window_shape_None_start():
|
|
assert shape(((None, 4), (None, 102))) == (4, 102)
|
|
|
|
|
|
def test_shape_None_stop():
|
|
assert shape(((10, None), (10, None)), 100, 90) == (90, 80)
|
|
|
|
|
|
def test_shape_positive():
|
|
assert shape(((0, 4), (1, 102))) == (4, 101)
|
|
|
|
|
|
def test_shape_negative_start():
|
|
assert shape(((-10, None), (-10, None)), 100, 90) == (10, 10)
|
|
assert shape(((-1, None), (-1, None)), 100, 90) == (1, 1)
|
|
|
|
|
|
def test_shape_negative_stop():
|
|
assert shape(((None, -1), (None, -1)), 100, 90) == (99, 89)
|
|
|
|
|
|
def test_window_class_intersects():
|
|
"""Windows intersect"""
|
|
assert intersect(Window(0, 0, 10, 10), Window(8, 8, 10, 10))
|
|
|
|
|
|
def test_window_class_intersects_list():
|
|
"""A list of Windows intersect"""
|
|
assert intersect([Window(0, 0, 10, 10), Window(8, 8, 10, 10)])
|
|
|
|
|
|
def test_window_class_nonintersects():
|
|
"""Windows do not intersect"""
|
|
assert not intersect(Window(0, 0, 10, 10), Window(10, 10, 10, 10))
|
|
|
|
|
|
def test_window_from_slices():
|
|
"""from_slices classmethod works."""
|
|
assert Window.from_slices((0, 1), (2, 3)) == Window.from_slices((0, 1), (2, 3))
|
|
|
|
|
|
def test_read_with_window_class(path_rgb_byte_tif):
|
|
"""Reading subset with Window class works"""
|
|
with rasterio.open(path_rgb_byte_tif) as src:
|
|
subset = src.read(1, window=Window(0, 0, 10, 10))
|
|
assert subset.shape == (10, 10)
|
|
|
|
|
|
def test_data_window_invalid_arr_dims():
|
|
"""An array of more than 3 dimensions is invalid."""
|
|
arr = np.ones((3, 3, 3, 3))
|
|
with pytest.raises(WindowError):
|
|
get_data_window(arr)
|
|
|
|
|
|
def test_data_window_full():
|
|
"""Get window of entirely valid data array."""
|
|
arr = np.ones((3, 3))
|
|
window = get_data_window(arr)
|
|
assert window == Window.from_slices((0, 3), (0, 3))
|
|
|
|
|
|
def test_data_window_nodata():
|
|
"""Get window of arr with nodata."""
|
|
arr = np.ones((3, 3))
|
|
arr[0, :] = 0
|
|
window = get_data_window(arr, nodata=0)
|
|
assert window == Window.from_slices((1, 3), (0, 3))
|
|
|
|
|
|
def test_data_window_novalid():
|
|
"""Get window of arr with nodata."""
|
|
arr = np.ones((3, 3))
|
|
arr[:, :] = 0
|
|
window = get_data_window(arr, nodata=0)
|
|
assert window == Window.from_slices((0, 0), (0, 0))
|
|
|
|
|
|
def test_data_window_maskedarray():
|
|
"""Get window of masked arr."""
|
|
arr = np.ones((3, 3))
|
|
arr[0, :] = 0
|
|
arr = np.ma.masked_array(arr, arr == 0)
|
|
window = get_data_window(arr)
|
|
assert window == Window.from_slices((1, 3), (0, 3))
|
|
|
|
|
|
def test_data_window_nodata_3d():
|
|
"""Get window of 3d arr with nodata."""
|
|
arr = np.ones((3, 3, 3))
|
|
arr[:, 0, :] = 0
|
|
window = get_data_window(arr, nodata=0)
|
|
assert window == Window.from_slices((1, 3), (0, 3))
|
|
|
|
|
|
def test_window_union():
|
|
"""Window union works."""
|
|
window = union(Window(0, 0, 1, 1), Window(1, 1, 2, 2))
|
|
assert window == Window.from_slices((0, 3), (0, 3))
|
|
|
|
|
|
def test_no_intersection():
|
|
"""Non intersecting windows raises error."""
|
|
with pytest.raises(WindowError):
|
|
intersection(Window(0, 0, 1, 1), Window(1, 1, 2, 2))
|
|
|
|
|
|
def test_intersection():
|
|
"""Window intersection works."""
|
|
window = intersection(Window(0, 0, 10, 10), Window(8, 8, 12, 12))
|
|
assert window == Window.from_slices((8, 10), (8, 10))
|
|
|
|
|
|
def test_round_window_to_full_blocks(path_alpha_tif):
|
|
with rasterio.open(path_alpha_tif) as src:
|
|
block_shapes = src.block_shapes
|
|
test_window = ((321, 548), (432, 765))
|
|
rounded_window = round_window_to_full_blocks(test_window, block_shapes)
|
|
block_shape = block_shapes[0]
|
|
height_shape = block_shape[0]
|
|
width_shape = block_shape[1]
|
|
assert rounded_window.row_off % height_shape == 0
|
|
assert rounded_window.height % height_shape == 0
|
|
assert rounded_window.col_off % width_shape == 0
|
|
assert rounded_window.width % width_shape == 0
|
|
|
|
|
|
def test_round_window_to_full_blocks_error():
|
|
with pytest.raises(WindowError):
|
|
round_window_to_full_blocks(
|
|
Window(0, 0, 10, 10), block_shapes=[(1, 1), (2, 2)])
|
|
|
|
|
|
def test_round_window_already_at_edge(path_alpha_tif):
|
|
with rasterio.open(path_alpha_tif) as src:
|
|
block_shapes = src.block_shapes
|
|
test_window = ((256, 512), (512, 768))
|
|
rounded_window = round_window_to_full_blocks(test_window, block_shapes)
|
|
assert rounded_window == Window.from_slices(*test_window)
|
|
|
|
|
|
def test_round_window_boundless(path_alpha_tif):
|
|
with rasterio.open(path_alpha_tif) as src:
|
|
block_shapes = src.block_shapes
|
|
test_window = ((256, 512), (1000, 1500))
|
|
rounded_window = round_window_to_full_blocks(test_window, block_shapes)
|
|
block_shape = block_shapes[0]
|
|
height_shape = block_shape[0]
|
|
width_shape = block_shape[1]
|
|
assert rounded_window.row_off % height_shape == 0
|
|
assert rounded_window.height % height_shape == 0
|
|
assert rounded_window.col_off % width_shape == 0
|
|
assert rounded_window.width % width_shape == 0
|
|
|
|
|
|
def test_round_lengths_no_op_error():
|
|
with pytest.raises(WindowError):
|
|
Window(0, 0, 1, 1).round_lengths(op='lolwut')
|
|
|
|
|
|
def test_round_offsets_no_op_error():
|
|
with pytest.raises(WindowError):
|
|
Window(0, 0, 1, 1).round_offsets(op='lolwut')
|