Module delta.imagery.rectangle

Simple rectangle class, useful for dealing with ROIs and tiles.

Expand source code
# Copyright © 2020, United States Government, as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All rights reserved.
#
# The DELTA (Deep Earth Learning, Tools, and Analysis) platform is
# licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#        http://www.apache.org/licenses/LICENSE-2.0.
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Simple rectangle class, useful for dealing with ROIs and tiles.
"""
import math
import copy

class Rectangle:
    """
    Simple rectangle class for ROIs. Max values are NON-INCLUSIVE.
    When using it, stay consistent with float or integer values.
    """
    def __init__(self, min_x, min_y, max_x=0, max_y=0,
                 width=0, height=0):
        """
        Parameters
        ----------
        min_x: int
        min_y: int
        max_x: int
        max_y: int
            Rectangle bounds.
        width: int
        height: int
            Specify width / height to use these instead of max_x/max_y.
        """
        self.min_x = min_x
        self.min_y = min_y
        if width > 0:
            self.max_x = min_x + width
        else:
            self.max_x = max_x
        if height > 0:
            self.max_y = min_y + height
        else:
            self.max_y = max_y

    def __str__(self):
        if isinstance(self.min_x, int):
            return ('min_x: %d, max_x: %d, min_y: %d, max_y: %d' %
                    (self.min_x, self.max_x, self.min_y, self.max_y))
        return ('min_x: %f, max_x: %f, min_y: %f, max_y: %f' %
                (self.min_x, self.max_x, self.min_y, self.max_y))

    def __repr__(self):
        return self.__str__()

#    def indexGenerator(self):
#        '''Generator function used to iterate over all integer indices.
#           Only use this with integer boundaries!'''
#        for row in range(self.min_y, self.max_y):
#            for col in range(self.min_x, self.max_x):
#                yield(TileIndex(row,col))

    def bounds(self):
        """
        Returns
        -------
        (int, int, int, int):
            (min_x, max_x, min_y, max_y)
        """
        return (self.min_x, self.max_x, self.min_y, self.max_y)

    def width(self):
        return self.max_x - self.min_x
    def height(self):
        return self.max_y - self.min_y

    def has_area(self):
        """
        Returns
        -------
        bool:
            true if the rectangle contains any area.
        """
        return (self.width() > 0) and (self.height() > 0)

    def perimeter(self):
        return 2*self.width() + 2*self.height()

    def area(self):
        if not self.has_area():
            return 0
        return self.height() * self.width()

    def get_min_coord(self):
        return (self.min_x, self.min_y)
    def get_max_coord(self):
        return (self.max_x, self.max_y)

    def shift(self, dx, dy):
        '''Shifts the entire box'''
        self.min_x += dx
        self.max_x += dx
        self.min_y += dy
        self.max_y += dy

    def scale_by_constant(self, xScale, yScale):
        '''Scale the units by a constant'''
        if yScale is None:
            yScale = xScale
        self.min_x *= xScale
        self.max_x *= xScale
        self.min_y *= yScale
        self.max_y *= yScale

    def expand(self, left, down, right=None, up=None):
        '''Expand the box by an amount in each direction'''
        self.min_x -= left
        self.min_y -= down
        if right is None: # If right and up are not passed in, use left and down for both sides.
            right = left
        if up is None:
            up = down
        self.max_x += right
        self.max_y += up

    def expand_to_contain_pt(self, x, y):
        '''Expands the rectangle to contain the given point'''
        if isinstance(self.min_x, float):
            delta = 0.001
        else: # These are needed because the upper bound is non-exclusive.
            delta = 1
        if x < self.min_x: self.min_x = x
        if y < self.min_y: self.min_y = y
        if x > self.max_x: self.max_x = x + delta
        if y > self.max_y: self.max_y = y + delta

    def expand_to_contain_rect(self, other_rect):
        '''Expands the rectangle to contain the given rectangle'''

        if other_rect.min_x < self.min_x: self.min_x = other_rect.min_x
        if other_rect.min_y < self.min_y: self.min_y = other_rect.min_y
        if other_rect.max_x > self.max_x: self.max_x = other_rect.max_x
        if other_rect.max_y > self.max_y: self.max_y = other_rect.max_y

    def get_intersection(self, other_rect):
        '''Returns the overlapping region of two rectangles'''
        overlap = Rectangle(max(self.min_x, other_rect.min_x),
                            max(self.min_y, other_rect.min_y),
                            min(self.max_x, other_rect.max_x),
                            min(self.max_y, other_rect.max_y))
        return overlap

    def contains_pt(self, x, y):
        '''Returns true if this rect contains the given point'''
        if self.min_x > x: return False
        if self.min_y > y: return False
        if self.max_x < x: return False
        if self.max_y < y: return False
        return True

    def contains_rect(self, other_rect):
        '''Returns true if this rect contains all of the other rect'''
        if self.min_x > other_rect.min_x: return False
        if self.min_y > other_rect.min_y: return False
        if self.max_x < other_rect.max_x: return False
        if self.max_y < other_rect.max_y: return False
        return True

    def overlaps(self, other_rect):
        '''Returns true if there is any overlap between this and another rectangle'''
        overlap_area = self.get_intersection(other_rect)
        return overlap_area.has_area()

    def make_tile_rois(self, tile_shape, overlap_shape=(0, 0), include_partials=True, min_shape=(0, 0), #pylint: disable=R0912,R0914
                       partials_overlap=False, by_block=False, containing_rect=None):
        """
        Return a list of tiles encompassing the entire area of this Rectangle.

        Parameters
        ----------
        tile_shape: (int, int)
            Shape of each tile (width, height)
        overlap_shape: (int, int)
            Amount to overlap tiles in x and y direction
        include_partials: bool
            If true, include partial tiles at the edge of the image.
        min_shape: (int, int)
            If true and `partials` is true, keep partial tiles of this minimum size.
        partials_overlap: bool
            If `partials` is false, and this is true, expand partial tiles
            to the desired size. Tiles may overlap in some areas.
        by_block: bool
            If true, changes the returned generator to group tiles by block.
            This is intended to optimize disk reads by reading the entire block at once.
        containing_rect: Rectangle
            Tiles are restricted to fit inside this rectangle instead of "self".

        Returns
        -------
        List[Rectangle]:
            Generator yielding ROIs. If `by_block` is true, returns a generator of (Rectangle, List[Rectangle])
            instead, where the first rectangle is a larger block containing multiple tiles in a list.
        List[Rectangle]:
            Same as the first output, but the ROIs do not include any overlap regions or any area outside the rectangle.
        """
        tile_width, tile_height = tile_shape
        min_width, min_height = min_shape
        if not containing_rect:
            containing_rect = self

        half_overlap = (overlap_shape[0] // 2, overlap_shape[1] // 2)
        tile_spacing_x = tile_width  - overlap_shape[0]
        tile_spacing_y = tile_height - overlap_shape[1]
        num_tiles_round_up = (int(math.ceil((self.width() - overlap_shape[0]) / tile_spacing_x)),
                              int(math.ceil((self.height()- overlap_shape[1]) / tile_spacing_y)))
        if include_partials or partials_overlap:
            num_tiles = num_tiles_round_up
        else:
            containing_rect_tiles = (int(math.floor((containing_rect.width() - overlap_shape[0]) / tile_spacing_x)),
                                     int(math.floor((containing_rect.height()- overlap_shape[1]) / tile_spacing_y)))
            num_tiles = (min(num_tiles_round_up[0], containing_rect_tiles[0]),
                         min(num_tiles_round_up[1], containing_rect_tiles[1]))

        output_tiles = []
        unique_tiles = []
        for r in range(0, num_tiles[1]):
            row_tiles = []
            unique_row_tiles = []
            for c in range(0, num_tiles[0]):
                tile = Rectangle(self.min_x + c*tile_spacing_x,
                                 self.min_y + r*tile_spacing_y,
                                 width=tile_width, height=tile_height)

                # The unique tile has overlap regions removed
                # and is always constrained by the rectangle size
                unique_tile = copy.copy(tile)
                if c > 0:
                    unique_tile.min_x += half_overlap[0]
                if r > 0:
                    unique_tile.min_y += half_overlap[1]
                if c < num_tiles[0]-1:
                    unique_tile.max_x -= half_overlap[0]
                if r < num_tiles[1]-1:
                    unique_tile.max_y -= half_overlap[1]
                unique_tile = unique_tile.get_intersection(self)

                if include_partials: # Crop the tile to the valid area and use it
                    tile = tile.get_intersection(self)
                    if tile.width() < min_width or tile.height() < min_height:
                        continue
                else: # Only use it if the uncropped tile fits entirely in this Rectangle
                    if not containing_rect.contains_rect(tile):
                        if not partials_overlap:
                            continue
                        # Try shifting tile "back" in x/y so that we can fit the entire proposed tile
                        new_max_x = tile.max_x
                        new_max_y = tile.max_y
                        if tile.max_x > containing_rect.max_x:
                            new_max_x = min(containing_rect.max_x, tile.max_x)
                        if tile.max_y > containing_rect.max_y:
                            new_max_y = min(containing_rect.max_y, tile.max_y)

                        tile = Rectangle(new_max_x - tile_width, new_max_y - tile_height,
                                         width=tile_width, height=tile_height)
                        if not containing_rect.contains_rect(tile):
                            continue
                if by_block:
                    row_tiles.append(tile)
                    unique_row_tiles.append(unique_tile)
                else:
                    output_tiles.append(tile)
                    unique_tiles.append(unique_tile)

            if by_block and row_tiles:
                row_rect = Rectangle(row_tiles[0].min_x, row_tiles[0].min_y,
                                     row_tiles[-1].max_x, row_tiles[-1].max_y)
                for t in row_tiles:
                    t.shift(-row_rect.min_x, -row_rect.min_y)
                for t in unique_row_tiles:
                    t.shift(-row_rect.min_x, -row_rect.min_y)
                output_tiles.append((row_rect, row_tiles))
                unique_tiles.append((row_rect, unique_row_tiles))

        return output_tiles, unique_tiles

    def make_tile_rois_yx(self, tile_shape, overlap_shape=(0, 0), include_partials=True, min_shape=(0, 0),
                          partials_overlap=False, by_block=False, containing_rect=None):
        '''As make_tile_rois but using a (y,x) input format instead of (x,y)'''
        return self.make_tile_rois((tile_shape[1], tile_shape[0]),
                                   (overlap_shape[1], overlap_shape[0]),
                                   include_partials, (min_shape[1], min_shape[0]),
                                   partials_overlap, by_block, containing_rect)

Classes

class Rectangle (min_x, min_y, max_x=0, max_y=0, width=0, height=0)

Simple rectangle class for ROIs. Max values are NON-INCLUSIVE. When using it, stay consistent with float or integer values.

Parameters

min_x : int
 
min_y : int
 
max_x : int
 
max_y : int
Rectangle bounds.
width : int
 
height : int
Specify width / height to use these instead of max_x/max_y.
Expand source code
class Rectangle:
    """
    Simple rectangle class for ROIs. Max values are NON-INCLUSIVE.
    When using it, stay consistent with float or integer values.
    """
    def __init__(self, min_x, min_y, max_x=0, max_y=0,
                 width=0, height=0):
        """
        Parameters
        ----------
        min_x: int
        min_y: int
        max_x: int
        max_y: int
            Rectangle bounds.
        width: int
        height: int
            Specify width / height to use these instead of max_x/max_y.
        """
        self.min_x = min_x
        self.min_y = min_y
        if width > 0:
            self.max_x = min_x + width
        else:
            self.max_x = max_x
        if height > 0:
            self.max_y = min_y + height
        else:
            self.max_y = max_y

    def __str__(self):
        if isinstance(self.min_x, int):
            return ('min_x: %d, max_x: %d, min_y: %d, max_y: %d' %
                    (self.min_x, self.max_x, self.min_y, self.max_y))
        return ('min_x: %f, max_x: %f, min_y: %f, max_y: %f' %
                (self.min_x, self.max_x, self.min_y, self.max_y))

    def __repr__(self):
        return self.__str__()

#    def indexGenerator(self):
#        '''Generator function used to iterate over all integer indices.
#           Only use this with integer boundaries!'''
#        for row in range(self.min_y, self.max_y):
#            for col in range(self.min_x, self.max_x):
#                yield(TileIndex(row,col))

    def bounds(self):
        """
        Returns
        -------
        (int, int, int, int):
            (min_x, max_x, min_y, max_y)
        """
        return (self.min_x, self.max_x, self.min_y, self.max_y)

    def width(self):
        return self.max_x - self.min_x
    def height(self):
        return self.max_y - self.min_y

    def has_area(self):
        """
        Returns
        -------
        bool:
            true if the rectangle contains any area.
        """
        return (self.width() > 0) and (self.height() > 0)

    def perimeter(self):
        return 2*self.width() + 2*self.height()

    def area(self):
        if not self.has_area():
            return 0
        return self.height() * self.width()

    def get_min_coord(self):
        return (self.min_x, self.min_y)
    def get_max_coord(self):
        return (self.max_x, self.max_y)

    def shift(self, dx, dy):
        '''Shifts the entire box'''
        self.min_x += dx
        self.max_x += dx
        self.min_y += dy
        self.max_y += dy

    def scale_by_constant(self, xScale, yScale):
        '''Scale the units by a constant'''
        if yScale is None:
            yScale = xScale
        self.min_x *= xScale
        self.max_x *= xScale
        self.min_y *= yScale
        self.max_y *= yScale

    def expand(self, left, down, right=None, up=None):
        '''Expand the box by an amount in each direction'''
        self.min_x -= left
        self.min_y -= down
        if right is None: # If right and up are not passed in, use left and down for both sides.
            right = left
        if up is None:
            up = down
        self.max_x += right
        self.max_y += up

    def expand_to_contain_pt(self, x, y):
        '''Expands the rectangle to contain the given point'''
        if isinstance(self.min_x, float):
            delta = 0.001
        else: # These are needed because the upper bound is non-exclusive.
            delta = 1
        if x < self.min_x: self.min_x = x
        if y < self.min_y: self.min_y = y
        if x > self.max_x: self.max_x = x + delta
        if y > self.max_y: self.max_y = y + delta

    def expand_to_contain_rect(self, other_rect):
        '''Expands the rectangle to contain the given rectangle'''

        if other_rect.min_x < self.min_x: self.min_x = other_rect.min_x
        if other_rect.min_y < self.min_y: self.min_y = other_rect.min_y
        if other_rect.max_x > self.max_x: self.max_x = other_rect.max_x
        if other_rect.max_y > self.max_y: self.max_y = other_rect.max_y

    def get_intersection(self, other_rect):
        '''Returns the overlapping region of two rectangles'''
        overlap = Rectangle(max(self.min_x, other_rect.min_x),
                            max(self.min_y, other_rect.min_y),
                            min(self.max_x, other_rect.max_x),
                            min(self.max_y, other_rect.max_y))
        return overlap

    def contains_pt(self, x, y):
        '''Returns true if this rect contains the given point'''
        if self.min_x > x: return False
        if self.min_y > y: return False
        if self.max_x < x: return False
        if self.max_y < y: return False
        return True

    def contains_rect(self, other_rect):
        '''Returns true if this rect contains all of the other rect'''
        if self.min_x > other_rect.min_x: return False
        if self.min_y > other_rect.min_y: return False
        if self.max_x < other_rect.max_x: return False
        if self.max_y < other_rect.max_y: return False
        return True

    def overlaps(self, other_rect):
        '''Returns true if there is any overlap between this and another rectangle'''
        overlap_area = self.get_intersection(other_rect)
        return overlap_area.has_area()

    def make_tile_rois(self, tile_shape, overlap_shape=(0, 0), include_partials=True, min_shape=(0, 0), #pylint: disable=R0912,R0914
                       partials_overlap=False, by_block=False, containing_rect=None):
        """
        Return a list of tiles encompassing the entire area of this Rectangle.

        Parameters
        ----------
        tile_shape: (int, int)
            Shape of each tile (width, height)
        overlap_shape: (int, int)
            Amount to overlap tiles in x and y direction
        include_partials: bool
            If true, include partial tiles at the edge of the image.
        min_shape: (int, int)
            If true and `partials` is true, keep partial tiles of this minimum size.
        partials_overlap: bool
            If `partials` is false, and this is true, expand partial tiles
            to the desired size. Tiles may overlap in some areas.
        by_block: bool
            If true, changes the returned generator to group tiles by block.
            This is intended to optimize disk reads by reading the entire block at once.
        containing_rect: Rectangle
            Tiles are restricted to fit inside this rectangle instead of "self".

        Returns
        -------
        List[Rectangle]:
            Generator yielding ROIs. If `by_block` is true, returns a generator of (Rectangle, List[Rectangle])
            instead, where the first rectangle is a larger block containing multiple tiles in a list.
        List[Rectangle]:
            Same as the first output, but the ROIs do not include any overlap regions or any area outside the rectangle.
        """
        tile_width, tile_height = tile_shape
        min_width, min_height = min_shape
        if not containing_rect:
            containing_rect = self

        half_overlap = (overlap_shape[0] // 2, overlap_shape[1] // 2)
        tile_spacing_x = tile_width  - overlap_shape[0]
        tile_spacing_y = tile_height - overlap_shape[1]
        num_tiles_round_up = (int(math.ceil((self.width() - overlap_shape[0]) / tile_spacing_x)),
                              int(math.ceil((self.height()- overlap_shape[1]) / tile_spacing_y)))
        if include_partials or partials_overlap:
            num_tiles = num_tiles_round_up
        else:
            containing_rect_tiles = (int(math.floor((containing_rect.width() - overlap_shape[0]) / tile_spacing_x)),
                                     int(math.floor((containing_rect.height()- overlap_shape[1]) / tile_spacing_y)))
            num_tiles = (min(num_tiles_round_up[0], containing_rect_tiles[0]),
                         min(num_tiles_round_up[1], containing_rect_tiles[1]))

        output_tiles = []
        unique_tiles = []
        for r in range(0, num_tiles[1]):
            row_tiles = []
            unique_row_tiles = []
            for c in range(0, num_tiles[0]):
                tile = Rectangle(self.min_x + c*tile_spacing_x,
                                 self.min_y + r*tile_spacing_y,
                                 width=tile_width, height=tile_height)

                # The unique tile has overlap regions removed
                # and is always constrained by the rectangle size
                unique_tile = copy.copy(tile)
                if c > 0:
                    unique_tile.min_x += half_overlap[0]
                if r > 0:
                    unique_tile.min_y += half_overlap[1]
                if c < num_tiles[0]-1:
                    unique_tile.max_x -= half_overlap[0]
                if r < num_tiles[1]-1:
                    unique_tile.max_y -= half_overlap[1]
                unique_tile = unique_tile.get_intersection(self)

                if include_partials: # Crop the tile to the valid area and use it
                    tile = tile.get_intersection(self)
                    if tile.width() < min_width or tile.height() < min_height:
                        continue
                else: # Only use it if the uncropped tile fits entirely in this Rectangle
                    if not containing_rect.contains_rect(tile):
                        if not partials_overlap:
                            continue
                        # Try shifting tile "back" in x/y so that we can fit the entire proposed tile
                        new_max_x = tile.max_x
                        new_max_y = tile.max_y
                        if tile.max_x > containing_rect.max_x:
                            new_max_x = min(containing_rect.max_x, tile.max_x)
                        if tile.max_y > containing_rect.max_y:
                            new_max_y = min(containing_rect.max_y, tile.max_y)

                        tile = Rectangle(new_max_x - tile_width, new_max_y - tile_height,
                                         width=tile_width, height=tile_height)
                        if not containing_rect.contains_rect(tile):
                            continue
                if by_block:
                    row_tiles.append(tile)
                    unique_row_tiles.append(unique_tile)
                else:
                    output_tiles.append(tile)
                    unique_tiles.append(unique_tile)

            if by_block and row_tiles:
                row_rect = Rectangle(row_tiles[0].min_x, row_tiles[0].min_y,
                                     row_tiles[-1].max_x, row_tiles[-1].max_y)
                for t in row_tiles:
                    t.shift(-row_rect.min_x, -row_rect.min_y)
                for t in unique_row_tiles:
                    t.shift(-row_rect.min_x, -row_rect.min_y)
                output_tiles.append((row_rect, row_tiles))
                unique_tiles.append((row_rect, unique_row_tiles))

        return output_tiles, unique_tiles

    def make_tile_rois_yx(self, tile_shape, overlap_shape=(0, 0), include_partials=True, min_shape=(0, 0),
                          partials_overlap=False, by_block=False, containing_rect=None):
        '''As make_tile_rois but using a (y,x) input format instead of (x,y)'''
        return self.make_tile_rois((tile_shape[1], tile_shape[0]),
                                   (overlap_shape[1], overlap_shape[0]),
                                   include_partials, (min_shape[1], min_shape[0]),
                                   partials_overlap, by_block, containing_rect)

Methods

def area(self)
Expand source code
def area(self):
    if not self.has_area():
        return 0
    return self.height() * self.width()
def bounds(self)

Returns

(int, int, int, int): (min_x, max_x, min_y, max_y)

Expand source code
def bounds(self):
    """
    Returns
    -------
    (int, int, int, int):
        (min_x, max_x, min_y, max_y)
    """
    return (self.min_x, self.max_x, self.min_y, self.max_y)
def contains_pt(self, x, y)

Returns true if this rect contains the given point

Expand source code
def contains_pt(self, x, y):
    '''Returns true if this rect contains the given point'''
    if self.min_x > x: return False
    if self.min_y > y: return False
    if self.max_x < x: return False
    if self.max_y < y: return False
    return True
def contains_rect(self, other_rect)

Returns true if this rect contains all of the other rect

Expand source code
def contains_rect(self, other_rect):
    '''Returns true if this rect contains all of the other rect'''
    if self.min_x > other_rect.min_x: return False
    if self.min_y > other_rect.min_y: return False
    if self.max_x < other_rect.max_x: return False
    if self.max_y < other_rect.max_y: return False
    return True
def expand(self, left, down, right=None, up=None)

Expand the box by an amount in each direction

Expand source code
def expand(self, left, down, right=None, up=None):
    '''Expand the box by an amount in each direction'''
    self.min_x -= left
    self.min_y -= down
    if right is None: # If right and up are not passed in, use left and down for both sides.
        right = left
    if up is None:
        up = down
    self.max_x += right
    self.max_y += up
def expand_to_contain_pt(self, x, y)

Expands the rectangle to contain the given point

Expand source code
def expand_to_contain_pt(self, x, y):
    '''Expands the rectangle to contain the given point'''
    if isinstance(self.min_x, float):
        delta = 0.001
    else: # These are needed because the upper bound is non-exclusive.
        delta = 1
    if x < self.min_x: self.min_x = x
    if y < self.min_y: self.min_y = y
    if x > self.max_x: self.max_x = x + delta
    if y > self.max_y: self.max_y = y + delta
def expand_to_contain_rect(self, other_rect)

Expands the rectangle to contain the given rectangle

Expand source code
def expand_to_contain_rect(self, other_rect):
    '''Expands the rectangle to contain the given rectangle'''

    if other_rect.min_x < self.min_x: self.min_x = other_rect.min_x
    if other_rect.min_y < self.min_y: self.min_y = other_rect.min_y
    if other_rect.max_x > self.max_x: self.max_x = other_rect.max_x
    if other_rect.max_y > self.max_y: self.max_y = other_rect.max_y
def get_intersection(self, other_rect)

Returns the overlapping region of two rectangles

Expand source code
def get_intersection(self, other_rect):
    '''Returns the overlapping region of two rectangles'''
    overlap = Rectangle(max(self.min_x, other_rect.min_x),
                        max(self.min_y, other_rect.min_y),
                        min(self.max_x, other_rect.max_x),
                        min(self.max_y, other_rect.max_y))
    return overlap
def get_max_coord(self)
Expand source code
def get_max_coord(self):
    return (self.max_x, self.max_y)
def get_min_coord(self)
Expand source code
def get_min_coord(self):
    return (self.min_x, self.min_y)
def has_area(self)

Returns

bool:
true if the rectangle contains any area.
Expand source code
def has_area(self):
    """
    Returns
    -------
    bool:
        true if the rectangle contains any area.
    """
    return (self.width() > 0) and (self.height() > 0)
def height(self)
Expand source code
def height(self):
    return self.max_y - self.min_y
def make_tile_rois(self, tile_shape, overlap_shape=(0, 0), include_partials=True, min_shape=(0, 0), partials_overlap=False, by_block=False, containing_rect=None)

Return a list of tiles encompassing the entire area of this Rectangle.

Parameters

tile_shape : (int, int)
Shape of each tile (width, height)
overlap_shape : (int, int)
Amount to overlap tiles in x and y direction
include_partials : bool
If true, include partial tiles at the edge of the image.
min_shape : (int, int)
If true and partials is true, keep partial tiles of this minimum size.
partials_overlap : bool
If partials is false, and this is true, expand partial tiles to the desired size. Tiles may overlap in some areas.
by_block : bool
If true, changes the returned generator to group tiles by block. This is intended to optimize disk reads by reading the entire block at once.
containing_rect : Rectangle
Tiles are restricted to fit inside this rectangle instead of "self".

Returns

List[Rectangle]:
Generator yielding ROIs. If by_block is true, returns a generator of (Rectangle, List[Rectangle]) instead, where the first rectangle is a larger block containing multiple tiles in a list.
List[Rectangle]:
Same as the first output, but the ROIs do not include any overlap regions or any area outside the rectangle.
Expand source code
def make_tile_rois(self, tile_shape, overlap_shape=(0, 0), include_partials=True, min_shape=(0, 0), #pylint: disable=R0912,R0914
                   partials_overlap=False, by_block=False, containing_rect=None):
    """
    Return a list of tiles encompassing the entire area of this Rectangle.

    Parameters
    ----------
    tile_shape: (int, int)
        Shape of each tile (width, height)
    overlap_shape: (int, int)
        Amount to overlap tiles in x and y direction
    include_partials: bool
        If true, include partial tiles at the edge of the image.
    min_shape: (int, int)
        If true and `partials` is true, keep partial tiles of this minimum size.
    partials_overlap: bool
        If `partials` is false, and this is true, expand partial tiles
        to the desired size. Tiles may overlap in some areas.
    by_block: bool
        If true, changes the returned generator to group tiles by block.
        This is intended to optimize disk reads by reading the entire block at once.
    containing_rect: Rectangle
        Tiles are restricted to fit inside this rectangle instead of "self".

    Returns
    -------
    List[Rectangle]:
        Generator yielding ROIs. If `by_block` is true, returns a generator of (Rectangle, List[Rectangle])
        instead, where the first rectangle is a larger block containing multiple tiles in a list.
    List[Rectangle]:
        Same as the first output, but the ROIs do not include any overlap regions or any area outside the rectangle.
    """
    tile_width, tile_height = tile_shape
    min_width, min_height = min_shape
    if not containing_rect:
        containing_rect = self

    half_overlap = (overlap_shape[0] // 2, overlap_shape[1] // 2)
    tile_spacing_x = tile_width  - overlap_shape[0]
    tile_spacing_y = tile_height - overlap_shape[1]
    num_tiles_round_up = (int(math.ceil((self.width() - overlap_shape[0]) / tile_spacing_x)),
                          int(math.ceil((self.height()- overlap_shape[1]) / tile_spacing_y)))
    if include_partials or partials_overlap:
        num_tiles = num_tiles_round_up
    else:
        containing_rect_tiles = (int(math.floor((containing_rect.width() - overlap_shape[0]) / tile_spacing_x)),
                                 int(math.floor((containing_rect.height()- overlap_shape[1]) / tile_spacing_y)))
        num_tiles = (min(num_tiles_round_up[0], containing_rect_tiles[0]),
                     min(num_tiles_round_up[1], containing_rect_tiles[1]))

    output_tiles = []
    unique_tiles = []
    for r in range(0, num_tiles[1]):
        row_tiles = []
        unique_row_tiles = []
        for c in range(0, num_tiles[0]):
            tile = Rectangle(self.min_x + c*tile_spacing_x,
                             self.min_y + r*tile_spacing_y,
                             width=tile_width, height=tile_height)

            # The unique tile has overlap regions removed
            # and is always constrained by the rectangle size
            unique_tile = copy.copy(tile)
            if c > 0:
                unique_tile.min_x += half_overlap[0]
            if r > 0:
                unique_tile.min_y += half_overlap[1]
            if c < num_tiles[0]-1:
                unique_tile.max_x -= half_overlap[0]
            if r < num_tiles[1]-1:
                unique_tile.max_y -= half_overlap[1]
            unique_tile = unique_tile.get_intersection(self)

            if include_partials: # Crop the tile to the valid area and use it
                tile = tile.get_intersection(self)
                if tile.width() < min_width or tile.height() < min_height:
                    continue
            else: # Only use it if the uncropped tile fits entirely in this Rectangle
                if not containing_rect.contains_rect(tile):
                    if not partials_overlap:
                        continue
                    # Try shifting tile "back" in x/y so that we can fit the entire proposed tile
                    new_max_x = tile.max_x
                    new_max_y = tile.max_y
                    if tile.max_x > containing_rect.max_x:
                        new_max_x = min(containing_rect.max_x, tile.max_x)
                    if tile.max_y > containing_rect.max_y:
                        new_max_y = min(containing_rect.max_y, tile.max_y)

                    tile = Rectangle(new_max_x - tile_width, new_max_y - tile_height,
                                     width=tile_width, height=tile_height)
                    if not containing_rect.contains_rect(tile):
                        continue
            if by_block:
                row_tiles.append(tile)
                unique_row_tiles.append(unique_tile)
            else:
                output_tiles.append(tile)
                unique_tiles.append(unique_tile)

        if by_block and row_tiles:
            row_rect = Rectangle(row_tiles[0].min_x, row_tiles[0].min_y,
                                 row_tiles[-1].max_x, row_tiles[-1].max_y)
            for t in row_tiles:
                t.shift(-row_rect.min_x, -row_rect.min_y)
            for t in unique_row_tiles:
                t.shift(-row_rect.min_x, -row_rect.min_y)
            output_tiles.append((row_rect, row_tiles))
            unique_tiles.append((row_rect, unique_row_tiles))

    return output_tiles, unique_tiles
def make_tile_rois_yx(self, tile_shape, overlap_shape=(0, 0), include_partials=True, min_shape=(0, 0), partials_overlap=False, by_block=False, containing_rect=None)

As make_tile_rois but using a (y,x) input format instead of (x,y)

Expand source code
def make_tile_rois_yx(self, tile_shape, overlap_shape=(0, 0), include_partials=True, min_shape=(0, 0),
                      partials_overlap=False, by_block=False, containing_rect=None):
    '''As make_tile_rois but using a (y,x) input format instead of (x,y)'''
    return self.make_tile_rois((tile_shape[1], tile_shape[0]),
                               (overlap_shape[1], overlap_shape[0]),
                               include_partials, (min_shape[1], min_shape[0]),
                               partials_overlap, by_block, containing_rect)
def overlaps(self, other_rect)

Returns true if there is any overlap between this and another rectangle

Expand source code
def overlaps(self, other_rect):
    '''Returns true if there is any overlap between this and another rectangle'''
    overlap_area = self.get_intersection(other_rect)
    return overlap_area.has_area()
def perimeter(self)
Expand source code
def perimeter(self):
    return 2*self.width() + 2*self.height()
def scale_by_constant(self, xScale, yScale)

Scale the units by a constant

Expand source code
def scale_by_constant(self, xScale, yScale):
    '''Scale the units by a constant'''
    if yScale is None:
        yScale = xScale
    self.min_x *= xScale
    self.max_x *= xScale
    self.min_y *= yScale
    self.max_y *= yScale
def shift(self, dx, dy)

Shifts the entire box

Expand source code
def shift(self, dx, dy):
    '''Shifts the entire box'''
    self.min_x += dx
    self.max_x += dx
    self.min_y += dy
    self.max_y += dy
def width(self)
Expand source code
def width(self):
    return self.max_x - self.min_x