Skip to content

Commit

Permalink
refactoring and renaming
Browse files Browse the repository at this point in the history
- refactor simplify and reproject modules
- rename extent to geometry
  • Loading branch information
pjhartzell committed Aug 7, 2023
1 parent 15cf3df commit 8019cd5
Show file tree
Hide file tree
Showing 12 changed files with 183 additions and 273 deletions.
20 changes: 8 additions & 12 deletions src/raster_footprint/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from .densify import (
densify_by_distance,
densify_by_factor,
densify_extent,
densify_geometry,
densify_multipolygon,
densify_polygon,
)
Expand All @@ -11,26 +11,22 @@
footprint_from_mask,
footprint_from_rasterio_reader,
)
from .mask import create_mask, get_mask_extent
from .reproject import reproject_extent, reproject_multipolygon, reproject_polygon
from .simplify import simplify_extent, simplify_multipolygon, simplify_polygon
from .mask import create_mask, get_mask_geometry
from .reproject import reproject_geometry
from .simplify import simplify_geometry

__all__ = [
"footprint_from_data",
"footprint_from_href",
"footprint_from_mask",
"footprint_from_rasterio_reader",
"create_mask",
"get_mask_extent",
"get_mask_geometry",
"densify_by_distance",
"densify_by_factor",
"densify_polygon",
"densify_multipolygon",
"densify_extent",
"reproject_polygon",
"reproject_multipolygon",
"reproject_extent",
"simplify_polygon",
"simplify_multipolygon",
"simplify_extent",
"densify_geometry",
"reproject_geometry",
"simplify_geometry",
]
16 changes: 8 additions & 8 deletions src/raster_footprint/densify.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,16 +149,16 @@ def densify_multipolygon(
return MultiPolygon(densified_polygons)


def densify_extent(
extent: T,
def densify_geometry(
geometry: T,
*,
factor: Optional[int] = None,
distance: Optional[float] = None,
) -> T:
"""Adds vertices to a polygon or each polygon in a multipolygon.
Args:
extent (T): The polygon or multipolygon to densify.
geometry (T): The polygon or multipolygon to densify.
factor (Optional[int]): The factor by which to increase the number of
polygon vertices, e.g., a ``factor`` of 2 will double the number of
vertices. Mutually exclusive with ``distance``. Defaults to ``None``.
Expand All @@ -170,9 +170,9 @@ def densify_extent(
Returns:
T: The densified polygon or multipolygon.
"""
if isinstance(extent, Polygon):
return densify_polygon(extent, factor=factor, distance=distance)
elif isinstance(extent, MultiPolygon):
return densify_multipolygon(extent, factor=factor, distance=distance)
if isinstance(geometry, Polygon):
return densify_polygon(geometry, factor=factor, distance=distance)
elif isinstance(geometry, MultiPolygon):
return densify_multipolygon(geometry, factor=factor, distance=distance)
else:
raise TypeError("extent must be a Polygon or MultiPolygon")
raise TypeError("geometry must be a Polygon or MultiPolygon")
82 changes: 53 additions & 29 deletions src/raster_footprint/footprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,49 +4,52 @@
import rasterio
from numpy import typing as npt
from rasterio import Affine
from rasterio.io import DatasetReader
from rasterio.crs import CRS
from rasterio.io import DatasetReader
from shapely.geometry import mapping

from .densify import densify_extent
from .mask import create_mask, get_mask_extent
from .reproject import DEFAULT_PRECISION, reproject_extent
from .simplify import simplify_extent
from .densify import densify_geometry
from .mask import create_mask, get_mask_geometry
from .reproject import DEFAULT_PRECISION, reproject_geometry
from .simplify import simplify_geometry

# TODO: change holes default to False


def footprint_from_mask(
mask: npt.NDArray[np.uint8],
crs: CRS,
transform: Affine,
source_crs: CRS,
*,
destination_crs: CRS = CRS.from_epsg(4326),
precision: int = DEFAULT_PRECISION,
densify_factor: Optional[int] = None,
densify_distance: Optional[float] = None,
simplify_tolerance: Optional[float] = None,
convex_hull: bool = False,
holes: bool = True,
) -> Optional[Dict[str, Any]]:
"""Produces a GeoJSON dictionary containing a polygon or multipolygon
"""Produces a GeoJSON dictionary containing a polygon or multipolygon geometry
surrounding valid data locations in the given ``mask`` array.
A polygon or multipolygon surrounding valid data pixels is extracted from
the given ``mask`` array. The polygon(s) are densified with additional
vertices, reprojected to the WGS84 (EPSG:4326) coordinate system, and then
simplified by reducing the number of polygon vertices. Densifying the
polygon(s) prior to reprojection reduces projection distortion error.
Simplification removes vertices that are redundant in defining the
polygon(s) to within the given ``simplify_tolerance``.
A polygon or multipolygon geometry surrounding valid data pixels is extracted from
the given ``mask`` array. The geometry polygon(s) are densified with additional
vertices, reprojected to the ``destination_crs``, and then simplified by reducing
the number of vertices. Densifying the polygon(s) prior to reprojection reduces
projection distortion error. Simplification removes vertices that are redundant
in defining the polygon(s) to within the given ``simplify_tolerance``.
Args:
mask (NDArray[uint8]): A 2D NumPy array containing 0s and
255s for nodata/data (invalid/valid) pixels.
crs (CRS): A :class:`rasterio.crs.CRS` object defining the
coordinate reference system of the given ``mask``.
transform (Affine): An :class:`affine.Affine` object defining
the affine transformation from the ``mask`` pixel coordinate system
to the given ``crs`` coordinate system.
source_crs (CRS): A :class:`rasterio.crs.CRS` object defining the
coordinate reference system of the given ``mask``.
destination_crs (CRS): A :class:`rasterio.crs.CRS` object defining the
desired coordinate reference system of output footprint. Defaults to
EPSG:4326 (WGS84).
precision (Optional[int]): The number of decimal places to include in
the final footprint polygon vertex coordinates. Defaults to 7.
densify_factor (Optional[int]): The factor by which to increase the
Expand All @@ -70,22 +73,30 @@ def footprint_from_mask(
Optional[Dict[str, Any]]: A GeoJSON dictionary containing the
footprint polygon or multipolygon.
"""
extent = get_mask_extent(
geometry = get_mask_geometry(
mask, transform=transform, convex_hull=convex_hull, holes=holes
)
if extent is None:

if geometry is None:
return None
densified = densify_extent(extent, factor=densify_factor, distance=densify_distance)
reprojected = reproject_extent(densified, crs, precision=precision)
simplified = simplify_extent(reprojected, tolerance=simplify_tolerance)
return mapping(simplified) # type: ignore

densified = densify_geometry(
geometry, factor=densify_factor, distance=densify_distance
)
reprojected = reproject_geometry(
source_crs, destination_crs, densified, precision=precision
)
simplified = simplify_geometry(reprojected, tolerance=simplify_tolerance)

return mapping(simplified)


def footprint_from_data(
data: npt.NDArray[Any],
crs: CRS,
transform: Affine,
source_crs: CRS,
*,
destination_crs: CRS = CRS.from_epsg(4326),
nodata: Optional[Union[int, float]] = None,
precision: int = DEFAULT_PRECISION,
densify_factor: Optional[int] = None,
Expand All @@ -99,12 +110,14 @@ def footprint_from_data(
Args:
data (NDArray[Any]): A 2D or 3D NumPy array of raster data.
crs (CRS): A :class:`rasterio.crs.CRS` object defining the
coordinate reference system of the raster data in the given
``numpy_array``.
transform (Affine): An :class:`affine.Affine` object defining
the affine transformation from the ``data`` pixel coordinate system
the affine transformation from the ``mask`` pixel coordinate system
to the given ``crs`` coordinate system.
source_crs (CRS): A :class:`rasterio.crs.CRS` object defining the
coordinate reference system of the given ``mask``.
destination_crs (CRS): A :class:`rasterio.crs.CRS` object defining the
desired coordinate reference system of output footprint. Defaults to
EPSG:4326 (WGS84).
nodata (Optional[Union[int, float]]): The nodata value to use for
creating a data/nodata mask array. If not provided, a footprint for
the entire raster, including nodata pixels, is returned.
Expand Down Expand Up @@ -134,8 +147,9 @@ def footprint_from_data(
mask = create_mask(data, nodata=nodata)
return footprint_from_mask(
mask,
crs,
transform,
source_crs,
destination_crs=destination_crs,
precision=precision,
densify_factor=densify_factor,
densify_distance=densify_distance,
Expand All @@ -148,6 +162,7 @@ def footprint_from_data(
def footprint_from_href(
href: str,
*,
destination_crs: CRS = CRS.from_epsg(4326),
nodata: Optional[Union[int, float]] = None,
precision: int = DEFAULT_PRECISION,
densify_factor: Optional[int] = None,
Expand All @@ -167,6 +182,9 @@ def footprint_from_href(
Args:
href (str): An href to a raster data file.
destination_crs (CRS): A :class:`rasterio.crs.CRS` object defining the
desired coordinate reference system of output footprint. Defaults to
EPSG:4326 (WGS84).
nodata (Optional[Union[int, float]]): Explicitly sets the nodata value
to use for creating a data/nodata mask array. If not provided, the
nodata value in the source file metadata is used. If not provided
Expand Down Expand Up @@ -205,6 +223,7 @@ def footprint_from_href(
with rasterio.open(href) as source:
return footprint_from_rasterio_reader(
source,
destination_crs=destination_crs,
precision=precision,
densify_factor=densify_factor,
densify_distance=densify_distance,
Expand All @@ -220,6 +239,7 @@ def footprint_from_href(
def footprint_from_rasterio_reader(
reader: DatasetReader,
*,
destination_crs: CRS = CRS.from_epsg(4326),
nodata: Optional[Union[int, float]] = None,
precision: int = DEFAULT_PRECISION,
densify_factor: Optional[int] = None,
Expand All @@ -237,6 +257,9 @@ def footprint_from_rasterio_reader(
Args:
reader (DatasetReader): A :class:`rasterio.io.DatasetReader` object.
destination_crs (CRS): A :class:`rasterio.crs.CRS` object defining the
desired coordinate reference system of output footprint. Defaults to
EPSG:4326 (WGS84).
nodata (Optional[Union[int, float]]): Explicitly sets the nodata value
to use for creating a data/nodata mask array. If not provided, the
nodata value in the source file metadata is used. If not provided
Expand Down Expand Up @@ -300,8 +323,9 @@ def footprint_from_rasterio_reader(

return footprint_from_mask(
mask,
reader.crs,
reader.transform,
reader.crs,
destination_crs=destination_crs,
precision=precision,
densify_factor=densify_factor,
densify_distance=densify_distance,
Expand Down
41 changes: 20 additions & 21 deletions src/raster_footprint/mask.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,32 +30,31 @@ def create_mask(
"""
if data_array.ndim == 2:
data_array = data_array[np.newaxis, :]
shape = data_array.shape
array_shape = data_array.shape
if nodata is not None:
mask: npt.NDArray[np.uint8] = np.full(shape, fill_value=0, dtype=np.uint8)
mask: npt.NDArray[np.uint8] = np.full(array_shape, fill_value=0, dtype=np.uint8)
if np.isnan(nodata):
mask[~np.isnan(data_array)] = 1
else:
mask[data_array != nodata] = 1
mask = np.sum(mask, axis=0, dtype=np.uint8)
mask[mask > 0] = 255
else:
mask = np.full(shape[-2:], fill_value=255, dtype=np.uint8)
mask = np.full(array_shape[-2:], fill_value=255, dtype=np.uint8)
return mask


def get_mask_extent(
def get_mask_geometry(
mask: npt.NDArray[np.uint8],
*,
transform: Affine = Affine(1, 0, 0, 0, 1, 0),
convex_hull: bool = False,
holes: bool = True,
) -> Optional[Union[Polygon, MultiPolygon]]:
"""Creates a polygon or multipolygon surrounding valid data locations.
"""Creates a polygon or multipolygon surrounding valid data pixels.
Polygons are created around each contiguous region of valid data locations
in the given ``mask``. Valid data locations are defined as locations in the
mask with a value of 255.
Polygons are created around each contiguous region of valid data pixels
in the given ``mask``, where a valid data pixel has a value of 255.
Args:
mask (numpy.NDArray[numpy.uint8]): A 2D NumPy array containing 0s and 255s
Expand All @@ -71,36 +70,36 @@ def get_mask_extent(
Returns:
Optional[Union[Polygon, MultiPolygon]: A polygon or multipolygon
enclosing data pixels in the given ``mask``. The polygon vertex
enclosing valid data pixels in the given ``mask``. The polygon vertex
coordinates are transformed according to the given ``transform``.
"""
data_polygons = [
polygons = [
shape(polygon_dict)
for polygon_dict, region_value in rasterio.features.shapes(
mask, transform=transform
)
if region_value == 255
]

if not data_polygons:
if not polygons:
return None

if not holes and not convex_hull:
data_polygons = [Polygon(poly.exterior.coords) for poly in data_polygons]
unioned_polygons = unary_union(data_polygons)
polygons = [Polygon(poly.exterior.coords) for poly in polygons]
unioned_polygons = unary_union(polygons)
if isinstance(unioned_polygons, Polygon):
data_polygons = [unioned_polygons]
polygons = [unioned_polygons]
else:
data_polygons = list(unioned_polygons.geoms)
polygons = list(unioned_polygons.geoms)

data_polygons = [orient(poly) for poly in data_polygons]
polygons = [orient(poly) for poly in polygons]

if len(data_polygons) == 1:
data_extent = data_polygons[0]
if len(polygons) == 1:
geometry = polygons[0]
else:
data_extent = MultiPolygon(data_polygons)
geometry = MultiPolygon(polygons)

if convex_hull:
data_extent = orient(data_extent.convex_hull)
geometry = orient(geometry.convex_hull)

return data_extent
return geometry
Loading

0 comments on commit 8019cd5

Please sign in to comment.