Skip to content

Commit

Permalink
inital move of windows methods/funcs out of _base to windows
Browse files Browse the repository at this point in the history
  • Loading branch information
perrygeo committed Jun 29, 2016
1 parent 603f818 commit 358e275
Show file tree
Hide file tree
Showing 2 changed files with 337 additions and 2 deletions.
164 changes: 162 additions & 2 deletions rasterio/windows.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
"""Windows and related functions."""
"""Window utilities and related functions.
A window is a 2D ndarray indexer in the form of a tuple:
((row_start, row_stop), (col_start, col_stop))
"""

import functools
import collections
import functools
import math

from affine import Affine


def iter_args(function):
Expand Down Expand Up @@ -90,3 +98,155 @@ def intersect(*windows):
"""
from rasterio._io import windows_intersect
return windows_intersect(windows)


def window(transform, left, bottom, right, top,
height=None, width=None, boundless=False):
"""Returns the window corresponding to the world bounding box.
If boundless is False, window is limited to extent of this dataset."""

window = get_window(left, bottom, right, top, transform)
if boundless:
return window
else:
if None in (height, width):
raise ValueError("Must supply height and width unless boundless")
return crop_window(window, height, width)


def window_transform(transform, window):
"""Returns the affine transform for a dataset window."""
(r, _), (c, _) = window
return transform * Affine.translation(c or 0, r or 0)


def window_bounds(transform, window):
"""Returns the bounds of a window as x_min, y_min, x_max, y_max."""
((row_min, row_max), (col_min, col_max)) = window
x_min, y_min = transform * (col_min, row_max)
x_max, y_max = transform * (col_max, row_min)
return x_min, y_min, x_max, y_max


def crop_window(window, height, width):
"""Returns a window cropped to fall within height and width."""
(r_start, r_stop), (c_start, c_stop) = window
return (
(min(max(r_start, 0), height), max(0, min(r_stop, height))),
(min(max(c_start, 0), width), max(0, min(c_stop, width))))


def eval_window(window, height, width):
"""Evaluates a window tuple that might contain negative values
in the context of a raster height and width."""
try:
r, c = window
assert len(r) == 2
assert len(c) == 2
except (ValueError, TypeError, AssertionError):
raise ValueError("invalid window structure; expecting ints"
"((row_start, row_stop), (col_start, col_stop))")
r_start = r[0] or 0
if r_start < 0:
if height < 0:
raise ValueError("invalid height: %d" % height)
r_start += height
r_stop = r[1] or height
if r_stop < 0:
if height < 0:
raise ValueError("invalid height: %d" % height)
r_stop += height
if not r_stop >= r_start:
raise ValueError(
"invalid window: row range (%d, %d)" % (r_start, r_stop))
c_start = c[0] or 0
if c_start < 0:
if width < 0:
raise ValueError("invalid width: %d" % width)
c_start += width
c_stop = c[1] or width
if c_stop < 0:
if width < 0:
raise ValueError("invalid width: %d" % width)
c_stop += width
if not c_stop >= c_start:
raise ValueError(
"invalid window: col range (%d, %d)" % (c_start, c_stop))
return (r_start, r_stop), (c_start, c_stop)


def get_index(x, y, affine, op=math.floor, precision=6):
"""
Returns the (row, col) index of the pixel containing (x, y) given a
coordinate reference system.
Parameters
----------
x : float
x value in coordinate reference system
y : float
y value in coordinate reference system
affine : tuple
Coefficients mapping pixel coordinates to coordinate reference system.
op : function
Function to convert fractional pixels to whole numbers (floor, ceiling,
round)
precision : int
Decimal places of precision in indexing, as in `round()`.
Returns
-------
row : int
row index
col : int
col index
"""
# Use an epsilon, magnitude determined by the precision parameter
# and sign determined by the op function: positive for floor, negative
# for ceil.
eps = 10.0**-precision * (1.0 - 2.0*op(0.1))
row = int(op((y - eps - affine[5]) / affine[4]))
col = int(op((x + eps - affine[2]) / affine[0]))
return row, col


def get_window(left, bottom, right, top, affine, precision=6):
"""
Returns a window tuple given coordinate bounds and the coordinate reference
system.
Parameters
----------
left : float
Left edge of window
bottom : float
Bottom edge of window
right : float
Right edge of window
top : float
top edge of window
affine : tuple
Coefficients mapping pixel coordinates to coordinate reference system.
precision : int
Decimal places of precision in indexing, as in `round()`.
"""
window_start = get_index(
left, top, affine, op=math.floor, precision=precision)
window_stop = get_index(
right, bottom, affine, op=math.ceil, precision=precision)
window = tuple(zip(window_start, window_stop))
return window


def window_shape(window, height=-1, width=-1):
"""Returns shape of a window.
height and width arguments are optional if there are no negative
values in the window.
"""
(a, b), (c, d) = eval_window(window, height, width)
return b-a, d-c


def window_index(window):
return tuple(slice(*w) for w in window)
175 changes: 175 additions & 0 deletions tests/test_windows.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import rasterio

import numpy as np
import pytest

from rasterio.windows import (
window, window_bounds, window_transform, eval_window, window_index, window_shape)

def test_window_method():
with rasterio.open('tests/data/RGB.byte.tif') as src:
left, bottom, right, top = src.bounds
dx, dy = src.res
eps = 1.0e-8
assert src.window(
left+eps, bottom+eps, right-eps, top-eps) == ((0, src.height),
(0, src.width))
assert src.window(left, top-400, left+400, top) == ((0, 2), (0, 2))
assert src.window(left, top-2*dy-eps, left+2*dx-eps, top) == ((0, 2), (0, 2))
# bounds cropped
assert src.window(left-2*dx, top-2*dy, left+2*dx, top+2*dy) == ((0, 2), (0, 2))
# boundless
assert src.window(left-2*dx, top-2*dy,
left+2*dx, top+2*dy, boundless=True) == ((-2, 2), (-2, 2))

def test_window_function():
with rasterio.open('tests/data/RGB.byte.tif') as src:
left, bottom, right, top = src.bounds
dx, dy = src.res
height = src.height
width = src.width
transform = src.affine # TODO deprecate
eps = 1.0e-8
assert window(
transform, left+eps, bottom+eps,
right-eps, top-eps, height, width) == ((0, height), (0, width))
assert window(transform, left, top-400,
left+400, top, height, width) == ((0, 2), (0, 2))
assert window(transform, left, top-2*dy-eps,
left+2*dx-eps, top, height, width) == ((0, 2), (0, 2))
# bounds cropped
assert window(transform, left-2*dx, top-2*dy,
left+2*dx, top+2*dy, height, width) == ((0, 2), (0, 2))
# boundless
assert window(transform, left-2*dx, top-2*dy,
left+2*dx, top+2*dy, boundless=True) == ((-2, 2), (-2, 2))


def test_window_function_valuerror():
with rasterio.open('tests/data/RGB.byte.tif') as src:
left, bottom, right, top = src.bounds
transform = src.affine # TODO deprecate
eps = 1.0e-8

with pytest.raises(ValueError):
# No height or width
assert window(
transform, left+eps, bottom+eps,
right-eps, top-eps) == ((0, height), (0, width))


def test_window_transform_method():
with rasterio.open('tests/data/RGB.byte.tif') as src:
assert src.window_transform(((0, None), (0, None))) == src.affine
assert src.window_transform(((None, None), (None, None))) == src.affine
assert src.window_transform(
((1, None), (1, None))).c == src.bounds.left + src.res[0]
assert src.window_transform(
((1, None), (1, None))).f == src.bounds.top - src.res[1]
assert src.window_transform(
((-1, None), (-1, None))).c == src.bounds.left - src.res[0]
assert src.window_transform(
((-1, None), (-1, None))).f == src.bounds.top + src.res[1]


def test_window_transform_function():
with rasterio.open('tests/data/RGB.byte.tif') as src:
transform = src.affine # TODO transform
assert window_transform(transform, ((0, None), (0, None))) == src.affine
assert window_transform(transform, ((None, None), (None, None))) == src.affine
assert window_transform(
transform, ((1, None), (1, None))).c == src.bounds.left + src.res[0]
assert window_transform(
transform, ((1, None), (1, None))).f == src.bounds.top - src.res[1]
assert window_transform(
transform, ((-1, None), (-1, None))).c == src.bounds.left - src.res[0]
assert window_transform(
transform, ((-1, None), (-1, None))).f == src.bounds.top + src.res[1]



def test_window_bounds_method():
with rasterio.open('tests/data/RGB.byte.tif') as src:
rows = src.height
cols = src.width
assert src.window_bounds(((0, rows), (0, cols))) == src.bounds


def test_window_bounds_function():
with rasterio.open('tests/data/RGB.byte.tif') as src:
rows = src.height
cols = src.width
transform = src.affine # todo deprecate
assert window_bounds(transform, ((0, rows), (0, cols))) == src.bounds


def test_eval_window_bad_structure():
with pytest.raises(ValueError):
eval_window((1, 2, 3), 10, 10)
with pytest.raises(ValueError):
eval_window((1, 2), 10, 10)
with pytest.raises(ValueError):
eval_window(((1, 0), 2), 10, 10)


def test_eval_window_invalid_dims():
with pytest.raises(ValueError):
eval_window(((-1, 10), (0, 10)), -1, 10)
with pytest.raises(ValueError):
eval_window(((1, -1), (0, 10)), -1, 10)
with pytest.raises(ValueError):
eval_window(((0, 10), (-1, 10)), 10, -1)
with pytest.raises(ValueError):
eval_window(((0, 10), (1, -1)), 10, -1)
with pytest.raises(ValueError):
eval_window(((10, 5), (0, 5)), 10, 10)
with pytest.raises(ValueError):
eval_window(((0, 5), (10, 5)), 10, 10)


def test_eval_window():
eval_window(((2, 4), (2, 4)), 10, 10) == ((2, 4), (2, 4))
# TODO check logic of eval_window


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)


# TODO remove redundant tests from test_blocks, test_indexing, test_transform
# TODO remove window_index, window_shape and eval_window from top level

def test_window_shape_errors():
# Positive height and width are needed when stop is None.
with pytest.raises(ValueError):
window_shape(((10, 20), (10, None)))

def test_window_shape_errors2():
with pytest.raises(ValueError):
window_shape(((-1, 10), (10, 20)))

def test_window_shape_None_start():
assert window_shape(((None, 4), (None, 102))) == (4, 102)

def test_window_shape_None_stop():
assert window_shape(((10, None), (10, None)), 100, 90) == (90, 80)

def test_window_shape_positive():
assert window_shape(((0, 4), (1, 102))) == (4, 101)

def test_window_shape_negative():
assert window_shape(((-10, None), (-10, None)), 100, 90) == (10, 10)
assert window_shape(((~0, None), (~0, None)), 100, 90) == (1, 1)
assert window_shape(((None, ~0), (None, ~0)), 100, 90) == (99, 89)

def test_eval():
assert eval_window(((-10, None), (-10, None)), 100, 90) == ((90, 100), (80, 90))
assert eval_window(((None, -10), (None, -10)), 100, 90) == ((0, 90), (0, 80))

0 comments on commit 358e275

Please sign in to comment.