geoai-py 0.2.3__py2.py3-none-any.whl → 0.3.1__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- geoai/__init__.py +1 -1
- geoai/extract.py +1006 -67
- geoai/geoai.py +1 -1
- geoai/preprocess.py +245 -2
- geoai/{common.py → utils.py} +463 -4
- {geoai_py-0.2.3.dist-info → geoai_py-0.3.1.dist-info}/METADATA +1 -1
- geoai_py-0.3.1.dist-info/RECORD +13 -0
- geoai_py-0.2.3.dist-info/RECORD +0 -13
- {geoai_py-0.2.3.dist-info → geoai_py-0.3.1.dist-info}/LICENSE +0 -0
- {geoai_py-0.2.3.dist-info → geoai_py-0.3.1.dist-info}/WHEEL +0 -0
- {geoai_py-0.2.3.dist-info → geoai_py-0.3.1.dist-info}/entry_points.txt +0 -0
- {geoai_py-0.2.3.dist-info → geoai_py-0.3.1.dist-info}/top_level.txt +0 -0
geoai/{common.py → utils.py}
RENAMED
|
@@ -12,9 +12,15 @@ import xarray as xr
|
|
|
12
12
|
import rioxarray
|
|
13
13
|
import rasterio as rio
|
|
14
14
|
from torch.utils.data import DataLoader
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
from torchgeo.
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from torchgeo.datasets import RasterDataset, stack_samples, unbind_samples, utils
|
|
18
|
+
from torchgeo.samplers import RandomGeoSampler, Units
|
|
19
|
+
from torchgeo.transforms import indices
|
|
20
|
+
except ImportError as e:
|
|
21
|
+
raise ImportError(
|
|
22
|
+
"Your torchgeo version is too old. Please upgrade to the latest version using 'pip install -U torchgeo'."
|
|
23
|
+
)
|
|
18
24
|
|
|
19
25
|
|
|
20
26
|
def view_raster(
|
|
@@ -588,6 +594,8 @@ def view_vector(
|
|
|
588
594
|
|
|
589
595
|
def view_vector_interactive(
|
|
590
596
|
vector_data,
|
|
597
|
+
layer_name="Vector Layer",
|
|
598
|
+
tiles_args=None,
|
|
591
599
|
**kwargs,
|
|
592
600
|
):
|
|
593
601
|
"""
|
|
@@ -599,6 +607,9 @@ def view_vector_interactive(
|
|
|
599
607
|
|
|
600
608
|
Args:
|
|
601
609
|
vector_data (geopandas.GeoDataFrame): The vector dataset to visualize.
|
|
610
|
+
layer_name (str, optional): The name of the layer. Defaults to "Vector Layer".
|
|
611
|
+
tiles_args (dict, optional): Additional arguments for the localtileserver client.
|
|
612
|
+
get_folium_tile_layer function. Defaults to None.
|
|
602
613
|
**kwargs: Additional keyword arguments to pass to GeoDataFrame.explore() function.
|
|
603
614
|
See https://geopandas.org/en/stable/docs/reference/api/geopandas.GeoDataFrame.explore.html
|
|
604
615
|
|
|
@@ -613,6 +624,59 @@ def view_vector_interactive(
|
|
|
613
624
|
>>> roads = gpd.read_file("roads.shp")
|
|
614
625
|
>>> view_vector_interactive(roads, figsize=(12, 8))
|
|
615
626
|
"""
|
|
627
|
+
import folium
|
|
628
|
+
import folium.plugins as plugins
|
|
629
|
+
from localtileserver import get_folium_tile_layer, TileClient
|
|
630
|
+
from leafmap import cog_tile
|
|
631
|
+
|
|
632
|
+
google_tiles = {
|
|
633
|
+
"Roadmap": {
|
|
634
|
+
"url": "https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}",
|
|
635
|
+
"attribution": "Google",
|
|
636
|
+
"name": "Google Maps",
|
|
637
|
+
},
|
|
638
|
+
"Satellite": {
|
|
639
|
+
"url": "https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}",
|
|
640
|
+
"attribution": "Google",
|
|
641
|
+
"name": "Google Satellite",
|
|
642
|
+
},
|
|
643
|
+
"Terrain": {
|
|
644
|
+
"url": "https://mt1.google.com/vt/lyrs=p&x={x}&y={y}&z={z}",
|
|
645
|
+
"attribution": "Google",
|
|
646
|
+
"name": "Google Terrain",
|
|
647
|
+
},
|
|
648
|
+
"Hybrid": {
|
|
649
|
+
"url": "https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}",
|
|
650
|
+
"attribution": "Google",
|
|
651
|
+
"name": "Google Hybrid",
|
|
652
|
+
},
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
basemap_layer_name = None
|
|
656
|
+
raster_layer = None
|
|
657
|
+
|
|
658
|
+
if "tiles" in kwargs and isinstance(kwargs["tiles"], str):
|
|
659
|
+
if kwargs["tiles"].title() in google_tiles:
|
|
660
|
+
basemap_layer_name = google_tiles[kwargs["tiles"].title()]["name"]
|
|
661
|
+
kwargs["tiles"] = google_tiles[kwargs["tiles"].title()]["url"]
|
|
662
|
+
kwargs["attr"] = "Google"
|
|
663
|
+
elif kwargs["tiles"].lower().endswith(".tif"):
|
|
664
|
+
if tiles_args is None:
|
|
665
|
+
tiles_args = {}
|
|
666
|
+
if kwargs["tiles"].lower().startswith("http"):
|
|
667
|
+
basemap_layer_name = "Remote Raster"
|
|
668
|
+
kwargs["tiles"] = cog_tile(kwargs["tiles"], **tiles_args)
|
|
669
|
+
kwargs["attr"] = "TiTiler"
|
|
670
|
+
else:
|
|
671
|
+
basemap_layer_name = "Local Raster"
|
|
672
|
+
client = TileClient(kwargs["tiles"])
|
|
673
|
+
raster_layer = get_folium_tile_layer(client, **tiles_args)
|
|
674
|
+
kwargs["tiles"] = raster_layer.tiles
|
|
675
|
+
kwargs["attr"] = "localtileserver"
|
|
676
|
+
|
|
677
|
+
if "max_zoom" not in kwargs:
|
|
678
|
+
kwargs["max_zoom"] = 30
|
|
679
|
+
|
|
616
680
|
if isinstance(vector_data, str):
|
|
617
681
|
vector_data = gpd.read_file(vector_data)
|
|
618
682
|
|
|
@@ -620,4 +684,399 @@ def view_vector_interactive(
|
|
|
620
684
|
if not isinstance(vector_data, gpd.GeoDataFrame):
|
|
621
685
|
raise TypeError("Input data must be a GeoDataFrame")
|
|
622
686
|
|
|
623
|
-
|
|
687
|
+
layer_control = kwargs.pop("layer_control", True)
|
|
688
|
+
fullscreen_control = kwargs.pop("fullscreen_control", True)
|
|
689
|
+
|
|
690
|
+
m = vector_data.explore(**kwargs)
|
|
691
|
+
|
|
692
|
+
# Change the layer name
|
|
693
|
+
for layer in m._children.values():
|
|
694
|
+
if isinstance(layer, folium.GeoJson):
|
|
695
|
+
layer.layer_name = layer_name
|
|
696
|
+
if isinstance(layer, folium.TileLayer) and basemap_layer_name:
|
|
697
|
+
layer.layer_name = basemap_layer_name
|
|
698
|
+
|
|
699
|
+
if layer_control:
|
|
700
|
+
m.add_child(folium.LayerControl())
|
|
701
|
+
|
|
702
|
+
if fullscreen_control:
|
|
703
|
+
plugins.Fullscreen().add_to(m)
|
|
704
|
+
|
|
705
|
+
return m
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
def regularization(
|
|
709
|
+
building_polygons,
|
|
710
|
+
angle_tolerance=10,
|
|
711
|
+
simplify_tolerance=0.5,
|
|
712
|
+
orthogonalize=True,
|
|
713
|
+
preserve_topology=True,
|
|
714
|
+
):
|
|
715
|
+
"""
|
|
716
|
+
Regularizes building footprint polygons with multiple techniques beyond minimum
|
|
717
|
+
rotated rectangles.
|
|
718
|
+
|
|
719
|
+
Args:
|
|
720
|
+
building_polygons: GeoDataFrame or list of shapely Polygons containing building footprints
|
|
721
|
+
angle_tolerance: Degrees within which angles will be regularized to 90/180 degrees
|
|
722
|
+
simplify_tolerance: Distance tolerance for Douglas-Peucker simplification
|
|
723
|
+
orthogonalize: Whether to enforce orthogonal angles in the final polygons
|
|
724
|
+
preserve_topology: Whether to preserve topology during simplification
|
|
725
|
+
|
|
726
|
+
Returns:
|
|
727
|
+
GeoDataFrame or list of shapely Polygons with regularized building footprints
|
|
728
|
+
"""
|
|
729
|
+
from shapely.geometry import Polygon, shape
|
|
730
|
+
from shapely.affinity import rotate, translate
|
|
731
|
+
from shapely import wkt
|
|
732
|
+
|
|
733
|
+
regularized_buildings = []
|
|
734
|
+
|
|
735
|
+
# Check if we're dealing with a GeoDataFrame
|
|
736
|
+
if isinstance(building_polygons, gpd.GeoDataFrame):
|
|
737
|
+
geom_objects = building_polygons.geometry
|
|
738
|
+
else:
|
|
739
|
+
geom_objects = building_polygons
|
|
740
|
+
|
|
741
|
+
for building in geom_objects:
|
|
742
|
+
# Handle potential string representations of geometries
|
|
743
|
+
if isinstance(building, str):
|
|
744
|
+
try:
|
|
745
|
+
# Try to parse as WKT
|
|
746
|
+
building = wkt.loads(building)
|
|
747
|
+
except Exception:
|
|
748
|
+
print(f"Failed to parse geometry string: {building[:30]}...")
|
|
749
|
+
continue
|
|
750
|
+
|
|
751
|
+
# Ensure we have a valid geometry
|
|
752
|
+
if not hasattr(building, "simplify"):
|
|
753
|
+
print(f"Invalid geometry type: {type(building)}")
|
|
754
|
+
continue
|
|
755
|
+
|
|
756
|
+
# Step 1: Simplify to remove noise and small vertices
|
|
757
|
+
simplified = building.simplify(
|
|
758
|
+
simplify_tolerance, preserve_topology=preserve_topology
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
if orthogonalize:
|
|
762
|
+
# Make sure we have a valid polygon with an exterior
|
|
763
|
+
if not hasattr(simplified, "exterior") or simplified.exterior is None:
|
|
764
|
+
print(f"Simplified geometry has no exterior: {simplified}")
|
|
765
|
+
regularized_buildings.append(building) # Use original instead
|
|
766
|
+
continue
|
|
767
|
+
|
|
768
|
+
# Step 2: Get the dominant angle to rotate building
|
|
769
|
+
coords = np.array(simplified.exterior.coords)
|
|
770
|
+
|
|
771
|
+
# Make sure we have enough coordinates for angle calculation
|
|
772
|
+
if len(coords) < 3:
|
|
773
|
+
print(f"Not enough coordinates for angle calculation: {len(coords)}")
|
|
774
|
+
regularized_buildings.append(building) # Use original instead
|
|
775
|
+
continue
|
|
776
|
+
|
|
777
|
+
segments = np.diff(coords, axis=0)
|
|
778
|
+
angles = np.arctan2(segments[:, 1], segments[:, 0]) * 180 / np.pi
|
|
779
|
+
|
|
780
|
+
# Find most common angle classes (0, 90, 180, 270 degrees)
|
|
781
|
+
binned_angles = np.round(angles / 90) * 90
|
|
782
|
+
dominant_angle = np.bincount(binned_angles.astype(int) % 180).argmax()
|
|
783
|
+
|
|
784
|
+
# Step 3: Rotate to align with axes, regularize, then rotate back
|
|
785
|
+
rotated = rotate(simplified, -dominant_angle, origin="centroid")
|
|
786
|
+
|
|
787
|
+
# Step 4: Rectify coordinates to enforce right angles
|
|
788
|
+
ext_coords = np.array(rotated.exterior.coords)
|
|
789
|
+
rect_coords = []
|
|
790
|
+
|
|
791
|
+
# Regularize each vertex to create orthogonal corners
|
|
792
|
+
for i in range(len(ext_coords) - 1):
|
|
793
|
+
rect_coords.append(ext_coords[i])
|
|
794
|
+
|
|
795
|
+
# Check if we need to add a right-angle vertex
|
|
796
|
+
angle = (
|
|
797
|
+
np.arctan2(
|
|
798
|
+
ext_coords[(i + 1) % (len(ext_coords) - 1), 1]
|
|
799
|
+
- ext_coords[i, 1],
|
|
800
|
+
ext_coords[(i + 1) % (len(ext_coords) - 1), 0]
|
|
801
|
+
- ext_coords[i, 0],
|
|
802
|
+
)
|
|
803
|
+
* 180
|
|
804
|
+
/ np.pi
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
if abs(angle % 90) > angle_tolerance and abs(angle % 90) < (
|
|
808
|
+
90 - angle_tolerance
|
|
809
|
+
):
|
|
810
|
+
# Add intermediate point to create right angle
|
|
811
|
+
rect_coords.append(
|
|
812
|
+
[
|
|
813
|
+
ext_coords[(i + 1) % (len(ext_coords) - 1), 0],
|
|
814
|
+
ext_coords[i, 1],
|
|
815
|
+
]
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
# Close the polygon by adding the first point again
|
|
819
|
+
rect_coords.append(rect_coords[0])
|
|
820
|
+
|
|
821
|
+
# Create regularized polygon and rotate back
|
|
822
|
+
regularized = Polygon(rect_coords)
|
|
823
|
+
final_building = rotate(regularized, dominant_angle, origin="centroid")
|
|
824
|
+
else:
|
|
825
|
+
final_building = simplified
|
|
826
|
+
|
|
827
|
+
regularized_buildings.append(final_building)
|
|
828
|
+
|
|
829
|
+
# If input was a GeoDataFrame, return a GeoDataFrame
|
|
830
|
+
if isinstance(building_polygons, gpd.GeoDataFrame):
|
|
831
|
+
return gpd.GeoDataFrame(
|
|
832
|
+
geometry=regularized_buildings, crs=building_polygons.crs
|
|
833
|
+
)
|
|
834
|
+
else:
|
|
835
|
+
return regularized_buildings
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def hybrid_regularization(building_polygons):
|
|
839
|
+
"""
|
|
840
|
+
A comprehensive hybrid approach to building footprint regularization.
|
|
841
|
+
|
|
842
|
+
Applies different strategies based on building characteristics.
|
|
843
|
+
|
|
844
|
+
Args:
|
|
845
|
+
building_polygons: GeoDataFrame or list of shapely Polygons containing building footprints
|
|
846
|
+
|
|
847
|
+
Returns:
|
|
848
|
+
GeoDataFrame or list of shapely Polygons with regularized building footprints
|
|
849
|
+
"""
|
|
850
|
+
from shapely.geometry import Polygon
|
|
851
|
+
from shapely.affinity import rotate
|
|
852
|
+
|
|
853
|
+
# Use minimum_rotated_rectangle instead of oriented_envelope
|
|
854
|
+
try:
|
|
855
|
+
from shapely.minimum_rotated_rectangle import minimum_rotated_rectangle
|
|
856
|
+
except ImportError:
|
|
857
|
+
# For older Shapely versions
|
|
858
|
+
def minimum_rotated_rectangle(geom):
|
|
859
|
+
"""Calculate the minimum rotated rectangle for a geometry"""
|
|
860
|
+
# For older Shapely versions, implement a simple version
|
|
861
|
+
return geom.minimum_rotated_rectangle
|
|
862
|
+
|
|
863
|
+
# Determine input type for correct return
|
|
864
|
+
is_gdf = isinstance(building_polygons, gpd.GeoDataFrame)
|
|
865
|
+
|
|
866
|
+
# Extract geometries if GeoDataFrame
|
|
867
|
+
if is_gdf:
|
|
868
|
+
geom_objects = building_polygons.geometry
|
|
869
|
+
else:
|
|
870
|
+
geom_objects = building_polygons
|
|
871
|
+
|
|
872
|
+
results = []
|
|
873
|
+
|
|
874
|
+
for building in geom_objects:
|
|
875
|
+
# 1. Analyze building characteristics
|
|
876
|
+
if not hasattr(building, "exterior") or building.is_empty:
|
|
877
|
+
results.append(building)
|
|
878
|
+
continue
|
|
879
|
+
|
|
880
|
+
# Calculate shape complexity metrics
|
|
881
|
+
complexity = building.length / (4 * np.sqrt(building.area))
|
|
882
|
+
|
|
883
|
+
# Calculate dominant angle
|
|
884
|
+
coords = np.array(building.exterior.coords)[:-1]
|
|
885
|
+
segments = np.diff(np.vstack([coords, coords[0]]), axis=0)
|
|
886
|
+
segment_lengths = np.sqrt(segments[:, 0] ** 2 + segments[:, 1] ** 2)
|
|
887
|
+
segment_angles = np.arctan2(segments[:, 1], segments[:, 0]) * 180 / np.pi
|
|
888
|
+
|
|
889
|
+
# Weight angles by segment length
|
|
890
|
+
hist, bins = np.histogram(
|
|
891
|
+
segment_angles % 180, bins=36, range=(0, 180), weights=segment_lengths
|
|
892
|
+
)
|
|
893
|
+
bin_centers = (bins[:-1] + bins[1:]) / 2
|
|
894
|
+
dominant_angle = bin_centers[np.argmax(hist)]
|
|
895
|
+
|
|
896
|
+
# Check if building is close to orthogonal
|
|
897
|
+
is_orthogonal = min(dominant_angle % 45, 45 - (dominant_angle % 45)) < 5
|
|
898
|
+
|
|
899
|
+
# 2. Apply appropriate regularization strategy
|
|
900
|
+
if complexity > 1.5:
|
|
901
|
+
# Complex buildings: use minimum rotated rectangle
|
|
902
|
+
result = minimum_rotated_rectangle(building)
|
|
903
|
+
elif is_orthogonal:
|
|
904
|
+
# Near-orthogonal buildings: orthogonalize in place
|
|
905
|
+
rotated = rotate(building, -dominant_angle, origin="centroid")
|
|
906
|
+
|
|
907
|
+
# Create orthogonal hull in rotated space
|
|
908
|
+
bounds = rotated.bounds
|
|
909
|
+
ortho_hull = Polygon(
|
|
910
|
+
[
|
|
911
|
+
(bounds[0], bounds[1]),
|
|
912
|
+
(bounds[2], bounds[1]),
|
|
913
|
+
(bounds[2], bounds[3]),
|
|
914
|
+
(bounds[0], bounds[3]),
|
|
915
|
+
]
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
result = rotate(ortho_hull, dominant_angle, origin="centroid")
|
|
919
|
+
else:
|
|
920
|
+
# Diagonal buildings: use custom approach for diagonal buildings
|
|
921
|
+
# Rotate to align with axes
|
|
922
|
+
rotated = rotate(building, -dominant_angle, origin="centroid")
|
|
923
|
+
|
|
924
|
+
# Simplify in rotated space
|
|
925
|
+
simplified = rotated.simplify(0.3, preserve_topology=True)
|
|
926
|
+
|
|
927
|
+
# Get the bounds in rotated space
|
|
928
|
+
bounds = simplified.bounds
|
|
929
|
+
min_x, min_y, max_x, max_y = bounds
|
|
930
|
+
|
|
931
|
+
# Create a rectangular hull in rotated space
|
|
932
|
+
rect_poly = Polygon(
|
|
933
|
+
[(min_x, min_y), (max_x, min_y), (max_x, max_y), (min_x, max_y)]
|
|
934
|
+
)
|
|
935
|
+
|
|
936
|
+
# Rotate back to original orientation
|
|
937
|
+
result = rotate(rect_poly, dominant_angle, origin="centroid")
|
|
938
|
+
|
|
939
|
+
results.append(result)
|
|
940
|
+
|
|
941
|
+
# Return in same format as input
|
|
942
|
+
if is_gdf:
|
|
943
|
+
return gpd.GeoDataFrame(geometry=results, crs=building_polygons.crs)
|
|
944
|
+
else:
|
|
945
|
+
return results
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
def adaptive_regularization(
|
|
949
|
+
building_polygons, simplify_tolerance=0.5, area_threshold=0.9, preserve_shape=True
|
|
950
|
+
):
|
|
951
|
+
"""
|
|
952
|
+
Adaptively regularizes building footprints based on their characteristics.
|
|
953
|
+
|
|
954
|
+
This approach determines the best regularization method for each building.
|
|
955
|
+
|
|
956
|
+
Args:
|
|
957
|
+
building_polygons: GeoDataFrame or list of shapely Polygons
|
|
958
|
+
simplify_tolerance: Distance tolerance for simplification
|
|
959
|
+
area_threshold: Minimum acceptable area ratio
|
|
960
|
+
preserve_shape: Whether to preserve overall shape for complex buildings
|
|
961
|
+
|
|
962
|
+
Returns:
|
|
963
|
+
GeoDataFrame or list of shapely Polygons with regularized building footprints
|
|
964
|
+
"""
|
|
965
|
+
from shapely.geometry import Polygon
|
|
966
|
+
from shapely.affinity import rotate
|
|
967
|
+
|
|
968
|
+
# Analyze the overall dataset to set appropriate parameters
|
|
969
|
+
if is_gdf := isinstance(building_polygons, gpd.GeoDataFrame):
|
|
970
|
+
geom_objects = building_polygons.geometry
|
|
971
|
+
else:
|
|
972
|
+
geom_objects = building_polygons
|
|
973
|
+
|
|
974
|
+
results = []
|
|
975
|
+
|
|
976
|
+
for building in geom_objects:
|
|
977
|
+
# Skip invalid geometries
|
|
978
|
+
if not hasattr(building, "exterior") or building.is_empty:
|
|
979
|
+
results.append(building)
|
|
980
|
+
continue
|
|
981
|
+
|
|
982
|
+
# Measure building complexity
|
|
983
|
+
complexity = building.length / (4 * np.sqrt(building.area))
|
|
984
|
+
|
|
985
|
+
# Determine if the building has a clear principal direction
|
|
986
|
+
coords = np.array(building.exterior.coords)[:-1]
|
|
987
|
+
segments = np.diff(np.vstack([coords, coords[0]]), axis=0)
|
|
988
|
+
segment_lengths = np.sqrt(segments[:, 0] ** 2 + segments[:, 1] ** 2)
|
|
989
|
+
angles = np.arctan2(segments[:, 1], segments[:, 0]) * 180 / np.pi
|
|
990
|
+
|
|
991
|
+
# Normalize angles to 0-180 range and get histogram
|
|
992
|
+
norm_angles = angles % 180
|
|
993
|
+
hist, bins = np.histogram(
|
|
994
|
+
norm_angles, bins=18, range=(0, 180), weights=segment_lengths
|
|
995
|
+
)
|
|
996
|
+
|
|
997
|
+
# Calculate direction clarity (ratio of longest direction to total)
|
|
998
|
+
direction_clarity = np.max(hist) / np.sum(hist) if np.sum(hist) > 0 else 0
|
|
999
|
+
|
|
1000
|
+
# Choose regularization method based on building characteristics
|
|
1001
|
+
if complexity < 1.2 and direction_clarity > 0.5:
|
|
1002
|
+
# Simple building with clear direction: use rotated rectangle
|
|
1003
|
+
bin_max = np.argmax(hist)
|
|
1004
|
+
bin_centers = (bins[:-1] + bins[1:]) / 2
|
|
1005
|
+
dominant_angle = bin_centers[bin_max]
|
|
1006
|
+
|
|
1007
|
+
# Rotate to align with coordinate system
|
|
1008
|
+
rotated = rotate(building, -dominant_angle, origin="centroid")
|
|
1009
|
+
|
|
1010
|
+
# Create bounding box in rotated space
|
|
1011
|
+
bounds = rotated.bounds
|
|
1012
|
+
rect = Polygon(
|
|
1013
|
+
[
|
|
1014
|
+
(bounds[0], bounds[1]),
|
|
1015
|
+
(bounds[2], bounds[1]),
|
|
1016
|
+
(bounds[2], bounds[3]),
|
|
1017
|
+
(bounds[0], bounds[3]),
|
|
1018
|
+
]
|
|
1019
|
+
)
|
|
1020
|
+
|
|
1021
|
+
# Rotate back
|
|
1022
|
+
result = rotate(rect, dominant_angle, origin="centroid")
|
|
1023
|
+
|
|
1024
|
+
# Quality check
|
|
1025
|
+
if (
|
|
1026
|
+
result.area / building.area < area_threshold
|
|
1027
|
+
or result.area / building.area > (1.0 / area_threshold)
|
|
1028
|
+
):
|
|
1029
|
+
# Too much area change, use simplified original
|
|
1030
|
+
result = building.simplify(simplify_tolerance, preserve_topology=True)
|
|
1031
|
+
|
|
1032
|
+
else:
|
|
1033
|
+
# Complex building or no clear direction: preserve shape
|
|
1034
|
+
if preserve_shape:
|
|
1035
|
+
# Simplify with topology preservation
|
|
1036
|
+
result = building.simplify(simplify_tolerance, preserve_topology=True)
|
|
1037
|
+
else:
|
|
1038
|
+
# Fall back to convex hull for very complex shapes
|
|
1039
|
+
result = building.convex_hull
|
|
1040
|
+
|
|
1041
|
+
results.append(result)
|
|
1042
|
+
|
|
1043
|
+
# Return in same format as input
|
|
1044
|
+
if is_gdf:
|
|
1045
|
+
return gpd.GeoDataFrame(geometry=results, crs=building_polygons.crs)
|
|
1046
|
+
else:
|
|
1047
|
+
return results
|
|
1048
|
+
|
|
1049
|
+
|
|
1050
|
+
def install_package(package):
|
|
1051
|
+
"""Install a Python package.
|
|
1052
|
+
|
|
1053
|
+
Args:
|
|
1054
|
+
package (str | list): The package name or a GitHub URL or a list of package names or GitHub URLs.
|
|
1055
|
+
"""
|
|
1056
|
+
import subprocess
|
|
1057
|
+
|
|
1058
|
+
if isinstance(package, str):
|
|
1059
|
+
packages = [package]
|
|
1060
|
+
elif isinstance(package, list):
|
|
1061
|
+
packages = package
|
|
1062
|
+
else:
|
|
1063
|
+
raise ValueError("The package argument must be a string or a list of strings.")
|
|
1064
|
+
|
|
1065
|
+
for package in packages:
|
|
1066
|
+
if package.startswith("https"):
|
|
1067
|
+
package = f"git+{package}"
|
|
1068
|
+
|
|
1069
|
+
# Execute pip install command and show output in real-time
|
|
1070
|
+
command = f"pip install {package}"
|
|
1071
|
+
process = subprocess.Popen(command.split(), stdout=subprocess.PIPE)
|
|
1072
|
+
|
|
1073
|
+
# Print output in real-time
|
|
1074
|
+
while True:
|
|
1075
|
+
output = process.stdout.readline()
|
|
1076
|
+
if output == b"" and process.poll() is not None:
|
|
1077
|
+
break
|
|
1078
|
+
if output:
|
|
1079
|
+
print(output.decode("utf-8").strip())
|
|
1080
|
+
|
|
1081
|
+
# Wait for process to complete
|
|
1082
|
+
process.wait()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
geoai/__init__.py,sha256=D1BgGoNkd6ZiimfM11EIeaTBVauMLz_XUnzp73IAJ80,923
|
|
2
|
+
geoai/download.py,sha256=4GiDmLrp2wKslgfm507WeZrwOdYcMekgQXxWGbl5cBw,13094
|
|
3
|
+
geoai/extract.py,sha256=2MdfLwlxbZ4YZIgEQhPcGpMTbNDTJ5-TbdJZnPfZ4Vw,71886
|
|
4
|
+
geoai/geoai.py,sha256=wNwKIqwOT10tU4uiWTcNp5Gd598rRFMANIfJsGdOWKM,90
|
|
5
|
+
geoai/preprocess.py,sha256=zQynNxQ_nxDkCEQU-h4G1SrgqxV1c5EREMV3JeS0cC0,118701
|
|
6
|
+
geoai/segmentation.py,sha256=Vcymnhwl_xikt4v9x8CYJq_vId9R1gB7-YzLfwg-F9M,11372
|
|
7
|
+
geoai/utils.py,sha256=uEZJLLnk2qOhsJLJgfJY6Fj_P0fP3FOZLJys0RwEkTs,38766
|
|
8
|
+
geoai_py-0.3.1.dist-info/LICENSE,sha256=vN2L5U7cZ6ZkOHFmc8WiGlsogWsZc5dllMeNxnKVOZg,1070
|
|
9
|
+
geoai_py-0.3.1.dist-info/METADATA,sha256=uBxf6OpFzAd1DlTqhrb6ge6N0-YYv4K53c86I8rTjk8,5754
|
|
10
|
+
geoai_py-0.3.1.dist-info/WHEEL,sha256=rF4EZyR2XVS6irmOHQIJx2SUqXLZKRMUrjsg8UwN-XQ,109
|
|
11
|
+
geoai_py-0.3.1.dist-info/entry_points.txt,sha256=uGp3Az3HURIsRHP9v-ys0hIbUuBBNUfXv6VbYHIXeg4,41
|
|
12
|
+
geoai_py-0.3.1.dist-info/top_level.txt,sha256=1YkCUWu-ii-0qIex7kbwAvfei-gos9ycyDyUCJPNWHY,6
|
|
13
|
+
geoai_py-0.3.1.dist-info/RECORD,,
|
geoai_py-0.2.3.dist-info/RECORD
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
geoai/__init__.py,sha256=TyhISsNnMOVPLx0wyM79k-Dgfak4s7QhfKPEIh0rBg8,923
|
|
2
|
-
geoai/common.py,sha256=stnGB-OiTdpb9hTSaaVn5jKAGweFNM760NFKP6suiv0,21735
|
|
3
|
-
geoai/download.py,sha256=4GiDmLrp2wKslgfm507WeZrwOdYcMekgQXxWGbl5cBw,13094
|
|
4
|
-
geoai/extract.py,sha256=wjo5KyaUMsci3oclnOGFqwPe1VIV4vlkFcbJlPWDOzI,31572
|
|
5
|
-
geoai/geoai.py,sha256=r9mFviDJrs7xvbK8kN3kIzZp-yswqTAleUejQvrZD4U,91
|
|
6
|
-
geoai/preprocess.py,sha256=2O3gaGN5imIpmTdudQdTbvCtrBqdmOb0AGNEz81MW2M,109762
|
|
7
|
-
geoai/segmentation.py,sha256=Vcymnhwl_xikt4v9x8CYJq_vId9R1gB7-YzLfwg-F9M,11372
|
|
8
|
-
geoai_py-0.2.3.dist-info/LICENSE,sha256=vN2L5U7cZ6ZkOHFmc8WiGlsogWsZc5dllMeNxnKVOZg,1070
|
|
9
|
-
geoai_py-0.2.3.dist-info/METADATA,sha256=T7lSXz-PE_AUSStKbnE5HUJi7-q5w04VIyiI7TZ4Xrw,5754
|
|
10
|
-
geoai_py-0.2.3.dist-info/WHEEL,sha256=rF4EZyR2XVS6irmOHQIJx2SUqXLZKRMUrjsg8UwN-XQ,109
|
|
11
|
-
geoai_py-0.2.3.dist-info/entry_points.txt,sha256=uGp3Az3HURIsRHP9v-ys0hIbUuBBNUfXv6VbYHIXeg4,41
|
|
12
|
-
geoai_py-0.2.3.dist-info/top_level.txt,sha256=1YkCUWu-ii-0qIex7kbwAvfei-gos9ycyDyUCJPNWHY,6
|
|
13
|
-
geoai_py-0.2.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|