Expanded Window tests, refactored from_slices to simplify logic

This commit is contained in:
Brendan Ward 2017-11-26 16:08:16 -08:00
parent 132fca0827
commit a0eb8a97c9
3 changed files with 271 additions and 58 deletions

View File

@ -98,7 +98,7 @@ Writing
=======
Writing works similarly. The following creates a blank 500 column x 300 row
GeoTIFF and plops 37500 pixels with value 127 into a window 30 pixels down from
GeoTIFF and plops 37,500 pixels with value 127 into a window 30 pixels down from
and 50 pixels to the right of the upper left corner of the GeoTIFF.
.. code-block:: python
@ -127,8 +127,10 @@ Below, the window is scaled to one third of the source image.
with rasterio.open('tests/data/RGB.byte.tif') as src:
b, g, r = (src.read(k) for k in (1, 2, 3))
# src.height = 718, src.width = 791
write_window = Window.from_slices((30, 269), (50, 313))
# write_window.height = 239, write_window.width = 263
with rasterio.open(
'/tmp/example.tif', 'w',

View File

@ -379,7 +379,7 @@ def evaluate(window, height, width, boundless=False):
Parameters
----------
window: Window.
window: Window or tuple of (rows, cols).
The input window.
height, width: int
The number of rows or columns in the array that the window
@ -393,8 +393,9 @@ def evaluate(window, height, width, boundless=False):
if isinstance(window, Window):
return window
else:
return Window.from_slices(window[0], window[1], height=height, width=width,
boundless=boundless)
rows, cols = window
return Window.from_slices(rows=rows, cols=cols, height=height,
width=width, boundless=boundless)
def shape(window, height=-1, width=-1):
@ -583,71 +584,93 @@ class Window(object):
@classmethod
def from_slices(cls, rows, cols, height=-1, width=-1, boundless=False):
"""Construct a Window from row and column slices or tuples.
"""Construct a Window from row and column slices or tuples / lists of
start and stop indexes. Converts the rows and cols to offsets, height,
and width.
In general, indexes are defined relative to the upper left corner of
the dataset: rows=(0, 10), cols=(0, 4) defines a window that is 4
columns wide and 10 rows high starting from the upper left.
Start indexes may be `None` and will default to 0.
Stop indexes may be `None` and will default to width or height, which
must be provided in this case.
Negative start indexes are evaluated relative to the lower right of the
dataset: rows=(-2, None), cols=(-2, None) defines a window that is 2
rows high and 2 columns wide starting from the bottom right.
Parameters
----------
rows, cols: slice or tuple
Slices or 2-tuples containing start, stop indexes.
rows, cols: slice, tuple, or list
Slices or 2 element tuples/lists containing start, stop indexes.
height, width: float
A shape to resolve relative values against.
A shape to resolve relative values against. Only used when a start
or stop index is negative or a stop index is None.
boundless: bool, optional
Whether the inputs are bounded or bot.
Whether the inputs are bounded (default) or not.
Returns
-------
Window
"""
# Convert the rows indexing obj to offset and height.
# Normalize to slices
if not isinstance(rows, (tuple, slice)):
raise WindowError("rows must be a tuple or slice")
else:
rows = slice(*rows) if isinstance(rows, tuple) else rows
if isinstance(rows, (tuple, list)):
if len(rows) != 2:
raise WindowError("rows must have a start and stop index")
rows = slice(*rows)
# Resolve the window height.
# Fail if the stop value is relative or implicit and there
# is no height context.
if not boundless and (
(rows.start is not None and rows.start < 0) or
rows.stop is None or rows.stop < 0) and height < 0:
raise WindowError(
"A non-negative height is required")
elif not isinstance(rows, slice):
raise WindowError("rows must be a slice, tuple, or list")
row_off = rows.start or 0.0
if not boundless and row_off < 0:
row_off += height
if isinstance(cols, (tuple, list)):
if len(cols) != 2:
raise WindowError("cols must have a start and stop index")
cols = slice(*cols)
elif not isinstance(cols, slice):
raise WindowError("cols must be a slice, tuple, or list")
# Height and width are required if stop indices are implicit
if rows.stop is None and height < 0:
raise WindowError("height is required if row stop index is None")
if cols.stop is None and width < 0:
raise WindowError("width is required if col stop index is None")
# Convert implicit indices to offsets, height, and width
row_off = 0.0 if rows.start is None else rows.start
row_stop = height if rows.stop is None else rows.stop
if not boundless and row_stop < 0:
row_stop += height
num_rows = row_stop - row_off
# Number of rows is never less than 0.
num_rows = max(num_rows, 0.0)
# Do the same for the cols indexing object.
if not isinstance(cols, (tuple, slice)):
raise WindowError("cols must be a tuple or slice")
else:
cols = slice(*cols) if isinstance(cols, tuple) else cols
if not boundless and (
(cols.start is not None and cols.start < 0) or
cols.stop is None or cols.stop < 0) and width < 0:
raise WindowError("A non-negative width is required")
col_off = cols.start or 0.0
if not boundless and col_off < 0:
col_off += width
col_off = 0.0 if cols.start is None else cols.start
col_stop = width if cols.stop is None else cols.stop
if not boundless and col_stop < 0:
col_stop += width
num_cols = col_stop - col_off
num_cols = max(num_cols, 0.0)
if not boundless:
if (row_off < 0 or row_stop < 0):
if height < 0:
raise WindowError("height is required when providing "
"negative indexes")
if row_off < 0:
row_off += height
if row_stop < 0:
row_stop += height
if (col_off < 0 or col_stop < 0):
if width < 0:
raise WindowError("width is required when providing "
"negative indexes")
if col_off < 0:
col_off += width
if col_stop < 0:
col_stop += width
num_cols = max(col_stop - col_off, 0.0)
num_rows = max(row_stop - row_off, 0.0)
return cls(col_off=col_off, row_off=row_off, width=num_cols,
height=num_rows)

View File

@ -1,10 +1,11 @@
import logging
import sys
from collections import namedtuple
import numpy as np
import pytest
from affine import Affine
from hypothesis import given
from hypothesis import given, assume
from hypothesis.strategies import floats, integers
import rasterio
@ -52,6 +53,16 @@ def test_window_class(col_off, row_off, width, height):
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"""
@ -96,6 +107,183 @@ def test_window_toslices(col_off, row_off, width, height):
)
@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):
@ -226,13 +414,13 @@ def test_shape_positive():
assert shape(((0, 4), (1, 102))) == (4, 101)
def test_shape_negative():
assert shape(((-10, None), (-10, None)), 100, 90) == (10, 10)
assert shape(((~0, None), (~0, None)), 100, 90) == (1, 1)
def test_shape_negative_start():
assert shape(((None, ~0), (None, ~0)), 100, 90) == (99, 89)
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():