mirror of
https://github.com/rasterio/rasterio.git
synced 2025-12-08 17:36:12 +00:00
Expanded Window tests, refactored from_slices to simplify logic
This commit is contained in:
parent
132fca0827
commit
a0eb8a97c9
@ -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',
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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():
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user