mirror of
https://github.com/rasterio/rasterio.git
synced 2025-12-08 17:36:12 +00:00
Fix last lingering window errors
This commit is contained in:
parent
2c1d7c5377
commit
f0d3eb0bfb
@ -51,6 +51,6 @@ install:
|
||||
- "pip list"
|
||||
script:
|
||||
- "if [[ $TRAVIS_PYTHON_VERSION == 3.5 && $GDALVERSION == 2.1.0 ]]; then python -m pytest --doctest-ignore-import-errors --doctest-glob='*.rst' docs/*.rst -k 'not index and not quickstart and not switch' ; fi"
|
||||
- py.test --cov rasterio --cov-report term-missing
|
||||
- python -Wall -m pytest --cov rasterio --cov-report term-missing
|
||||
after_success:
|
||||
- coveralls || echo "!! intermittent coveralls failure"
|
||||
|
||||
@ -24,7 +24,7 @@ from rasterio.enums import (
|
||||
from rasterio.env import Env
|
||||
from rasterio.errors import (
|
||||
RasterioIOError, CRSError, DriverRegistrationError,
|
||||
NotGeoreferencedWarning)
|
||||
NotGeoreferencedWarning, RasterioDeprecationWarning)
|
||||
from rasterio.profiles import Profile
|
||||
from rasterio.transform import Affine, guard_transform, tastes_like_gdal
|
||||
from rasterio.vfs import parse_path, vsi_path
|
||||
@ -425,7 +425,7 @@ cdef class DatasetBase(object):
|
||||
def __get__(self):
|
||||
warnings.warn(
|
||||
"'mask_flags' is deprecated. Switch to 'mask_flag_enums'",
|
||||
DeprecationWarning,
|
||||
RasterioDeprecationWarning,
|
||||
stacklevel=2)
|
||||
return self._mask_flags
|
||||
|
||||
@ -535,7 +535,7 @@ cdef class DatasetBase(object):
|
||||
col = i * w
|
||||
width = min(w, self.width - col)
|
||||
yield (j, i), windows.Window(
|
||||
col_off=col, row_off=row, num_cols=width, num_rows=height)
|
||||
col_off=col, row_off=row, width=width, height=height)
|
||||
|
||||
property bounds:
|
||||
"""Returns the lower left and upper right bounds of the dataset
|
||||
@ -703,7 +703,7 @@ cdef class DatasetBase(object):
|
||||
"'src.affine' is deprecated. Please switch to "
|
||||
"'src.transform'. See "
|
||||
"https://github.com/mapbox/rasterio/issues/86 for details.",
|
||||
DeprecationWarning,
|
||||
RasterioDeprecationWarning,
|
||||
stacklevel=2)
|
||||
return Affine.from_gdal(*self.get_transform())
|
||||
|
||||
|
||||
168
rasterio/_io.pyx
168
rasterio/_io.pyx
@ -24,11 +24,11 @@ from rasterio import dtypes
|
||||
from rasterio.enums import ColorInterp, MaskFlags, Resampling
|
||||
from rasterio.errors import CRSError, DriverRegistrationError
|
||||
from rasterio.errors import RasterioIOError
|
||||
from rasterio.errors import NodataShadowWarning
|
||||
from rasterio.errors import NodataShadowWarning, WindowError
|
||||
from rasterio.sample import sample_gen
|
||||
from rasterio.transform import Affine
|
||||
from rasterio.vfs import parse_path, vsi_path
|
||||
from rasterio import windows
|
||||
from rasterio.windows import Window, intersection
|
||||
|
||||
from libc.stdio cimport FILE
|
||||
cimport numpy as np
|
||||
@ -121,7 +121,7 @@ cdef class DatasetReaderBase(DatasetBase):
|
||||
|
||||
Cannot combined with `out`.
|
||||
|
||||
window : a pair (tuple) of pairs of ints, optional
|
||||
window : a pair (tuple) of pairs of ints or Window, optional
|
||||
The optional `window` argument is a 2 item tuple. The first
|
||||
item is a tuple containing the indexes of the rows at which
|
||||
the window starts and stops and the second is a tuple
|
||||
@ -225,15 +225,15 @@ cdef class DatasetReaderBase(DatasetBase):
|
||||
if window:
|
||||
|
||||
if isinstance(window, tuple):
|
||||
window = windows.coerce_and_warn(window)
|
||||
window = Window.from_slices(
|
||||
*window, height=self.height, width=self.width,
|
||||
boundless=boundless)
|
||||
|
||||
if not boundless:
|
||||
window = windows.crop(
|
||||
windows.evaluate(window, self.height, self.width),
|
||||
self.height, self.width)
|
||||
window = window.crop(self.height, self.width)
|
||||
|
||||
int_window = window.round_shape()
|
||||
win_shape += (int(int_window.num_rows), int(int_window.num_cols))
|
||||
int_window = window.round_lengths()
|
||||
win_shape += (int(int_window.height), int(int_window.width))
|
||||
|
||||
else:
|
||||
win_shape += self.shape
|
||||
@ -319,19 +319,23 @@ cdef class DatasetReaderBase(DatasetBase):
|
||||
|
||||
else:
|
||||
# Compute the overlap between the dataset and the boundless window.
|
||||
overlap = ((
|
||||
max(min(window[0][0], self.height), 0),
|
||||
max(min(window[0][1], self.height), 0)), (
|
||||
max(min(window[1][0], self.width), 0),
|
||||
max(min(window[1][1], self.width), 0)))
|
||||
|
||||
log.debug("Overlap: %r", overlap)
|
||||
|
||||
if overlap != ((0, 0), (0, 0)):
|
||||
try:
|
||||
overlap = intersection(
|
||||
window, Window(0, 0, self.width, self.height))
|
||||
except WindowError:
|
||||
log.info("Dataset and window do not overlap")
|
||||
data = None
|
||||
if masked:
|
||||
kwds = {'mask': True}
|
||||
if len(set(nodatavals)) == 1:
|
||||
if nodatavals[0] is not None:
|
||||
kwds['fill_value'] = nodatavals[0]
|
||||
out = np.ma.array(out, **kwds)
|
||||
else:
|
||||
# Prepare a buffer.
|
||||
window_h, window_w = win_shape[-2:]
|
||||
overlap_h = overlap[0][1] - overlap[0][0]
|
||||
overlap_w = overlap[1][1] - overlap[1][0]
|
||||
overlap_h = overlap.height
|
||||
overlap_w = overlap.width
|
||||
scaling_h = float(out.shape[-2:][0])/window_h
|
||||
scaling_w = float(out.shape[-2:][1])/window_w
|
||||
buffer_shape = (
|
||||
@ -352,24 +356,15 @@ cdef class DatasetReaderBase(DatasetBase):
|
||||
kwds['fill_value'] = nodatavals[0]
|
||||
data = np.ma.array(data, **kwds)
|
||||
|
||||
else:
|
||||
data = None
|
||||
if masked:
|
||||
kwds = {'mask': True}
|
||||
if len(set(nodatavals)) == 1:
|
||||
if nodatavals[0] is not None:
|
||||
kwds['fill_value'] = nodatavals[0]
|
||||
out = np.ma.array(out, **kwds)
|
||||
|
||||
if data is not None:
|
||||
# Determine where to put the data in the output window.
|
||||
data_h, data_w = buffer_shape
|
||||
roff = 0
|
||||
coff = 0
|
||||
if window[0][0] < 0:
|
||||
roff = int(-window[0][0] * scaling_h)
|
||||
if window[1][0] < 0:
|
||||
coff = int(-window[1][0] * scaling_w)
|
||||
if window.row_off < 0:
|
||||
roff = int(-window.row_off * scaling_h)
|
||||
if window.col_off < 0:
|
||||
coff = int(-window.col_off * scaling_w)
|
||||
|
||||
for dst, src in zip(
|
||||
out if len(out.shape) == 3 else [out],
|
||||
@ -422,7 +417,7 @@ cdef class DatasetReaderBase(DatasetBase):
|
||||
|
||||
Cannot combined with `out`.
|
||||
|
||||
window : a pair (tuple) of pairs of ints, optional
|
||||
window : a pair (tuple) of pairs of ints or Window, optional
|
||||
The optional `window` argument is a 2 item tuple. The first
|
||||
item is a tuple containing the indexes of the rows at which
|
||||
the window starts and stops and the second is a tuple
|
||||
@ -463,15 +458,15 @@ cdef class DatasetReaderBase(DatasetBase):
|
||||
if window:
|
||||
|
||||
if isinstance(window, tuple):
|
||||
window = windows.coerce_and_warn(window)
|
||||
window = Window.from_slices(
|
||||
*window, height=self.height, width=self.width,
|
||||
boundless=boundless)
|
||||
|
||||
if not boundless:
|
||||
window = windows.crop(
|
||||
windows.evaluate(window, self.height, self.width),
|
||||
self.height, self.width)
|
||||
window = window.crop(self.height, self.width)
|
||||
|
||||
int_window = window.round_shape()
|
||||
win_shape += (int(int_window.num_rows), int(int_window.num_cols))
|
||||
int_window = window.round_lengths()
|
||||
win_shape += (int(int_window.height), int(int_window.width))
|
||||
|
||||
else:
|
||||
win_shape += self.shape
|
||||
@ -506,25 +501,23 @@ cdef class DatasetReaderBase(DatasetBase):
|
||||
|
||||
else:
|
||||
# Compute the overlap between the dataset and the boundless window.
|
||||
overlap = ((
|
||||
max(min(window[0][0], self.height), 0),
|
||||
max(min(window[0][1], self.height), 0)), (
|
||||
max(min(window[1][0], self.width), 0),
|
||||
max(min(window[1][1], self.width), 0)))
|
||||
|
||||
if overlap != ((0, 0), (0, 0)):
|
||||
try:
|
||||
overlap = intersection(
|
||||
window, Window(0, 0, self.width, self.height))
|
||||
except WindowError:
|
||||
log.info("Window and dataset do not overlap")
|
||||
data = None
|
||||
else:
|
||||
# Prepare a buffer.
|
||||
window_h, window_w = win_shape[-2:]
|
||||
overlap_h = overlap[0][1] - overlap[0][0]
|
||||
overlap_w = overlap[1][1] - overlap[1][0]
|
||||
overlap_h = overlap.height
|
||||
overlap_w = overlap.width
|
||||
scaling_h = float(out.shape[-2:][0])/window_h
|
||||
scaling_w = float(out.shape[-2:][1])/window_w
|
||||
buffer_shape = (int(overlap_h*scaling_h), int(overlap_w*scaling_w))
|
||||
data = np.zeros(win_shape[:-2] + buffer_shape, 'uint8')
|
||||
data = self._read(indexes, data, overlap, dtype, masks=True,
|
||||
resampling=resampling)
|
||||
else:
|
||||
data = None
|
||||
|
||||
if data is not None:
|
||||
# Determine where to put the data in the output window.
|
||||
@ -575,27 +568,19 @@ cdef class DatasetReaderBase(DatasetBase):
|
||||
|
||||
dataset = self.handle()
|
||||
|
||||
# Turning the read window into GDAL offsets and lengths is
|
||||
# the job of _read().
|
||||
if window:
|
||||
if isinstance(window, tuple):
|
||||
window = windows.Window.from_ranges(*window)
|
||||
|
||||
window = windows.evaluate(window, self.height, self.width)
|
||||
|
||||
log.debug("Eval'd window: %r", window)
|
||||
if not isinstance(window, Window):
|
||||
raise WindowError("window must be an instance of Window")
|
||||
|
||||
yoff = window.row_off
|
||||
xoff = window.col_off
|
||||
height = window.num_rows
|
||||
width = window.num_cols
|
||||
|
||||
# Now that we have floating point windows it's easy for
|
||||
# the number of pixels to read to slip below 1 due to
|
||||
# loss of floating point precision. Here we ensure that
|
||||
# we're reading at least one pixel.
|
||||
height = max(1.0, height)
|
||||
width = max(1.0, width)
|
||||
height = max(1.0, window.height)
|
||||
width = max(1.0, window.width)
|
||||
|
||||
else:
|
||||
xoff = yoff = <int>0
|
||||
@ -685,46 +670,13 @@ cdef class DatasetReaderBase(DatasetBase):
|
||||
"""Read the mask band into an `out` array if provided,
|
||||
otherwise return a new array containing the dataset's
|
||||
valid data mask.
|
||||
|
||||
The optional `window` argument takes a tuple like:
|
||||
|
||||
((row_start, row_stop), (col_start, col_stop))
|
||||
|
||||
specifying a raster subset to write into.
|
||||
"""
|
||||
cdef GDALRasterBandH band
|
||||
cdef GDALRasterBandH mask
|
||||
|
||||
warnings.warn(
|
||||
"read_mask() is deprecated and will be removed by Rasterio 1.0. "
|
||||
"Please use read_masks() instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2)
|
||||
|
||||
band = self.band(1)
|
||||
mask = GDALGetMaskBand(band)
|
||||
if mask == NULL:
|
||||
return None
|
||||
|
||||
if out is None:
|
||||
out_shape = (
|
||||
window
|
||||
and windows.shape(window, self.height, self.width)
|
||||
or self.shape)
|
||||
out = np.zeros(out_shape, np.uint8)
|
||||
if window:
|
||||
window = windows.evaluate(window, self.height, self.width)
|
||||
yoff = window[0][0]
|
||||
xoff = window[1][0]
|
||||
height = window[0][1] - yoff
|
||||
width = window[1][1] - xoff
|
||||
else:
|
||||
xoff = yoff = 0
|
||||
width = self.width
|
||||
height = self.height
|
||||
|
||||
io_band(mask, 0, xoff, yoff, width, height, out)
|
||||
return out
|
||||
return self.read_masks(1, out=out, window=window, boundless=boundless)
|
||||
|
||||
def sample(self, xy, indexes=None):
|
||||
"""Get the values of a dataset at certain positions
|
||||
@ -1315,11 +1267,12 @@ cdef class DatasetWriterBase(DatasetReaderBase):
|
||||
|
||||
# Prepare the IO window.
|
||||
if window:
|
||||
window = windows.evaluate(window, self.height, self.width)
|
||||
yoff = <int>window[0][0]
|
||||
xoff = <int>window[1][0]
|
||||
height = <int>window[0][1] - yoff
|
||||
width = <int>window[1][1] - xoff
|
||||
if isinstance(window, tuple):
|
||||
window = Window.from_slices(*window, self.height, self.width)
|
||||
yoff = window.row_off
|
||||
xoff = window.col_off
|
||||
height = window.height
|
||||
width = window.width
|
||||
else:
|
||||
xoff = yoff = <int>0
|
||||
width = <int>self.width
|
||||
@ -1497,11 +1450,12 @@ cdef class DatasetWriterBase(DatasetReaderBase):
|
||||
raise RasterioIOError("Failed to get mask.")
|
||||
|
||||
if window:
|
||||
window = windows.evaluate(window, self.height, self.width)
|
||||
yoff = window[0][0]
|
||||
xoff = window[1][0]
|
||||
height = window[0][1] - yoff
|
||||
width = window[1][1] - xoff
|
||||
if isinstance(window, tuple):
|
||||
window = Window.from_slices(*window, self.height, self.width)
|
||||
yoff = window.row_off
|
||||
xoff = window.col_off
|
||||
height = window.height
|
||||
width = window.width
|
||||
else:
|
||||
xoff = yoff = 0
|
||||
width = self.width
|
||||
|
||||
@ -3,12 +3,20 @@
|
||||
from click import FileError
|
||||
|
||||
|
||||
class RasterioError(Exception):
|
||||
"""Root exception class"""
|
||||
|
||||
|
||||
class WindowError(RasterioError):
|
||||
"""Raised when errors occur during window operations"""
|
||||
|
||||
|
||||
class CRSError(ValueError):
|
||||
"""Raised when a CRS string or mapping is invalid or cannot serve
|
||||
to define a coordinate transformation."""
|
||||
|
||||
|
||||
class EnvError(Exception):
|
||||
class EnvError(RasterioError):
|
||||
"""Raised when the state of GDAL/AWS environment cannot be created
|
||||
or modified."""
|
||||
|
||||
@ -48,8 +56,15 @@ class GDALBehaviorChangeException(RuntimeError):
|
||||
example, antimeridian cutting is always on as of GDAL 2.2.0. Users
|
||||
expecting it to be off will be presented with a MultiPolygon when the
|
||||
rest of their code expects a Polygon.
|
||||
|
||||
|
||||
# Raises an exception on GDAL >= 2.2.0
|
||||
rasterio.warp.transform_geometry(
|
||||
src_crs, dst_crs, antimeridian_cutting=False)
|
||||
"""
|
||||
|
||||
class WindowEvaluationError(ValueError):
|
||||
"""Raised when window evaluation fails"""
|
||||
|
||||
|
||||
class RasterioDeprecationWarning(UserWarning):
|
||||
"""Rasterio module deprecations"""
|
||||
|
||||
@ -105,7 +105,7 @@ class WindowMethodsMixin(object):
|
||||
properties: `transform`, `height` and `width`
|
||||
"""
|
||||
|
||||
def window(self, left, bottom, right, top, boundless=False, precision=6):
|
||||
def window(self, left, bottom, right, top, boundless=True, precision=6):
|
||||
"""Get the window corresponding to the bounding coordinates.
|
||||
|
||||
Parameters
|
||||
@ -127,10 +127,7 @@ class WindowMethodsMixin(object):
|
||||
|
||||
Returns
|
||||
-------
|
||||
window: tuple
|
||||
((row_start, row_stop), (col_start, col_stop))
|
||||
corresponding to the bounding coordinates
|
||||
|
||||
window: Window
|
||||
"""
|
||||
|
||||
transform = guard_transform(self.transform)
|
||||
|
||||
@ -104,7 +104,7 @@ def mask(raster, shapes, nodata=None, crop=False, all_touched=False,
|
||||
|
||||
# Get the window with integer height
|
||||
# and width that contains the bounds window.
|
||||
out_window = bounds_window.round_shape()
|
||||
out_window = bounds_window.round_shape(op='ceil')
|
||||
height = int(out_window.num_rows)
|
||||
width = int(out_window.num_cols)
|
||||
|
||||
|
||||
@ -155,7 +155,7 @@ def merge(sources, bounds=None, res=None, nodata=None, precision=7):
|
||||
# 4. Initialize temp array.
|
||||
tcount = first.count
|
||||
trows, tcols = (
|
||||
int(round(dst_window.num_rows)), int(round(dst_window.num_cols)))
|
||||
int(round(dst_window.height)), int(round(dst_window.width)))
|
||||
|
||||
temp_shape = (tcount, trows, tcols)
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import warnings
|
||||
|
||||
from rasterio.compat import UserDict
|
||||
from rasterio.dtypes import uint8
|
||||
from rasterio.errors import RasterioDeprecationWarning
|
||||
|
||||
|
||||
class Profile(UserDict):
|
||||
@ -24,11 +25,11 @@ class Profile(UserDict):
|
||||
|
||||
if 'affine' in initdata and 'transform' in initdata:
|
||||
warnings.warn("affine item is deprecated, use transform only",
|
||||
DeprecationWarning)
|
||||
RasterioDeprecationWarning)
|
||||
del initdata['affine']
|
||||
elif 'affine' in initdata:
|
||||
warnings.warn("affine item is deprecated, use transform instead",
|
||||
DeprecationWarning)
|
||||
RasterioDeprecationWarning)
|
||||
initdata['transform'] = initdata.pop('affine')
|
||||
|
||||
self.data.update(initdata)
|
||||
@ -38,7 +39,7 @@ class Profile(UserDict):
|
||||
if key == 'affine':
|
||||
key = 'transform'
|
||||
warnings.warn("affine item is deprecated, use transform instead",
|
||||
DeprecationWarning)
|
||||
RasterioDeprecationWarning)
|
||||
return self.data[key]
|
||||
|
||||
def __setitem__(self, key, val):
|
||||
@ -53,7 +54,7 @@ class Profile(UserDict):
|
||||
DEPRECATED.
|
||||
"""
|
||||
warnings.warn("__call__() is deprecated, use mapping methods instead",
|
||||
DeprecationWarning)
|
||||
RasterioDeprecationWarning)
|
||||
profile = self.data.copy()
|
||||
profile.update(**kwds)
|
||||
return profile
|
||||
|
||||
@ -7,6 +7,7 @@ from .helpers import resolve_inout
|
||||
from . import options
|
||||
import rasterio
|
||||
from rasterio.coords import disjoint_bounds
|
||||
from rasterio.windows import Window
|
||||
|
||||
|
||||
# Geographic (default), projected, or Mercator switch.
|
||||
@ -100,13 +101,15 @@ def clip(ctx, files, output, bounds, like, driver, projection,
|
||||
raise click.UsageError('--bounds or --like required')
|
||||
|
||||
bounds_window = src.window(*bounds)
|
||||
bounds_window = bounds_window.intersection(
|
||||
Window(0, 0, src.width, src.height))
|
||||
|
||||
# Get the window with integer height
|
||||
# and width that contains the bounds window.
|
||||
out_window = bounds_window.round_shape()
|
||||
out_window = bounds_window.round_lengths(op='ceil')
|
||||
|
||||
height = int(out_window.num_rows)
|
||||
width = int(out_window.num_cols)
|
||||
height = int(out_window.height)
|
||||
width = int(out_window.width)
|
||||
|
||||
out_kwargs = src.meta.copy()
|
||||
out_kwargs.update({
|
||||
|
||||
@ -12,7 +12,7 @@ def sample_gen(dataset, xy, indexes=None):
|
||||
indexes = [indexes]
|
||||
|
||||
for x, y in xy:
|
||||
r, c = index(x, y)
|
||||
window = Window(c, r, 1, 1)
|
||||
row_off, col_off = index(x, y)
|
||||
window = Window(col_off, row_off, 1, 1)
|
||||
data = read(indexes, window=window, masked=False, boundless=True)
|
||||
yield data[:, 0, 0]
|
||||
|
||||
@ -1,11 +1,20 @@
|
||||
"""Window utilities and related functions.
|
||||
|
||||
A window is an instance of Window or a 2D ndarray indexer in the form
|
||||
of a tuple:
|
||||
A window is an instance of Window
|
||||
|
||||
Window(column_offset, row_offset, width, height)
|
||||
|
||||
or a 2D N-D array indexer in the form of a tuple.
|
||||
|
||||
((row_start, row_stop), (col_start, col_stop))
|
||||
|
||||
This latter form will be deprecated. Please change your usage.
|
||||
The latter can be evaluated within the context of a given height and
|
||||
width and a boolean flag specifying whether the evaluation is boundless
|
||||
or not. If boundless=True, negative index values do not mean index from
|
||||
the end of the array dimension as they do in the boundless=False case.
|
||||
|
||||
The newer float precision read-write window capabilities of Rasterio
|
||||
require instances of Window to be used.
|
||||
"""
|
||||
|
||||
from __future__ import division
|
||||
@ -19,26 +28,11 @@ import attr
|
||||
from affine import Affine
|
||||
import numpy as np
|
||||
|
||||
from rasterio.errors import RasterioDeprecationWarning, WindowError
|
||||
from rasterio.transform import rowcol
|
||||
|
||||
|
||||
def warn_window_deprecation():
|
||||
"""Standard warning about range tuple deprecation"""
|
||||
warnings.warn(
|
||||
"Range tuple window are deprecated. Please switch to Window class",
|
||||
DeprecationWarning)
|
||||
|
||||
|
||||
def coerce_and_warn(value):
|
||||
"""Coerce ranges to Window and warn"""
|
||||
if isinstance(value, Window):
|
||||
return value
|
||||
else:
|
||||
warn_window_deprecation()
|
||||
if not (len(value) == 2 and len(value[0]) == 2 and
|
||||
len(value[1]) == 2):
|
||||
raise ValueError("Not a valid pair of value")
|
||||
return Window.from_ranges(*value)
|
||||
PIXEL_PRECISION = 6
|
||||
|
||||
|
||||
def iter_args(function):
|
||||
@ -59,7 +53,6 @@ def toranges(window):
|
||||
if isinstance(window, Window):
|
||||
return window.toranges()
|
||||
else:
|
||||
warn_window_deprecation()
|
||||
return window
|
||||
|
||||
|
||||
@ -82,20 +75,21 @@ def get_data_window(arr, nodata=None):
|
||||
|
||||
num_dims = len(arr.shape)
|
||||
if num_dims > 3:
|
||||
raise ValueError('get_data_window input array must have no more than '
|
||||
'3 dimensions')
|
||||
raise WindowError(
|
||||
"get_data_window input array must have no more than "
|
||||
"3 dimensions")
|
||||
|
||||
if nodata is None:
|
||||
if not hasattr(arr, 'mask'):
|
||||
return Window.from_ranges((0, arr.shape[-2]), (0, arr.shape[-1]))
|
||||
return Window.from_slices((0, arr.shape[-2]), (0, arr.shape[-1]))
|
||||
else:
|
||||
arr = np.ma.masked_array(arr, arr == nodata)
|
||||
|
||||
if num_dims == 2:
|
||||
data_rows, data_cols = np.where(arr.mask == False)
|
||||
data_rows, data_cols = np.where(np.equal(arr.mask, False))
|
||||
else:
|
||||
data_rows, data_cols = np.where(
|
||||
np.any(np.rollaxis(arr.mask, 0, 3) == False, axis=2))
|
||||
np.any(np.equal(np.rollaxis(arr.mask, 0, 3), False), axis=2))
|
||||
|
||||
if data_rows.size:
|
||||
row_range = (data_rows.min(), data_rows.max() + 1)
|
||||
@ -107,7 +101,7 @@ def get_data_window(arr, nodata=None):
|
||||
else:
|
||||
col_range = (0, 0)
|
||||
|
||||
return Window.from_ranges(row_range, col_range)
|
||||
return Window.from_slices(row_range, col_range)
|
||||
|
||||
|
||||
@iter_args
|
||||
@ -125,7 +119,7 @@ def union(*windows):
|
||||
Window
|
||||
"""
|
||||
stacked = np.dstack([toranges(w) for w in windows])
|
||||
return Window.from_ranges(
|
||||
return Window.from_slices(
|
||||
(stacked[0, 0].min(), stacked[0, 1].max()),
|
||||
(stacked[1, 0].min(), stacked[1, 1]. max()))
|
||||
|
||||
@ -134,7 +128,7 @@ def union(*windows):
|
||||
def intersection(*windows):
|
||||
"""Innermost extent of window intersections.
|
||||
|
||||
Will raise ValueError if windows do not intersect.
|
||||
Will raise WindowError if windows do not intersect.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@ -146,10 +140,10 @@ def intersection(*windows):
|
||||
Window
|
||||
"""
|
||||
if not intersect(windows):
|
||||
raise ValueError('windows do not intersect')
|
||||
raise WindowError("windows do not intersect")
|
||||
|
||||
stacked = np.dstack([toranges(w) for w in windows])
|
||||
return Window.from_ranges(
|
||||
return Window.from_slices(
|
||||
(stacked[0, 0].max(), stacked[0, 1].min()),
|
||||
(stacked[1, 0].max(), stacked[1, 1]. min()))
|
||||
|
||||
@ -185,7 +179,7 @@ def intersect(*windows):
|
||||
|
||||
|
||||
def from_bounds(left, bottom, right, top, transform=None,
|
||||
height=None, width=None, boundless=False, precision=6):
|
||||
height=None, width=None, precision=6, **kwargs):
|
||||
"""Get the window corresponding to the bounding coordinates.
|
||||
|
||||
Parameters
|
||||
@ -197,9 +191,6 @@ def from_bounds(left, bottom, right, top, transform=None,
|
||||
Affine transform matrix.
|
||||
height, width : int
|
||||
Number of rows and columns of the window.
|
||||
boundless : boolean, optional
|
||||
If True, the output window's size may exceed the given height
|
||||
and width.
|
||||
precision : int, optional
|
||||
Number of decimal points of precision when computing inverse
|
||||
transform.
|
||||
@ -209,20 +200,19 @@ def from_bounds(left, bottom, right, top, transform=None,
|
||||
Window
|
||||
A new Window
|
||||
"""
|
||||
window_start = rowcol(
|
||||
if 'boundless' in kwargs:
|
||||
warnings.warn("boundless keyword should not be used",
|
||||
RasterioDeprecationWarning)
|
||||
|
||||
row_start, col_start = rowcol(
|
||||
transform, left, top, op=float, precision=precision)
|
||||
|
||||
window_stop = rowcol(
|
||||
row_stop, col_stop = rowcol(
|
||||
transform, right, bottom, op=float, precision=precision)
|
||||
|
||||
window = Window.from_ranges(*tuple(zip(window_start, window_stop)))
|
||||
|
||||
if boundless:
|
||||
return window
|
||||
else:
|
||||
if None in (height, width):
|
||||
raise ValueError("Must supply height and width unless boundless")
|
||||
return crop(window, height, width)
|
||||
return Window.from_slices(
|
||||
(row_start, row_stop), (col_start, col_stop), height=height,
|
||||
width=width, boundless=True)
|
||||
|
||||
|
||||
def transform(window, transform):
|
||||
@ -240,14 +230,14 @@ def transform(window, transform):
|
||||
Affine
|
||||
The affine transform matrix for the given window
|
||||
"""
|
||||
window = coerce_and_warn(window)
|
||||
window = evaluate(window, height=0, width=0)
|
||||
|
||||
x, y = transform * (window.col_off or 0.0, window.row_off or 0.0)
|
||||
return Affine.translation(
|
||||
x - transform.c, y - transform.f) * transform
|
||||
|
||||
|
||||
def bounds(window, transform):
|
||||
def bounds(window, transform, height=0, width=0):
|
||||
"""Get the spatial bounds of a window.
|
||||
|
||||
Parameters
|
||||
@ -262,12 +252,12 @@ def bounds(window, transform):
|
||||
left, bottom, right, top: float
|
||||
A tuple of spatial coordinate bounding values.
|
||||
"""
|
||||
window = coerce_and_warn(window)
|
||||
|
||||
window = evaluate(window, height=height, width=width)
|
||||
|
||||
row_min = window.row_off
|
||||
row_max = row_min + window.num_rows
|
||||
row_max = row_min + window.height
|
||||
col_min = window.col_off
|
||||
col_max = col_min + window.num_cols
|
||||
col_max = col_min + window.width
|
||||
|
||||
left, bottom = transform * (col_min, row_max)
|
||||
right, top = transform * (col_max, row_min)
|
||||
@ -289,17 +279,18 @@ def crop(window, height, width):
|
||||
Window
|
||||
A new Window object.
|
||||
"""
|
||||
window = coerce_and_warn(window)
|
||||
window = evaluate(window, height=height, width=width)
|
||||
|
||||
row_start = min(max(window.row_off, 0), height)
|
||||
col_start = min(max(window.col_off, 0), width)
|
||||
row_stop = max(0, min(window.row_off + window.num_rows, height))
|
||||
col_stop = max(0, min(window.col_off + window.num_cols, width))
|
||||
row_stop = max(0, min(window.row_off + window.height, height))
|
||||
col_stop = max(0, min(window.col_off + window.width, width))
|
||||
|
||||
return Window.from_ranges((row_start, row_stop), (col_start, col_stop))
|
||||
|
||||
return Window(col_start, row_start, col_stop - col_start,
|
||||
row_stop - row_start)
|
||||
|
||||
def evaluate(window, height, width):
|
||||
|
||||
def evaluate(window, height, width, boundless=False):
|
||||
"""Evaluates a window tuple that may contain relative index values.
|
||||
|
||||
The height and width of the array the window targets is the context
|
||||
@ -307,9 +298,9 @@ def evaluate(window, height, width):
|
||||
|
||||
Parameters
|
||||
----------
|
||||
window : Window.
|
||||
window: Window.
|
||||
The input window.
|
||||
height, width : int
|
||||
height, width: int
|
||||
The number of rows or columns in the array that the window
|
||||
targets.
|
||||
|
||||
@ -318,43 +309,11 @@ def evaluate(window, height, width):
|
||||
Window
|
||||
A new Window object with absolute index values.
|
||||
"""
|
||||
window = coerce_and_warn(window)
|
||||
|
||||
r, c = window.toranges()
|
||||
|
||||
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 Window.from_ranges((r_start, r_stop), (c_start, c_stop))
|
||||
if isinstance(window, Window):
|
||||
return window
|
||||
else:
|
||||
return Window.from_slices(window[0], window[1], height=height, width=width,
|
||||
boundless=boundless)
|
||||
|
||||
|
||||
def shape(window, height=-1, width=-1):
|
||||
@ -377,10 +336,10 @@ def shape(window, height=-1, width=-1):
|
||||
The number of rows and columns of the window.
|
||||
"""
|
||||
evaluated = evaluate(window, height, width)
|
||||
return evaluated.num_rows, evaluated.num_cols
|
||||
return evaluated.height, evaluated.width
|
||||
|
||||
|
||||
def window_index(window):
|
||||
def window_index(window, height=0, width=0):
|
||||
"""Construct a pair of slice objects for ndarray indexing
|
||||
|
||||
Starting indexes are rounded down, Stopping indexes are rounded up.
|
||||
@ -395,7 +354,7 @@ def window_index(window):
|
||||
row_slice, col_slice: slice
|
||||
A pair of slices in row, column order
|
||||
"""
|
||||
window = coerce_and_warn(window)
|
||||
window = evaluate(window, height=height, width=width)
|
||||
|
||||
(row_start, row_stop), (col_start, col_stop) = window.toranges()
|
||||
return (
|
||||
@ -403,9 +362,8 @@ def window_index(window):
|
||||
slice(int(math.floor(col_start)), int(math.ceil(col_stop))))
|
||||
|
||||
|
||||
def round_window_to_full_blocks(window, block_shapes):
|
||||
"""
|
||||
Round window to include full expanse of intersecting tiles.
|
||||
def round_window_to_full_blocks(window, block_shapes, height=0, width=0):
|
||||
"""Round window to include full expanse of intersecting tiles.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@ -413,18 +371,19 @@ def round_window_to_full_blocks(window, block_shapes):
|
||||
The input window.
|
||||
|
||||
block_shapes : tuple of block shapes
|
||||
The input raster's block shape. All bands must have the same block/stripe structure
|
||||
The input raster's block shape. All bands must have the same
|
||||
block/stripe structure
|
||||
|
||||
Returns
|
||||
-------
|
||||
Window
|
||||
"""
|
||||
if len(set(block_shapes)) != 1: # pragma: no cover
|
||||
raise ValueError(
|
||||
raise WindowError(
|
||||
"All bands must have the same block/stripe structure")
|
||||
|
||||
window = coerce_and_warn(window)
|
||||
|
||||
window = evaluate(window, height=height, width=width)
|
||||
|
||||
height_shape = block_shapes[0][0]
|
||||
width_shape = block_shapes[0][1]
|
||||
|
||||
@ -438,7 +397,14 @@ def round_window_to_full_blocks(window, block_shapes):
|
||||
col_max = int(col_stop // width_shape) * width_shape + \
|
||||
(width_shape if col_stop % width_shape != 0 else 0)
|
||||
|
||||
return Window.from_ranges((row_min, row_max), (col_min, col_max))
|
||||
return Window(col_min, row_min, col_max - col_min, row_max - row_min)
|
||||
|
||||
|
||||
def validate_length_value(instance, attribute, value):
|
||||
if value and value < 0:
|
||||
raise ValueError("Number of columns or rows must be non-negative")
|
||||
|
||||
_default = attr.Factory(lambda x: 0.0 if x is None else float(x))
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
@ -450,54 +416,53 @@ class Window(object):
|
||||
|
||||
Attributes
|
||||
----------
|
||||
col_off
|
||||
num_cols
|
||||
row_off
|
||||
num_rows
|
||||
col_off, row_off: float
|
||||
The offset for the window.
|
||||
width, height: float
|
||||
Lengths of the window.
|
||||
|
||||
Previously the lengths were called 'num_cols' and 'num_rows' but
|
||||
this is a bit confusing in the new float precision world and the
|
||||
attributes have been changed. The originals are deprecated.
|
||||
"""
|
||||
col_off = attr.ib(default=0.0)
|
||||
row_off = attr.ib(default=0.0)
|
||||
num_cols = attr.ib(default=0.0)
|
||||
num_rows = attr.ib(default=0.0)
|
||||
col_off = attr.ib(default=_default)
|
||||
row_off = attr.ib(default=_default)
|
||||
width = attr.ib(default=_default, validator=validate_length_value)
|
||||
height = attr.ib(default=_default, validator=validate_length_value)
|
||||
|
||||
def __repr__(self):
|
||||
"""Return a nicely formatted representation string"""
|
||||
return (
|
||||
"Window(col_off={self.col_off}, row_off={self.row_off}, "
|
||||
"num_cols={self.num_cols}, num_rows={self.num_rows})").format(
|
||||
"width={self.width}, height={self.height})").format(
|
||||
self=self)
|
||||
|
||||
def __getnewargs__(self):
|
||||
'Return self as a plain tuple. Used by copy and pickle.'
|
||||
return self.flatten()
|
||||
|
||||
def flatten(self):
|
||||
"""A flattened form of the window.
|
||||
|
||||
Returns
|
||||
-------
|
||||
col_off, row_off, num_cols, num_rows: int
|
||||
col_off, row_off, width, height: float
|
||||
Window offsets and lengths.
|
||||
"""
|
||||
return (self.col_off, self.row_off, self.num_cols, self.num_rows)
|
||||
return (self.col_off, self.row_off, self.width, self.height)
|
||||
|
||||
def todict(self):
|
||||
"""A mapping of field names and values.
|
||||
"""A mapping of attribute names and values.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
"""
|
||||
return collections.OrderedDict(
|
||||
col_off=self.col_off, row_off=self.row_off, num_cols=self.num_cols,
|
||||
num_rows=self.num_rows)
|
||||
col_off=self.col_off, row_off=self.row_off, width=self.width,
|
||||
height=self.height)
|
||||
|
||||
def toranges(self):
|
||||
"""Makes an equivalent pair of range tuples"""
|
||||
row_stop = None if self.num_rows is None else self.row_off + self.num_rows
|
||||
col_stop = None if self.num_cols is None else self.col_off + self.num_cols
|
||||
return (
|
||||
(self.row_off, row_stop), (self.col_off, col_stop))
|
||||
(self.row_off, self.row_off + self.height),
|
||||
(self.col_off, self.col_off + self.width))
|
||||
|
||||
def toslices(self):
|
||||
"""Slice objects for use as an ndarray indexer.
|
||||
@ -510,44 +475,109 @@ class Window(object):
|
||||
return tuple(slice(*rng) for rng in self.toranges())
|
||||
|
||||
@property
|
||||
def __array_interface__(self):
|
||||
return {'shape': (2, 2), 'typestr': 'f', 'version': 3,
|
||||
'data': np.array(
|
||||
[[a, b] for a, b in self.toranges()], dtype='float')}
|
||||
def num_cols(self):
|
||||
warnings.warn("use 'width' attribute instead", RasterioDeprecationWarning)
|
||||
return self.width
|
||||
|
||||
@property
|
||||
def num_rows(self):
|
||||
warnings.warn("use 'height' attribute instead",
|
||||
RasterioDeprecationWarning, stacklevel=2)
|
||||
return self.height
|
||||
|
||||
def __getitem__(self, index):
|
||||
"""Provides backwards compatibility for clients using tuples"""
|
||||
warnings.warn("This usage is deprecated", RasterioDeprecationWarning)
|
||||
return self.toranges()[index]
|
||||
|
||||
@classmethod
|
||||
def from_offlen(cls, col_off, row_off, num_cols, num_rows):
|
||||
"""For backwards compatibility only"""
|
||||
warnings.warn("Use the class constructor instead of this method",
|
||||
DeprecationWarning)
|
||||
RasterioDeprecationWarning)
|
||||
return cls(col_off, row_off, num_cols, num_rows)
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_ranges(cls, row_range, col_range):
|
||||
"""Construct a Window from row and column range tuples.
|
||||
def from_slices(cls, rows, cols, height=-1, width=-1, boundless=False):
|
||||
"""Construct a Window from row and column slices or tuples.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
row_range, col_range: tuple
|
||||
2-tuples containing start, stop indexes.
|
||||
rows, cols: slice or tuple
|
||||
Slices or 2-tuples containing start, stop indexes.
|
||||
height, width: float
|
||||
A shape to resolve relative values against.
|
||||
boundless: bool, optional
|
||||
Whether the inputs are bounded or bot.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Window
|
||||
"""
|
||||
col_off = col_range[0] or 0.0
|
||||
row_off = row_range[0] or 0.0
|
||||
num_cols = None if col_range[1] is None else col_range[1] - col_off
|
||||
num_rows = None if row_range[1] is None else row_range[1] - row_off
|
||||
# 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
|
||||
|
||||
# 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")
|
||||
|
||||
row_off = rows.start or 0.0
|
||||
if not boundless and row_off < 0:
|
||||
row_off += height
|
||||
|
||||
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_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)
|
||||
|
||||
return cls(col_off, row_off, num_cols, num_rows)
|
||||
|
||||
def round_shape(self, op='ceil', pixel_precision=3):
|
||||
"""Return a copy with column and row numbers rounded
|
||||
|
||||
Numbers are rounded to the nearest whole number. The offsets
|
||||
@classmethod
|
||||
def from_ranges(cls, rows, cols):
|
||||
"""For backwards compatibility only"""
|
||||
warnings.warn("Use the from_slices class method instead",
|
||||
RasterioDeprecationWarning)
|
||||
return cls.from_slices(rows, cols)
|
||||
|
||||
def round_lengths(self, op='floor', pixel_precision=3):
|
||||
"""Return a copy with width and height rounded.
|
||||
|
||||
Lengths are rounded to the nearest whole number. The offsets
|
||||
are not changed.
|
||||
|
||||
Parameters
|
||||
@ -563,8 +593,54 @@ class Window(object):
|
||||
"""
|
||||
operator = getattr(math, op, None)
|
||||
if not operator:
|
||||
raise ValueError("operator must be 'ceil' or 'floor'")
|
||||
|
||||
return Window(self.col_off, self.row_off,
|
||||
operator(round(self.num_cols, pixel_precision)),
|
||||
operator(round(self.num_rows, pixel_precision)))
|
||||
raise WindowError("operator must be 'ceil' or 'floor'")
|
||||
else:
|
||||
return Window(self.col_off, self.row_off,
|
||||
operator(round(self.width, pixel_precision)),
|
||||
operator(round(self.height, pixel_precision)))
|
||||
|
||||
round_shape = round_lengths
|
||||
|
||||
def round_offsets(self, op='floor', pixel_precision=3):
|
||||
"""Return a copy with column and row offsets rounded.
|
||||
|
||||
Offsets are rounded to the nearest whole number. The lengths
|
||||
are not changed.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
op: str
|
||||
'ceil' or 'floor'
|
||||
pixel_precision: int
|
||||
Number of places of rounding precision.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Window
|
||||
"""
|
||||
operator = getattr(math, op, None)
|
||||
if not operator:
|
||||
raise WindowError("operator must be 'ceil' or 'floor'")
|
||||
else:
|
||||
return Window(operator(round(self.col_off, pixel_precision)),
|
||||
operator(round(self.row_off, pixel_precision)),
|
||||
self.width, self.height)
|
||||
|
||||
def crop(self, height, width):
|
||||
"""Return a copy cropped to height and width"""
|
||||
return crop(self, height, width)
|
||||
|
||||
def intersection(self, other):
|
||||
"""Return the intersection of this window and another
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
other: Window
|
||||
Another window
|
||||
|
||||
Returns
|
||||
-------
|
||||
Window
|
||||
"""
|
||||
return intersection([self, other])
|
||||
|
||||
@ -7,10 +7,7 @@ import pytest
|
||||
|
||||
import rasterio
|
||||
from rasterio.enums import MaskFlags
|
||||
from rasterio.errors import NodataShadowWarning
|
||||
|
||||
|
||||
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
|
||||
from rasterio.errors import NodataShadowWarning, RasterioDeprecationWarning
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
@ -46,8 +43,7 @@ def tiffs(tmpdir):
|
||||
|
||||
def test_mask_flags_deprecation():
|
||||
"""mask_flags is deprecated"""
|
||||
warnings.simplefilter('always')
|
||||
with pytest.warns(DeprecationWarning):
|
||||
with pytest.warns(RasterioDeprecationWarning):
|
||||
with rasterio.open('tests/data/RGB.byte.tif') as src:
|
||||
src.mask_flags
|
||||
|
||||
|
||||
@ -14,21 +14,8 @@ import rasterio
|
||||
from rasterio import windows
|
||||
|
||||
|
||||
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
|
||||
|
||||
class WindowTest(unittest.TestCase):
|
||||
|
||||
def test_window_shape_errors(self):
|
||||
# Positive height and width are needed when stop is None.
|
||||
self.assertRaises(
|
||||
ValueError,
|
||||
rasterio.window_shape,
|
||||
(((10, 20), (10, None)),))
|
||||
self.assertRaises(
|
||||
ValueError,
|
||||
rasterio.window_shape,
|
||||
(((None, 10), (10, 20)),))
|
||||
|
||||
def test_window_shape_None_start(self):
|
||||
self.assertEqual(
|
||||
rasterio.window_shape(((None, 4), (None, 102))),
|
||||
@ -131,7 +118,7 @@ class WindowWriteTest(unittest.TestCase):
|
||||
s.write(a, indexes=1, window=windows.Window(10, 30, 50, 50))
|
||||
# subprocess.call(["open", name])
|
||||
info = subprocess.check_output(["gdalinfo", "-stats", name])
|
||||
self.assert_(
|
||||
self.assertTrue(
|
||||
"Minimum=0.000, Maximum=127.000, "
|
||||
"Mean=31.750, StdDev=54.993" in info.decode('utf-8'),
|
||||
info)
|
||||
@ -169,7 +156,8 @@ def test_block_windows_filtered_none(path_rgb_byte_tif):
|
||||
"""Get no block windows using filter"""
|
||||
with rasterio.open(path_rgb_byte_tif) as src:
|
||||
w, s, e, n = src.bounds
|
||||
focus_window = src.window(w - 100.0, n + 100.0, w - 1.0, n + 1.0)
|
||||
focus_window = src.window(w - 100.0, n + 1.0, w - 1.0, n + 100.0,
|
||||
boundless=True)
|
||||
filter_func = partial(windows.intersect, focus_window)
|
||||
itr = ((ij, win) for ij, win in src.block_windows() if filter_func(win))
|
||||
with pytest.raises(StopIteration):
|
||||
|
||||
@ -6,9 +6,9 @@ def test_bounds():
|
||||
|
||||
def test_ul():
|
||||
with rasterio.open('tests/data/RGB.byte.tif') as src:
|
||||
assert src.ul(0, 0) == (101985.0, 2826915.0)
|
||||
assert src.ul(1, 0) == (101985.0, 2826614.95821727)
|
||||
assert src.ul(src.height, src.width) == (339315.0, 2611485.0)
|
||||
assert src.xy(0, 0, offset='ul') == (101985.0, 2826915.0)
|
||||
assert src.xy(1, 0, offset='ul') == (101985.0, 2826614.95821727)
|
||||
assert src.xy(src.height, src.width, offset='ul') == (339315.0, 2611485.0)
|
||||
|
||||
def test_res():
|
||||
with rasterio.open('tests/data/RGB.byte.tif') as src:
|
||||
|
||||
@ -25,4 +25,4 @@ class CopyTest(unittest.TestCase):
|
||||
'tests/data/RGB.byte.tif',
|
||||
name)
|
||||
info = subprocess.check_output(["gdalinfo", name])
|
||||
self.assert_("GTiff" in info.decode('utf-8'))
|
||||
self.assertTrue("GTiff" in info.decode('utf-8'))
|
||||
|
||||
@ -5,6 +5,7 @@ import affine
|
||||
import pytest
|
||||
|
||||
import rasterio
|
||||
from rasterio.errors import RasterioDeprecationWarning
|
||||
|
||||
|
||||
def test_open_affine_and_transform(path_rgb_byte_tif):
|
||||
@ -18,14 +19,13 @@ def test_open_affine_and_transform(path_rgb_byte_tif):
|
||||
with rasterio.open(
|
||||
path_rgb_byte_tif,
|
||||
affine=rasterio,
|
||||
transform=affine.Affine.identity()) as src:
|
||||
transform=affine.Affine.identity()):
|
||||
pass
|
||||
assert len(record) == 2
|
||||
assert "The 'affine' kwarg in rasterio.open() is deprecated" in str(record[0].message)
|
||||
assert "choosing 'transform'" in str(record[1].message)
|
||||
|
||||
|
||||
|
||||
def test_open_transform_gdal_geotransform(path_rgb_byte_tif):
|
||||
"""Passing a GDAL geotransform to rasterio.open(transform=...) should raise
|
||||
an exception.
|
||||
@ -33,7 +33,7 @@ def test_open_transform_gdal_geotransform(path_rgb_byte_tif):
|
||||
with pytest.raises(TypeError):
|
||||
with rasterio.open(
|
||||
path_rgb_byte_tif,
|
||||
transform=tuple(affine.Affine.identity())) as src:
|
||||
transform=tuple(affine.Affine.identity())):
|
||||
pass
|
||||
|
||||
|
||||
@ -42,12 +42,12 @@ def test_open_affine_kwarg_warning(path_rgb_byte_tif):
|
||||
with pytest.warns(DeprecationWarning):
|
||||
with rasterio.open(
|
||||
path_rgb_byte_tif,
|
||||
affine=affine.Affine.identity()) as src:
|
||||
affine=affine.Affine.identity()):
|
||||
pass
|
||||
|
||||
|
||||
def test_src_affine_warning(path_rgb_byte_tif):
|
||||
"""Calling src.affine should raise a warning."""
|
||||
with pytest.warns(DeprecationWarning):
|
||||
with pytest.warns(RasterioDeprecationWarning):
|
||||
with rasterio.open(path_rgb_byte_tif) as src:
|
||||
assert src.affine == src.transform
|
||||
|
||||
@ -6,13 +6,14 @@ import numpy as np
|
||||
import pytest
|
||||
|
||||
import rasterio
|
||||
from rasterio.errors import WindowError
|
||||
from rasterio import windows
|
||||
|
||||
|
||||
DATA_WINDOW = ((3, 5), (2, 6))
|
||||
|
||||
|
||||
def assert_window_almost_equals(a, b, precision=6):
|
||||
def assert_window_almost_equals(a, b, precision=3):
|
||||
for pair_outer in zip(a, b):
|
||||
for x, y in zip(*pair_outer):
|
||||
assert round(x, precision) == round(y, precision)
|
||||
@ -163,7 +164,7 @@ def test_window_intersection():
|
||||
|
||||
|
||||
def test_window_intersection_disjunct():
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(WindowError):
|
||||
windows.intersection(
|
||||
((0, 6), (3, 6)),
|
||||
((100, 200), (0, 12)),
|
||||
@ -208,7 +209,7 @@ def test_3x3matrix():
|
||||
arrangement = product(pairs, pairs)
|
||||
for wins in combinations(arrangement, 2):
|
||||
assert not windows.intersect(*wins)
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(WindowError):
|
||||
windows.intersection(*wins)
|
||||
|
||||
|
||||
|
||||
@ -3,11 +3,13 @@ import pytest
|
||||
|
||||
import rasterio
|
||||
from rasterio.io import WindowMethodsMixin
|
||||
from rasterio.windows import Window
|
||||
|
||||
|
||||
EPS = 1.0e-8
|
||||
|
||||
|
||||
def assert_window_almost_equals(a, b, precision=6):
|
||||
def assert_window_almost_equals(a, b, precision=3):
|
||||
for pair_outer in zip(a, b):
|
||||
for x, y in zip(*pair_outer):
|
||||
assert round(x, precision) == round(y, precision)
|
||||
@ -35,13 +37,10 @@ def test_windows_mixin():
|
||||
src.window(*src.bounds),
|
||||
((0, src.height), (0, src.width)))
|
||||
|
||||
assert src.window_bounds(
|
||||
((0, src.height),
|
||||
(0, src.width))) == src.bounds
|
||||
assert src.window_bounds(Window(0, 0, src.width, src.height)) == src.bounds
|
||||
|
||||
assert src.window_transform(
|
||||
((0, src.height),
|
||||
(0, src.width))) == src.transform
|
||||
Window(0, 0, src.width, src.height)) == src.transform
|
||||
|
||||
|
||||
def test_windows_mixin_fail():
|
||||
@ -51,26 +50,26 @@ def test_windows_mixin_fail():
|
||||
pass
|
||||
|
||||
src = MockDataset()
|
||||
with pytest.raises(AttributeError):
|
||||
assert src.window(0, 0, 1, 1, boundless=True)
|
||||
with pytest.raises(AttributeError):
|
||||
assert src.window_bounds(((0, 1), (0, 1)))
|
||||
with pytest.raises(AttributeError):
|
||||
assert src.window_transform(((0, 1), (0, 1)))
|
||||
with pytest.raises(TypeError):
|
||||
src.window()
|
||||
with pytest.raises(TypeError):
|
||||
src.window_bounds()
|
||||
with pytest.raises(TypeError):
|
||||
src.window_transform()
|
||||
|
||||
|
||||
def test_window_transform_method():
|
||||
with rasterio.open('tests/data/RGB.byte.tif') as src:
|
||||
assert src.window_transform(((0, None), (0, None))) == src.transform
|
||||
assert src.window_transform(((None, None), (None, None))) == src.transform
|
||||
assert src.window_transform(Window(0, 0, None, None)) == src.transform
|
||||
assert src.window_transform(Window(None, None, None, None)) == src.transform
|
||||
assert src.window_transform(
|
||||
((1, None), (1, None))).c == src.bounds.left + src.res[0]
|
||||
Window(1, 1, None, None)).c == src.bounds.left + src.res[0]
|
||||
assert src.window_transform(
|
||||
((1, None), (1, None))).f == src.bounds.top - src.res[1]
|
||||
((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]
|
||||
((-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]
|
||||
((-1, None), (-1, None))).f == src.bounds.top + src.res[1]
|
||||
|
||||
|
||||
def test_window_method():
|
||||
@ -90,14 +89,8 @@ def test_window_method():
|
||||
src.window(left, top - 2 * dy - EPS, left + 2 * dx - EPS, top),
|
||||
((0, 2), (0, 2)))
|
||||
|
||||
# bounds cropped
|
||||
assert_window_almost_equals(
|
||||
src.window(left - 2 * dx, top - 2 * dy, left + 2 * dx, top + 2 * dy),
|
||||
((0, 2), (0, 2)))
|
||||
|
||||
# boundless
|
||||
assert_window_almost_equals(
|
||||
src.window(left - 2 * dx, top - 2 * dy, left + 2 * dx, top + 2 * dy, boundless=True),
|
||||
((-2, 2), (-2, 2)))
|
||||
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import warnings
|
||||
import pytest
|
||||
|
||||
import rasterio
|
||||
from rasterio.errors import RasterioDeprecationWarning
|
||||
from rasterio.profiles import Profile, DefaultGTiffProfile
|
||||
from rasterio.profiles import default_gtiff_profile
|
||||
|
||||
@ -15,12 +16,10 @@ def test_base_profile_kwarg():
|
||||
assert Profile(foo='bar')['foo'] == 'bar'
|
||||
|
||||
|
||||
def test_gtiff_profile_format(recwarn):
|
||||
def test_gtiff_profile_format():
|
||||
assert DefaultGTiffProfile()['driver'] == 'GTiff'
|
||||
warnings.simplefilter('always')
|
||||
assert DefaultGTiffProfile()()['driver'] == 'GTiff'
|
||||
assert len(recwarn) == 1
|
||||
assert recwarn.pop(DeprecationWarning)
|
||||
with pytest.warns(RasterioDeprecationWarning):
|
||||
assert DefaultGTiffProfile()()['driver'] == 'GTiff'
|
||||
|
||||
|
||||
def test_gtiff_profile_interleave():
|
||||
@ -116,33 +115,27 @@ def test_dataset_profile_creation_kwds(data):
|
||||
assert src.profile['foo'] == 'bar'
|
||||
|
||||
|
||||
def test_profile_affine_stashing(recwarn):
|
||||
def test_profile_affine_stashing():
|
||||
"""Passing affine sets transform, with a warning"""
|
||||
warnings.simplefilter('always')
|
||||
profile = Profile(affine='foo')
|
||||
assert len(recwarn) == 1
|
||||
assert recwarn.pop(DeprecationWarning)
|
||||
assert 'affine' not in profile
|
||||
assert profile['transform'] == 'foo'
|
||||
with pytest.warns(RasterioDeprecationWarning):
|
||||
profile = Profile(affine='foo')
|
||||
assert 'affine' not in profile
|
||||
assert profile['transform'] == 'foo'
|
||||
|
||||
|
||||
def test_profile_mixed_error(recwarn):
|
||||
def test_profile_mixed_error():
|
||||
"""Warn if both affine and transform are passed"""
|
||||
warnings.simplefilter('always')
|
||||
profile = Profile(affine='foo', transform='bar')
|
||||
assert len(recwarn) == 1
|
||||
assert recwarn.pop(DeprecationWarning)
|
||||
assert 'affine' not in profile
|
||||
assert profile['transform'] == 'bar'
|
||||
with pytest.warns(RasterioDeprecationWarning):
|
||||
profile = Profile(affine='foo', transform='bar')
|
||||
assert 'affine' not in profile
|
||||
assert profile['transform'] == 'bar'
|
||||
|
||||
|
||||
def test_profile_affine_alias(recwarn):
|
||||
def test_profile_affine_alias():
|
||||
"""affine is an alias for transform, with a warning"""
|
||||
profile = Profile(transform='foo')
|
||||
warnings.simplefilter('always')
|
||||
assert profile['affine'] == 'foo'
|
||||
assert len(recwarn) == 1
|
||||
assert recwarn.pop(DeprecationWarning)
|
||||
with pytest.warns(RasterioDeprecationWarning):
|
||||
assert profile['affine'] == 'foo'
|
||||
|
||||
|
||||
def test_profile_affine_set():
|
||||
|
||||
@ -36,7 +36,7 @@ class ReaderContextTest(unittest.TestCase):
|
||||
self.assertEqual(s.nodatavals, (0, 0, 0))
|
||||
self.assertEqual(s.indexes, (1, 2, 3))
|
||||
self.assertEqual(s.crs['init'], 'epsg:32618')
|
||||
self.assert_(s.crs.wkt.startswith('PROJCS'), s.crs.wkt)
|
||||
self.assertTrue(s.crs.wkt.startswith('PROJCS'), s.crs.wkt)
|
||||
for i, v in enumerate((101985.0, 2611485.0, 339315.0, 2826915.0)):
|
||||
self.assertAlmostEqual(s.bounds[i], v)
|
||||
self.assertEqual(
|
||||
@ -69,10 +69,10 @@ class ReaderContextTest(unittest.TestCase):
|
||||
|
||||
def test_derived_spatial(self):
|
||||
with rasterio.open('tests/data/RGB.byte.tif') as s:
|
||||
self.assert_(s.crs.wkt.startswith('PROJCS'), s.crs.wkt)
|
||||
self.assertTrue(s.crs.wkt.startswith('PROJCS'), s.crs.wkt)
|
||||
for i, v in enumerate((101985.0, 2611485.0, 339315.0, 2826915.0)):
|
||||
self.assertAlmostEqual(s.bounds[i], v)
|
||||
for a, b in zip(s.ul(0, 0), (101985.0, 2826915.0)):
|
||||
for a, b in zip(s.xy(0, 0, offset='ul'), (101985.0, 2826915.0)):
|
||||
self.assertAlmostEqual(a, b)
|
||||
|
||||
def test_read_ubyte(self):
|
||||
@ -127,7 +127,7 @@ class ReaderContextTest(unittest.TestCase):
|
||||
a = s.read(masked=True) # floating point values
|
||||
self.assertEqual(a.ndim, 3)
|
||||
self.assertEqual(a.shape, (1, 2, 3))
|
||||
self.assert_(hasattr(a, 'mask'))
|
||||
self.assertTrue(hasattr(a, 'mask'))
|
||||
self.assertEqual(list(set(s.nodatavals)), [None])
|
||||
self.assertEqual(a.dtype, rasterio.float64)
|
||||
|
||||
@ -169,8 +169,6 @@ class ReaderContextTest(unittest.TestCase):
|
||||
|
||||
def test_read_window(self):
|
||||
with rasterio.open('tests/data/RGB.byte.tif') as s:
|
||||
# correct format
|
||||
self.assertRaises(ValueError, s.read, window=(300, 320, 320, 330))
|
||||
# window with 1 nodata on band 3
|
||||
a = s.read(window=((300, 320), (320, 330)), masked=True)
|
||||
self.assertEqual(a.ndim, 3)
|
||||
|
||||
@ -8,9 +8,6 @@ import rasterio
|
||||
from rasterio.windows import Window
|
||||
|
||||
|
||||
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def rgb_array(path_rgb_byte_tif):
|
||||
with rasterio.open(path_rgb_byte_tif) as src:
|
||||
@ -134,17 +131,3 @@ def test_read_boundless_noshift():
|
||||
r2 = src.read(boundless=True,
|
||||
window=((-1, src.shape[0] + 1), (100, 101)))[0, 0, 0:9]
|
||||
assert np.array_equal(r1, r2)
|
||||
|
||||
|
||||
def test_np_warning(recwarn, rgb_byte_tif_reader):
|
||||
"""Ensure no deprecation warnings
|
||||
On np 1.11 and previous versions of rasterio you might see:
|
||||
VisibleDeprecationWarning: using a non-integer number
|
||||
instead of an integer will result in an error in the future
|
||||
"""
|
||||
import warnings
|
||||
warnings.simplefilter('always')
|
||||
with rgb_byte_tif_reader as src:
|
||||
window = Window(-10, -10, 110, 110)
|
||||
src.read(1, window=window, boundless=True)
|
||||
assert len(recwarn) == 0
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import logging
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
@ -17,8 +16,6 @@ from rasterio.rio.main import main_group
|
||||
|
||||
DEFAULT_SHAPE = (10, 10)
|
||||
|
||||
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
|
||||
|
||||
|
||||
# Custom markers.
|
||||
xfail_pixel_sensitive_gdal22 = pytest.mark.xfail(
|
||||
@ -124,13 +121,13 @@ def test_mask_out_of_bounds(runner, tmpdir, basic_feature,
|
||||
|
||||
output = str(tmpdir.join('test.tif'))
|
||||
|
||||
with pytest.warns(UserWarning) as warnings:
|
||||
with pytest.warns(UserWarning) as rec:
|
||||
result = runner.invoke(
|
||||
main_group,
|
||||
['mask', pixelated_image_file, output, '--geojson-mask', '-'],
|
||||
input=json.dumps(basic_feature))
|
||||
assert result.exit_code == 0
|
||||
assert any(['outside bounds' in w.message.args[0] for w in warnings])
|
||||
assert any(['outside bounds' in w.message.args[0] for w in rec])
|
||||
assert os.path.exists(output)
|
||||
|
||||
with rasterio.open(output) as out:
|
||||
@ -277,8 +274,7 @@ def test_mask_crop_and_invert(runner, tmpdir, basic_feature, pixelated_image,
|
||||
|
||||
|
||||
def test_shapes(runner, pixelated_image_file):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
with pytest.warns(None):
|
||||
|
||||
result = runner.invoke(main_group, ['shapes', pixelated_image_file])
|
||||
|
||||
@ -303,8 +299,7 @@ def test_shapes_sequence(runner, pixelated_image_file):
|
||||
--sequence option should produce 4 features in series rather than
|
||||
inside a feature collection.
|
||||
"""
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
with pytest.warns(None):
|
||||
|
||||
result = runner.invoke(
|
||||
main_group, ['shapes', pixelated_image_file, '--sequence'])
|
||||
@ -349,8 +344,7 @@ def test_shapes_indent(runner, pixelated_image_file):
|
||||
"""
|
||||
--indent option should produce lots of newlines and contiguous spaces
|
||||
"""
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
with pytest.warns(None):
|
||||
|
||||
result = runner.invoke(
|
||||
main_group, ['shapes', pixelated_image_file, '--indent', 2])
|
||||
@ -363,8 +357,7 @@ def test_shapes_indent(runner, pixelated_image_file):
|
||||
|
||||
|
||||
def test_shapes_compact(runner, pixelated_image_file):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
with pytest.warns(None):
|
||||
|
||||
result = runner.invoke(
|
||||
main_group, ['shapes', pixelated_image_file, '--compact'])
|
||||
@ -408,8 +401,7 @@ def test_shapes_mask(runner, pixelated_image, pixelated_image_file):
|
||||
with rasterio.open(pixelated_image_file, 'r+') as out:
|
||||
out.write(pixelated_image, indexes=1)
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
with pytest.warns(None):
|
||||
result = runner.invoke(
|
||||
main_group, ['shapes', pixelated_image_file, '--mask'])
|
||||
assert result.exit_code == 0
|
||||
@ -431,8 +423,8 @@ def test_shapes_mask_sampling(runner, pixelated_image, pixelated_image_file):
|
||||
with rasterio.open(pixelated_image_file, 'r+') as out:
|
||||
out.write(pixelated_image, indexes=1)
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
with pytest.warns(None):
|
||||
|
||||
result = runner.invoke(
|
||||
main_group,
|
||||
['shapes', pixelated_image_file, '--mask', '--sampling', 5])
|
||||
@ -457,8 +449,7 @@ def test_shapes_band1_as_mask(runner, pixelated_image, pixelated_image_file):
|
||||
with rasterio.open(pixelated_image_file, 'r+') as out:
|
||||
out.write(pixelated_image, indexes=1)
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
with pytest.warns(None):
|
||||
result = runner.invoke(
|
||||
main_group,
|
||||
['shapes', pixelated_image_file, '--band', '--bidx', '1', '--as-mask'])
|
||||
|
||||
@ -11,18 +11,18 @@ def test_window_transform():
|
||||
assert src.window_transform(((0, None), (0, None))) == src.transform
|
||||
assert src.window_transform(((None, None), (None, None))) == src.transform
|
||||
assert src.window_transform(
|
||||
((1, None), (1, None))).c == src.bounds.left + src.res[0]
|
||||
((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]
|
||||
((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]
|
||||
((-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]
|
||||
((-1, None), (-1, None))).f == src.bounds.top + src.res[1]
|
||||
|
||||
|
||||
def test_from_origin():
|
||||
with rasterio.open('tests/data/RGB.byte.tif') as src:
|
||||
w, n = src.ul(0, 0)
|
||||
w, n = src.xy(0, 0, offset='ul')
|
||||
xs, ys = src.res
|
||||
tr = transform.from_origin(w, n, xs, ys)
|
||||
assert [round(v, 7) for v in tr] == [round(v, 7) for v in src.transform]
|
||||
@ -58,37 +58,6 @@ def test_window_bounds():
|
||||
assert ds_x_min <= w_x_min <= w_x_max <= ds_x_max
|
||||
assert ds_y_min <= w_y_min <= w_y_max <= ds_y_max
|
||||
|
||||
# Test a small window in each corner, both in and slightly out of bounds
|
||||
p = 10
|
||||
for ranges in (
|
||||
# In bounds (UL, UR, LL, LR)
|
||||
((0, p), (0, p)),
|
||||
((0, p), (cols - p, p)),
|
||||
((rows - p, p), (0, p)),
|
||||
((rows - p, p), (cols - p, p)),
|
||||
|
||||
# Out of bounds (UL, UR, LL, LR)
|
||||
((-1, p), (-1, p)),
|
||||
((-1, p), (cols - p, p + 1)),
|
||||
((rows - p, p + 1), (-1, p)),
|
||||
((rows - p, p + 1), (cols - p, p + 1))):
|
||||
|
||||
# Alternate formula
|
||||
|
||||
window = Window.from_ranges(*ranges)
|
||||
(row_min, row_max), (col_min, col_max) = ranges
|
||||
win_aff = src.window_transform(window)
|
||||
|
||||
x_min, y_max = win_aff.c, win_aff.f
|
||||
x_max = win_aff.c + (src.res[0] * (col_max - col_min))
|
||||
y_min = win_aff.f - (src.res[1] * (row_max - row_min))
|
||||
|
||||
expected = (x_min, y_min, x_max, y_max)
|
||||
actual = src.window_bounds(window)
|
||||
|
||||
for e, a in zip(expected, actual):
|
||||
assert round(e, 7) == round(a, 7)
|
||||
|
||||
|
||||
def test_affine_roundtrip(tmpdir):
|
||||
output = str(tmpdir.join('test.tif'))
|
||||
@ -174,6 +143,11 @@ def test_xy():
|
||||
xy(aff, 1, 0, offset='ur')
|
||||
|
||||
|
||||
def test_bogus_offset():
|
||||
with pytest.raises(ValueError):
|
||||
xy(None, 1, 0, offset='bogus')
|
||||
|
||||
|
||||
def test_guard_transform_gdal_TypeError(path_rgb_byte_tif):
|
||||
"""As part of the 1.0 migration, guard_transform() should raise a TypeError
|
||||
if a GDAL geotransform is encountered"""
|
||||
@ -200,7 +174,7 @@ def test_rowcol():
|
||||
assert rowcol(aff, right, bottom) == (src.height, src.width)
|
||||
assert rowcol(aff, left, bottom) == (src.height, 0)
|
||||
assert rowcol(aff, 101985.0, 2826915.0) == (0, 0)
|
||||
assert rowcol(aff, 101985.0+400.0, 2826915.0) == (0, 1)
|
||||
assert rowcol(aff, 101985.0 + 400.0, 2826915.0) == (0, 1)
|
||||
|
||||
|
||||
def test_xy_rowcol_inverse():
|
||||
|
||||
@ -2,6 +2,7 @@ from copy import copy
|
||||
import logging
|
||||
import math
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
from affine import Affine
|
||||
from hypothesis import given
|
||||
@ -10,50 +11,19 @@ import numpy as np
|
||||
import pytest
|
||||
|
||||
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,
|
||||
toranges)
|
||||
crop, from_bounds, bounds, transform, evaluate, window_index, shape,
|
||||
Window, intersect, intersection, get_data_window, union,
|
||||
round_window_to_full_blocks, toranges)
|
||||
|
||||
EPS = 1.0e-8
|
||||
|
||||
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
|
||||
|
||||
|
||||
def assert_window_almost_equals(a, b, precision=3):
|
||||
for pair_outer in zip(a, b):
|
||||
for x, y in zip(*pair_outer):
|
||||
assert round(x, precision) == round(y, precision)
|
||||
|
||||
|
||||
@given(col_off=floats(min_value=-1.0e+7, max_value=1.0e+7),
|
||||
row_off=floats(min_value=-1.0e+7, max_value=1.0e+7),
|
||||
num_cols=floats(min_value=0.0, max_value=1.0e+7),
|
||||
num_rows=floats(min_value=0.0, max_value=1.0e+7))
|
||||
def test_window_ctor(col_off, row_off, num_cols, num_rows):
|
||||
window = Window(col_off, row_off, num_cols, num_rows)
|
||||
assert window.col_off == col_off
|
||||
assert window.row_off == row_off
|
||||
assert window.num_cols == num_cols
|
||||
assert window.num_rows == num_rows
|
||||
|
||||
|
||||
|
||||
@given(col_off=floats(min_value=-1.0e+7, max_value=1.0e+7),
|
||||
row_off=floats(min_value=-1.0e+7, max_value=1.0e+7),
|
||||
num_cols=floats(min_value=0.0, max_value=1.0e+7),
|
||||
num_rows=floats(min_value=0.0, max_value=1.0e+7))
|
||||
def test_window_round_up(col_off, row_off, num_cols, num_rows):
|
||||
window = Window(col_off, row_off, num_cols, num_rows).round_shape()
|
||||
assert window.col_off == col_off
|
||||
assert window.row_off == row_off
|
||||
assert window.num_cols == math.ceil(round(num_cols, 3))
|
||||
assert window.num_rows == math.ceil(round(num_rows, 3))
|
||||
|
||||
|
||||
def test_round_shape_invalid_op():
|
||||
with pytest.raises(ValueError):
|
||||
Window(0, 0, 1, 1).round_shape(op='bogus')
|
||||
def assert_window_almost_equals(a, b):
|
||||
assert np.allclose(a.flatten(), b.flatten(), rtol=1e-3, atol=1e-4)
|
||||
|
||||
|
||||
@given(col_off=floats(min_value=-1.0e+7, max_value=1.0e+7),
|
||||
@ -66,23 +36,8 @@ def test_crop(col_off, row_off, num_cols, num_rows, height, width):
|
||||
window = crop(Window(col_off, row_off, num_cols, num_rows), height, width)
|
||||
assert 0.0 <= round(window.col_off, 3) <= width
|
||||
assert 0.0 <= round(window.row_off, 3) <= height
|
||||
assert round(window.num_cols, 3) <= round(width - window.col_off, 3)
|
||||
assert round(window.num_rows, 3) <= round(height - window.row_off, 3)
|
||||
|
||||
|
||||
def test_crop_coerce():
|
||||
with pytest.warns(DeprecationWarning):
|
||||
assert crop(((-10, 10), (-10, 10)), 5, 5).num_cols == 5
|
||||
|
||||
|
||||
def test_transform_coerce():
|
||||
with pytest.warns(DeprecationWarning):
|
||||
assert transform(((-10, 10), (-10, 10)), Affine.identity()).a == 1.0
|
||||
|
||||
|
||||
def test_window_index_coerce():
|
||||
with pytest.warns(DeprecationWarning):
|
||||
assert window_index(((-10, 10), (-10, 10)))[0].start == -10
|
||||
assert round(window.width, 3) <= round(width - window.col_off, 3)
|
||||
assert round(window.height, 3) <= round(height - window.row_off, 3)
|
||||
|
||||
|
||||
def test_window_function():
|
||||
@ -95,16 +50,19 @@ def test_window_function():
|
||||
|
||||
assert_window_almost_equals(from_bounds(
|
||||
left + EPS, bottom + EPS, right - EPS, top - EPS, src.transform,
|
||||
height, width), ((0, height), (0, width)))
|
||||
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), ((0, 2), (0, 2)))
|
||||
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, boundless=True), ((-2, 2), (-2, 2)))
|
||||
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():
|
||||
@ -117,97 +75,21 @@ def test_window_float():
|
||||
|
||||
assert_window_almost_equals(from_bounds(
|
||||
left, top - 400, left + 400, top, src.transform,
|
||||
height, width), ((0, 400 / src.res[1]), (0, 400 / src.res[0])))
|
||||
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),
|
||||
precision=5)
|
||||
|
||||
|
||||
def test_toranges():
|
||||
assert Window(0, 0, 1, 1).toranges() == ((0, 1), (0, 1))
|
||||
|
||||
|
||||
def test_toranges_warn():
|
||||
with pytest.warns(DeprecationWarning):
|
||||
toranges(((0, 1), (0, 1)))
|
||||
|
||||
|
||||
def test_window_function():
|
||||
# TODO: break this test up.
|
||||
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
|
||||
|
||||
assert_window_almost_equals(from_bounds(
|
||||
left + EPS, bottom + EPS, right - EPS, top - EPS, src.transform,
|
||||
height, width), ((0, height), (0, width)))
|
||||
|
||||
assert_window_almost_equals(from_bounds(
|
||||
left, top - 2 * dy - EPS, left + 2 * dx - EPS, top, src.transform,
|
||||
height, width), ((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, boundless=True), ((-2, 2), (-2, 2)))
|
||||
|
||||
|
||||
def test_window_float():
|
||||
"""Test window float values"""
|
||||
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
|
||||
|
||||
assert_window_almost_equals(from_bounds(
|
||||
left, top - 400, left + 400, top, src.transform,
|
||||
height, width), ((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),
|
||||
precision=5)
|
||||
|
||||
def test_toranges():
|
||||
assert Window(0, 0, 1, 1).toranges() == ((0, 1), (0, 1))
|
||||
|
||||
@given(col_off=floats(min_value=-1.0e+7, max_value=1.0e+7),
|
||||
row_off=floats(min_value=-1.0e+7, max_value=1.0e+7),
|
||||
num_cols=floats(min_value=0.0, max_value=1.0e+7),
|
||||
num_rows=floats(min_value=0.0, max_value=1.0e+7))
|
||||
def test_array_interface(col_off, row_off, num_cols, num_rows):
|
||||
arr = np.array(Window(col_off, row_off, num_cols, num_rows))
|
||||
assert arr.shape == (2, 2)
|
||||
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),
|
||||
precision=5)
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="feature eliminated")
|
||||
def test_window_function_valuerror():
|
||||
with rasterio.open('tests/data/RGB.byte.tif') as src:
|
||||
left, bottom, right, top = src.bounds
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
# No height or width
|
||||
from_bounds(left + EPS, bottom + EPS, right - EPS, top - EPS,
|
||||
src.transform)
|
||||
Window(0, 0, 10, 10))
|
||||
|
||||
|
||||
def test_window_transform_function():
|
||||
@ -231,51 +113,17 @@ def test_window_bounds_function():
|
||||
assert bounds(((0, rows), (0, cols)), src.transform) == src.bounds
|
||||
|
||||
|
||||
bad_value_windows = [
|
||||
(1, 2, 3),
|
||||
((1, 0), (2,))]
|
||||
|
||||
bad_type_windows = [
|
||||
(1, 2),
|
||||
((1, 0), 2)]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("window", bad_value_windows)
|
||||
def test_eval_window_bad_value(window):
|
||||
with pytest.raises(ValueError):
|
||||
evaluate(window, 10, 10)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("window", bad_type_windows)
|
||||
def test_eval_window_bad_type(window):
|
||||
with pytest.raises(TypeError):
|
||||
with pytest.raises(WindowError):
|
||||
evaluate(window, 10, 10)
|
||||
|
||||
|
||||
bad_params = (
|
||||
(((-1, 10), (0, 10)), -1, 10),
|
||||
(((1, -1), (0, 10)), -1, 10),
|
||||
(((0, 10), (-1, 10)), 10, -1),
|
||||
(((0, 10), (1, -1)), 10, -1),
|
||||
(((10, 5), (0, 5)), 10, 10),
|
||||
(((0, 5), (10, 5)), 10, 10))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("params", bad_params)
|
||||
def test_eval_window_invalid_dims(params):
|
||||
with pytest.raises(ValueError):
|
||||
evaluate(*params)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("params,expected", [
|
||||
([((2, 4), (2, 4)), 10, 10], ((2, 4), (2, 4))),
|
||||
([((-10, None), (-10, None)), 100, 90], ((90, 100), (80, 90))),
|
||||
([((None, -10), (None, -10)), 100, 90], ((0, 90), (0, 80))),
|
||||
([((0, 256), (0, 256)), 7791, 7621], ((0, 256), (0, 256)))])
|
||||
def test_windows_evaluate(params, expected):
|
||||
assert evaluate(*params) == Window.from_ranges(*expected)
|
||||
|
||||
|
||||
def test_window_index():
|
||||
idx = window_index(((0, 4), (1, 12)))
|
||||
assert len(idx) == 2
|
||||
@ -290,12 +138,9 @@ def test_window_index():
|
||||
|
||||
def test_window_shape_errors():
|
||||
# Positive height and width are needed when stop is None.
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(WindowError):
|
||||
assert shape(((10, 20), (10, None)))
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
assert shape(((-1, 10), (10, 20)))
|
||||
|
||||
|
||||
def test_window_shape_None_start():
|
||||
assert shape(((None, 4), (None, 102))) == (4, 102)
|
||||
@ -318,57 +163,6 @@ def test_shape_negative_start():
|
||||
assert shape(((None, ~0), (None, ~0)), 100, 90) == (99, 89)
|
||||
|
||||
|
||||
def test_window_class_constructor():
|
||||
"""Construct a Window from offsets, height, and width"""
|
||||
window = Window(row_off=0, col_off=1, num_rows=100, num_cols=200)
|
||||
assert window == Window.from_ranges((0, 100), (1, 201))
|
||||
|
||||
|
||||
def test_window_class_constructor_positional():
|
||||
"""Construct a Window using positional parameters"""
|
||||
window = Window(1, 0, 200, 100)
|
||||
assert window == Window.from_ranges((0, 100), (1, 201))
|
||||
|
||||
|
||||
def test_window_class_attrs():
|
||||
"""Test Window attributes"""
|
||||
window = Window(row_off=0, col_off=1, num_rows=100, num_cols=200)
|
||||
assert window.col_off == 1
|
||||
assert window.row_off == 0
|
||||
assert window.num_cols == 200
|
||||
assert window.num_rows == 100
|
||||
|
||||
|
||||
def test_window_class_repr():
|
||||
"""Test Window respresentation"""
|
||||
window = Window(row_off=0, col_off=1, num_rows=100, num_cols=200)
|
||||
assert repr(window) == 'Window(col_off=1, row_off=0, num_cols=200, num_rows=100)'
|
||||
assert eval(repr(window)) == Window.from_ranges((0, 100), (1, 201))
|
||||
|
||||
|
||||
def test_window_class_copy():
|
||||
"""Test Window copying"""
|
||||
window = Window(row_off=0, col_off=1, num_rows=100, num_cols=200)
|
||||
assert copy(window) == Window.from_ranges((0, 100), (1, 201))
|
||||
|
||||
|
||||
def test_window_class_todict():
|
||||
"""Test Window.todict"""
|
||||
window = Window(row_off=0, col_off=1, num_rows=100, num_cols=200)
|
||||
assert window.todict() == {
|
||||
'col_off': 1, 'num_cols': 200, 'num_rows': 100, 'row_off': 0}
|
||||
|
||||
|
||||
def test_window_class_toslices():
|
||||
"""Test Window.toslices"""
|
||||
window = Window(row_off=0, col_off=1, num_rows=100, num_cols=200)
|
||||
yslice, xslice = window.toslices()
|
||||
assert yslice.start == 0
|
||||
assert yslice.stop == 100
|
||||
assert xslice.start == 1
|
||||
assert xslice.stop == 201
|
||||
|
||||
|
||||
def test_window_class_intersects():
|
||||
"""Windows intersect"""
|
||||
assert intersect(Window(0, 0, 10, 10), Window(8, 8, 10, 10))
|
||||
@ -384,14 +178,15 @@ def test_window_class_nonintersects():
|
||||
assert not intersect(Window(0, 0, 10, 10), Window(10, 10, 10, 10))
|
||||
|
||||
|
||||
def test_window_from_ranges():
|
||||
"""from_ranges classmethod works."""
|
||||
assert Window.from_ranges((0, 1), (2, 3)) == Window.from_ranges((0, 1), (2, 3))
|
||||
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_window_from_offlen():
|
||||
"""from_offlen classmethod works."""
|
||||
assert Window.from_offlen(2, 0, 1, 1) == Window.from_ranges((0, 1), (2, 3))
|
||||
with pytest.warns(RasterioDeprecationWarning):
|
||||
assert Window.from_offlen(2, 0, 1, 1) == Window.from_slices((0, 1), (2, 3))
|
||||
|
||||
|
||||
def test_read_with_window_class():
|
||||
@ -404,7 +199,7 @@ def test_read_with_window_class():
|
||||
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(ValueError):
|
||||
with pytest.raises(WindowError):
|
||||
get_data_window(arr)
|
||||
|
||||
|
||||
@ -412,7 +207,7 @@ 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_ranges((0, 3), (0, 3))
|
||||
assert window == Window.from_slices((0, 3), (0, 3))
|
||||
|
||||
|
||||
def test_data_window_nodata():
|
||||
@ -420,7 +215,7 @@ def test_data_window_nodata():
|
||||
arr = np.ones((3, 3))
|
||||
arr[0, :] = 0
|
||||
window = get_data_window(arr, nodata=0)
|
||||
assert window == Window.from_ranges((1, 3), (0, 3))
|
||||
assert window == Window.from_slices((1, 3), (0, 3))
|
||||
|
||||
|
||||
def test_data_window_novalid():
|
||||
@ -428,7 +223,7 @@ def test_data_window_novalid():
|
||||
arr = np.ones((3, 3))
|
||||
arr[:, :] = 0
|
||||
window = get_data_window(arr, nodata=0)
|
||||
assert window == Window.from_ranges((0, 0), (0, 0))
|
||||
assert window == Window.from_slices((0, 0), (0, 0))
|
||||
|
||||
|
||||
def test_data_window_maskedarray():
|
||||
@ -437,7 +232,7 @@ def test_data_window_maskedarray():
|
||||
arr[0, :] = 0
|
||||
arr = np.ma.masked_array(arr, arr == 0)
|
||||
window = get_data_window(arr)
|
||||
assert window == Window.from_ranges((1, 3), (0, 3))
|
||||
assert window == Window.from_slices((1, 3), (0, 3))
|
||||
|
||||
|
||||
def test_data_window_nodata_3d():
|
||||
@ -445,43 +240,43 @@ def test_data_window_nodata_3d():
|
||||
arr = np.ones((3, 3, 3))
|
||||
arr[:, 0, :] = 0
|
||||
window = get_data_window(arr, nodata=0)
|
||||
assert window == Window.from_ranges((1, 3), (0, 3))
|
||||
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_ranges((0, 3), (0, 3))
|
||||
assert window == Window.from_slices((0, 3), (0, 3))
|
||||
|
||||
|
||||
def test_no_intersection():
|
||||
"""Non intersecting windows raises error."""
|
||||
with pytest.raises(ValueError):
|
||||
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_ranges((8, 10), (8, 10))
|
||||
assert window == Window.from_slices((8, 10), (8, 10))
|
||||
|
||||
|
||||
def test_round_window_to_full_blocks():
|
||||
with rasterio.open('tests/data/alpha.tif') as src, pytest.warns(DeprecationWarning):
|
||||
with rasterio.open('tests/data/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[0][0] % height_shape == 0
|
||||
assert rounded_window[0][1] % height_shape == 0
|
||||
assert rounded_window[1][0] % width_shape == 0
|
||||
assert rounded_window[1][1] % width_shape == 0
|
||||
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(ValueError):
|
||||
with pytest.raises(WindowError):
|
||||
round_window_to_full_blocks(
|
||||
Window(0, 0, 10, 10), block_shapes=[(1, 1), (2, 2)])
|
||||
|
||||
@ -491,7 +286,8 @@ def test_round_window_already_at_edge():
|
||||
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_ranges(*test_window)
|
||||
assert rounded_window == Window.from_slices(*test_window)
|
||||
|
||||
|
||||
def test_round_window_boundless():
|
||||
with rasterio.open('tests/data/alpha.tif') as src:
|
||||
@ -501,7 +297,7 @@ def test_round_window_boundless():
|
||||
block_shape = block_shapes[0]
|
||||
height_shape = block_shape[0]
|
||||
width_shape = block_shape[1]
|
||||
assert rounded_window[0][0] % height_shape == 0
|
||||
assert rounded_window[0][1] % height_shape == 0
|
||||
assert rounded_window[1][0] % width_shape == 0
|
||||
assert rounded_window[1][1] % width_shape == 0
|
||||
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user