diff --git a/src/tile2net/raster/grid.py b/src/tile2net/raster/grid.py index 9bc33c6..0428ac9 100644 --- a/src/tile2net/raster/grid.py +++ b/src/tile2net/raster/grid.py @@ -4,7 +4,6 @@ import pandas as pd import datetime import tempfile -from pathlib import Path from typing import Dict, Any, Union import shapely import rasterio @@ -23,7 +22,7 @@ deg2num, num2deg, createfolder, ) from tile2net.raster.tile_utils.geodata_utils import ( - _reduce_geom_precision, list_to_affine, read_gdf, buff_dfs, to_metric, + _reduce_geom_precision, list_to_affine, read_gdf, buff_dfs, ) import logging from tile2net.raster.project import Project @@ -67,7 +66,7 @@ def from_bounding_box(self): 'topLeft_lat, topLeft_lon, bottomRight_lat, bottomRight_lon' ) - def round_loc(self, other: list = None) -> list[float]: + def round_loc(self, other: list | float = None) -> list[float]: if other: return list(np.around(np.array(other), 10)) else: @@ -599,11 +598,13 @@ def save_ntw_polygon(self, crs_metric: int = 3857): poly_network.geometry = poly_network.simplify(0.6) unioned = buff_dfs(poly_network) unioned.geometry = unioned.geometry.simplify(0.9) - unioned.dropna(inplace=True) unioned = unioned[unioned.geometry.notna()] unioned['geometry'] = unioned.apply(fill_holes, args=(25,), axis=1) simplified = replace_convexhull(unioned) + simplified = simplified[simplified.geometry.notna()] + simplified = simplified[['geometry', 'f_type']] simplified.to_crs(self.crs, inplace=True) + self.ntw_poly = simplified simplified.to_file( os.path.join(poly_fold, f'{self.name}-Polygons-{datetime.datetime.now().strftime("%d-%m-%Y_%H")}')) diff --git a/src/tile2net/raster/tile_utils/geodata_utils.py b/src/tile2net/raster/tile_utils/geodata_utils.py index 961f200..c58cf27 100644 --- a/src/tile2net/raster/tile_utils/geodata_utils.py +++ b/src/tile2net/raster/tile_utils/geodata_utils.py @@ -1,7 +1,9 @@ import os + +import affine from tile2net.logger import logger import shapely -import geopandas as gpd +from geopandas import GeoDataFrame, read_file, sjoin import pandas as pd import numpy as np import rasterio @@ -9,6 +11,10 @@ import skimage from affine import Affine +import warnings + +warnings.filterwarnings('ignore') + def read_gdf(path): """ @@ -22,7 +28,7 @@ def read_gdf(path): ------- gdf: GeoDataFrame """ - gdf = gpd.read_file(path) + gdf = read_file(path) return gdf @@ -59,7 +65,7 @@ def change_crs(gdf, crs): return gdf -def prepare_spindex(gdf: gpd.GeoDataFrame): +def prepare_spindex(gdf: GeoDataFrame) -> GeoDataFrame.sindex: """ Prepare a GeoDataFrame for spatial indexing Parameters @@ -94,14 +100,13 @@ def _reduce_geom_precision(geom, precision=2): return shape(geojson) -def affine_to_list(affine_obj): +def affine_to_list(affine_obj: affine.Affine): """Convert a :class:`affine.Affine` instance to a list for Shapely.""" return [affine_obj.a, affine_obj.b, affine_obj.d, affine_obj.e, affine_obj.xoff, affine_obj.yoff] - -def list_to_affine(xform_mat): +def list_to_affine(xform_mat: list): """Create an Affine from a list or array-formatted [a, b, d, e, xoff, yoff] Arguments @@ -147,7 +152,7 @@ def _check_skimage_im_load(im): ) -def prepare_class_gdf(polys, class_name) -> object: +def prepare_class_gdf(polys, class_name) -> GeoDataFrame: """ separates the polygons of each class, given the keyboard (sidewalk, crosswalk, road) Args: @@ -196,9 +201,9 @@ def read_dataframe(src_path, geo=True, cols=None): """ if geo: if cols: - df = gpd.read_file(src_path, usecols=cols) + df = read_file(src_path, usecols=cols) else: - df = gpd.read_file(src_path) + df = read_file(src_path) else: if cols: df = pd.read_csv(src_path, usecols=cols) @@ -207,20 +212,10 @@ def read_dataframe(src_path, geo=True, cols=None): return df -# def unary_multi(gdf): -# """ -# handles the errors with multipolygon -# """ -# if gdf.unary_union.type == 'MultiPolygon': -# gdf_uni = gpd.GeoDataFrame(geometry=gpd.GeoSeries([geom for geom in gdf.unary_union.geoms])) -# else: -# gdf_uni = gpd.GeoDataFrame(geometry=gpd.GeoSeries(gdf.unary_union)) -# return gdf_uni - -def unary_multi(gdf: gpd.GeoDataFrame) -> gpd.GeoDataFrame: +def unary_multi(gdf: GeoDataFrame) -> GeoDataFrame: # handles the errors with multipolygon loc = ~gdf.is_valid.values - logger.warning(f'Number of invalid geometries: {loc.sum()} out of {len(gdf)}') + # logger.warning(f'Number of invalid geometries: {loc.sum()} out of {len(gdf)}') gdf.geometry.loc[loc] = shapely.make_valid(gdf.geometry.loc[loc]) result = ( gdf @@ -257,15 +252,38 @@ def buffer_union(gdf, buff, simp1, simp2): return gdf_uni -def buffer_union_erode(gdf, buff, erode, simp1, simp2, simp3): +def buffer_union_erode(gdf: GeoDataFrame, buff: float, erode: float, simp1: float, simp2: float, simp3: float): + """ Buffer, union, erode, simplify the polygons in a GeoDataFrame to create elongated polygons + Parameters + ---------- + gdf: GeoDataFrame + Polygon GeoDataFrame + buff: float + buffer distance + erode: float + erode distance + simp1: float + simplification tolerance for the buffer + simp2: float + simplification tolerance for the union + simp3: float + simplification tolerance for the final simplification + Returns + ------- + gdf_uni: GeoDataFrame + Transformed GeoDataFrame + """ gdf_buff = buffer_union(gdf, buff, simp1, simp2) gdf_erode = gdf_buff.copy() gdf_erode.geometry = gdf_buff.geometry.buffer(erode, join_style=2, cap_style=3) gdf_uni = unary_multi(gdf_erode) gdf_uni.geometry = gdf_uni.geometry.set_crs(3857) gdf_uni.geometry = gdf_uni.geometry.simplify(simp3) + gdf_uni = gdf_uni[(gdf_uni.geometry.geom_type == 'MultiPolygon') | (gdf_uni.geometry.geom_type == 'Polygon')] + return gdf_uni + def to_metric(gdf, crs=3857): """Converts a GeoDataFrame to metric (3857) coordinate Parameters @@ -283,7 +301,7 @@ def to_metric(gdf, crs=3857): return gdf -def geo2geodf(geo_lst): +def geo2geodf(geo_lst: list) -> GeoDataFrame: """ Converts a list of shapely geometries to a GeoDataFrame Parameters @@ -295,22 +313,23 @@ def geo2geodf(geo_lst): gdf: GeoDataFrame """ - gdf = gpd.GeoDataFrame(geometry=geo_lst) + gdf = GeoDataFrame(geometry=geo_lst) return gdf -def merge_dfs(gdf1, gdf2, crs=4326): +def merge_dfs(gdf1: GeoDataFrame, gdf2: GeoDataFrame, crs: int = 4326): """ - merges two dataframes with the results of segmentation (three classes) + merges two geodataframes of segmentation results (three classes) Parameters ---------- gdf1: GeoDataFrame gdf2: GeoDataFrame crs: int - + Coordinate system to convert to Returns ------- - + concsw: GeoDataFrame + Concatenated GeoDataFrames """ if gdf1.crs != gdf2.crs: gdf1.to_crs(crs, inplace=True) @@ -349,16 +368,18 @@ def merge_dfs(gdf1, gdf2, crs=4326): return merged -def create_stats(gdf): - """ - +def create_stats(gdf: GeoDataFrame): + """ Creates summary statistics of the polygons Parameters ---------- gdf: GeoDataFrame Returns ------- - + ss: GeoDataFrame + summary statistics of the polygons + cgdf: GeoDataFrame + copy of the input GeoDataFrame with additional columns of the summary statistics """ cgdf = gdf.copy() cgdf['primeter'] = cgdf.length @@ -369,7 +390,7 @@ def create_stats(gdf): return ss, cgdf -def buff_dfs(gdf): +def buff_dfs(gdf: GeoDataFrame): """ union and buffer the polygons of each class separately, to create continuous polygons and merge them into one GeoDataFrame.