geoai-py 0.26.0__py2.py3-none-any.whl → 0.28.0__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 +41 -1
- geoai/auto.py +4 -1
- geoai/change_detection.py +1 -1
- geoai/detectron2.py +4 -1
- geoai/extract.py +10 -7
- geoai/hf.py +3 -3
- geoai/moondream.py +2 -2
- geoai/onnx.py +1155 -0
- geoai/prithvi.py +92 -7
- geoai/sam.py +2 -1
- geoai/segment.py +10 -1
- geoai/timm_regress.py +1652 -0
- geoai/train.py +1 -1
- geoai/utils.py +550 -1
- {geoai_py-0.26.0.dist-info → geoai_py-0.28.0.dist-info}/METADATA +9 -7
- {geoai_py-0.26.0.dist-info → geoai_py-0.28.0.dist-info}/RECORD +20 -18
- {geoai_py-0.26.0.dist-info → geoai_py-0.28.0.dist-info}/WHEEL +1 -1
- {geoai_py-0.26.0.dist-info → geoai_py-0.28.0.dist-info}/entry_points.txt +0 -0
- {geoai_py-0.26.0.dist-info → geoai_py-0.28.0.dist-info}/licenses/LICENSE +0 -0
- {geoai_py-0.26.0.dist-info → geoai_py-0.28.0.dist-info}/top_level.txt +0 -0
geoai/train.py
CHANGED
geoai/utils.py
CHANGED
|
@@ -24,7 +24,6 @@ from typing import (
|
|
|
24
24
|
)
|
|
25
25
|
|
|
26
26
|
# Third-Party Libraries
|
|
27
|
-
import cv2
|
|
28
27
|
import geopandas as gpd
|
|
29
28
|
import leafmap
|
|
30
29
|
import matplotlib.pyplot as plt
|
|
@@ -3095,6 +3094,7 @@ def export_geotiff_tiles(
|
|
|
3095
3094
|
apply_augmentation=False,
|
|
3096
3095
|
augmentation_count=3,
|
|
3097
3096
|
augmentation_transforms=None,
|
|
3097
|
+
tiling_strategy="grid",
|
|
3098
3098
|
):
|
|
3099
3099
|
"""
|
|
3100
3100
|
Export georeferenced GeoTIFF tiles and labels from raster and classification data.
|
|
@@ -3123,6 +3123,10 @@ def export_geotiff_tiles(
|
|
|
3123
3123
|
If None and apply_augmentation=True, uses default transforms from
|
|
3124
3124
|
get_default_augmentation_transforms(). Should be an albumentations.Compose object.
|
|
3125
3125
|
Defaults to None.
|
|
3126
|
+
tiling_strategy (str, optional): Tiling strategy to use. Options are:
|
|
3127
|
+
- "grid": Regular grid tiling with specified stride (default behavior)
|
|
3128
|
+
- "flipnslide": Flip-n-Slide augmentation strategy with overlapping tiles
|
|
3129
|
+
Defaults to "grid".
|
|
3126
3130
|
|
|
3127
3131
|
Returns:
|
|
3128
3132
|
None: Tiles and labels are saved to out_folder.
|
|
@@ -3145,12 +3149,50 @@ def export_geotiff_tiles(
|
|
|
3145
3149
|
... apply_augmentation=True,
|
|
3146
3150
|
... augmentation_count=5,
|
|
3147
3151
|
... augmentation_transforms=custom_transform)
|
|
3152
|
+
>>>
|
|
3153
|
+
>>> # Export with Flip-n-Slide tiling strategy
|
|
3154
|
+
>>> export_geotiff_tiles('image.tif', 'output/', 'labels.tif',
|
|
3155
|
+
... tiling_strategy='flipnslide')
|
|
3148
3156
|
"""
|
|
3149
3157
|
|
|
3150
3158
|
import logging
|
|
3151
3159
|
|
|
3152
3160
|
logging.getLogger("rasterio").setLevel(logging.ERROR)
|
|
3153
3161
|
|
|
3162
|
+
# Handle FlipnSlide tiling strategy
|
|
3163
|
+
if tiling_strategy == "flipnslide":
|
|
3164
|
+
if apply_augmentation:
|
|
3165
|
+
warnings.warn(
|
|
3166
|
+
"apply_augmentation is ignored when using tiling_strategy='flipnslide'. "
|
|
3167
|
+
"FlipnSlide applies its own augmentation scheme."
|
|
3168
|
+
)
|
|
3169
|
+
if stride != 128:
|
|
3170
|
+
warnings.warn(
|
|
3171
|
+
"stride parameter is ignored when using tiling_strategy='flipnslide'. "
|
|
3172
|
+
"FlipnSlide uses its own stride pattern (tile_size/2)."
|
|
3173
|
+
)
|
|
3174
|
+
|
|
3175
|
+
# Use the dedicated FlipnSlide export function
|
|
3176
|
+
stats = export_flipnslide_tiles(
|
|
3177
|
+
in_raster=in_raster,
|
|
3178
|
+
out_folder=out_folder,
|
|
3179
|
+
in_class_data=in_class_data,
|
|
3180
|
+
tile_size=tile_size,
|
|
3181
|
+
quiet=quiet,
|
|
3182
|
+
)
|
|
3183
|
+
|
|
3184
|
+
if not quiet:
|
|
3185
|
+
print("Used Flip-n-Slide tiling strategy")
|
|
3186
|
+
print(
|
|
3187
|
+
f"Generated {stats['total_tiles']} tiles with spatial context preservation"
|
|
3188
|
+
)
|
|
3189
|
+
|
|
3190
|
+
return
|
|
3191
|
+
elif tiling_strategy != "grid":
|
|
3192
|
+
raise ValueError(
|
|
3193
|
+
f"Unknown tiling_strategy '{tiling_strategy}'. Must be 'grid' or 'flipnslide'."
|
|
3194
|
+
)
|
|
3195
|
+
|
|
3154
3196
|
# Initialize augmentation transforms if needed
|
|
3155
3197
|
if apply_augmentation:
|
|
3156
3198
|
if augmentation_transforms is None:
|
|
@@ -6109,6 +6151,8 @@ def masks_to_vector(
|
|
|
6109
6151
|
Returns:
|
|
6110
6152
|
Any: GeoDataFrame with building footprints
|
|
6111
6153
|
"""
|
|
6154
|
+
import cv2 # Lazy import to avoid QGIS opencv conflicts
|
|
6155
|
+
|
|
6112
6156
|
# Set default output path if not provided
|
|
6113
6157
|
# if output_path is None:
|
|
6114
6158
|
# output_path = os.path.splitext(mask_path)[0] + ".geojson"
|
|
@@ -7136,6 +7180,7 @@ def orthogonalize(
|
|
|
7136
7180
|
Returns:
|
|
7137
7181
|
Any: A GeoDataFrame containing the orthogonalized features.
|
|
7138
7182
|
"""
|
|
7183
|
+
import cv2 # Lazy import to avoid QGIS opencv conflicts
|
|
7139
7184
|
|
|
7140
7185
|
from functools import partial
|
|
7141
7186
|
|
|
@@ -9465,3 +9510,507 @@ def smooth_vector(
|
|
|
9465
9510
|
if output_path is not None:
|
|
9466
9511
|
smoothed_vector_data.to_file(output_path)
|
|
9467
9512
|
return smoothed_vector_data
|
|
9513
|
+
|
|
9514
|
+
|
|
9515
|
+
def flipnslide_augmentation(
|
|
9516
|
+
image,
|
|
9517
|
+
tile_size=256,
|
|
9518
|
+
output_format="numpy",
|
|
9519
|
+
crop_to_multiple=True,
|
|
9520
|
+
):
|
|
9521
|
+
"""
|
|
9522
|
+
Apply Flip-n-Slide tiling strategy for geospatial imagery data augmentation.
|
|
9523
|
+
|
|
9524
|
+
This function implements the Flip-n-Slide algorithm from "A Concise Tiling
|
|
9525
|
+
Strategy for Preserving Spatial Context in Earth Observation Imagery" by
|
|
9526
|
+
Abrahams et al., presented at the ML4RS workshop at ICLR 2024 (best short
|
|
9527
|
+
paper). The strategy generates overlapping tiles with diverse augmentations
|
|
9528
|
+
while eliminating redundant pixel representations.
|
|
9529
|
+
|
|
9530
|
+
The algorithm produces two sets of tiles:
|
|
9531
|
+
|
|
9532
|
+
1. **Standard overlapping tiles** with half-stride (stride = tile_size / 2)
|
|
9533
|
+
and rotational augmentations determined by grid position:
|
|
9534
|
+
|
|
9535
|
+
- Even row, even col: identity (no augmentation)
|
|
9536
|
+
- Odd row, even col: 180 degree rotation
|
|
9537
|
+
- Even row, odd col: 90 degree rotation
|
|
9538
|
+
- Odd row, odd col: 270 degree rotation
|
|
9539
|
+
|
|
9540
|
+
2. **Inner offset tiles** extracted from the image interior (inset by
|
|
9541
|
+
tile_size / 2 from each edge) with the same half-stride, applying flip
|
|
9542
|
+
and rotation augmentations:
|
|
9543
|
+
|
|
9544
|
+
- Even row, even col: horizontal flip
|
|
9545
|
+
- Even row, odd col: vertical flip
|
|
9546
|
+
- Odd row, odd col: 90 degree rotation + horizontal flip
|
|
9547
|
+
- Odd row, even col: 90 degree rotation + vertical flip
|
|
9548
|
+
|
|
9549
|
+
Args:
|
|
9550
|
+
image (Union[str, numpy.ndarray]): Input image as a numpy array of
|
|
9551
|
+
shape ``(channels, height, width)`` or a file path to a raster
|
|
9552
|
+
readable by rasterio.
|
|
9553
|
+
tile_size (int, optional): Size of each square tile in pixels.
|
|
9554
|
+
Defaults to 256.
|
|
9555
|
+
output_format (str, optional): ``"numpy"`` to return a
|
|
9556
|
+
:class:`numpy.ndarray` or ``"torch"`` to return a
|
|
9557
|
+
:class:`torch.Tensor`. Defaults to ``"numpy"``.
|
|
9558
|
+
crop_to_multiple (bool, optional): If ``True``, crop the image to the
|
|
9559
|
+
nearest dimensions that are multiples of *tile_size* before tiling.
|
|
9560
|
+
Defaults to ``True``.
|
|
9561
|
+
|
|
9562
|
+
Returns:
|
|
9563
|
+
Tuple[Union[numpy.ndarray, torch.Tensor], List[int]]:
|
|
9564
|
+
- **tiles** -- Array of shape
|
|
9565
|
+
``(num_tiles, channels, tile_size, tile_size)``.
|
|
9566
|
+
- **augmentation_indices** -- List of integers indicating the
|
|
9567
|
+
augmentation applied to each tile:
|
|
9568
|
+
|
|
9569
|
+
- 0: Identity (no augmentation)
|
|
9570
|
+
- 1: 180 degree rotation
|
|
9571
|
+
- 2: 90 degree rotation
|
|
9572
|
+
- 3: 270 degree rotation
|
|
9573
|
+
- 4: Horizontal flip
|
|
9574
|
+
- 5: Vertical flip
|
|
9575
|
+
- 6: 90 degree rotation + horizontal flip
|
|
9576
|
+
- 7: 90 degree rotation + vertical flip
|
|
9577
|
+
|
|
9578
|
+
Example:
|
|
9579
|
+
>>> import numpy as np
|
|
9580
|
+
>>> image = np.random.rand(3, 512, 512)
|
|
9581
|
+
>>> tiles, aug_indices = flipnslide_augmentation(image, tile_size=256)
|
|
9582
|
+
>>> print(f"Generated {tiles.shape[0]} tiles of shape {tiles.shape[1:]}")
|
|
9583
|
+
>>> print(f"Augmentation types used: {sorted(set(aug_indices))}")
|
|
9584
|
+
|
|
9585
|
+
References:
|
|
9586
|
+
Abrahams, E., Snow, T., Siegfried, M. R., & Perez, F. (2024).
|
|
9587
|
+
*A Concise Tiling Strategy for Preserving Spatial Context in Earth
|
|
9588
|
+
Observation Imagery*. ML4RS Workshop @ ICLR 2024.
|
|
9589
|
+
https://doi.org/10.48550/arXiv.2404.10927
|
|
9590
|
+
"""
|
|
9591
|
+
# Load image if path provided
|
|
9592
|
+
if isinstance(image, str):
|
|
9593
|
+
import rasterio
|
|
9594
|
+
|
|
9595
|
+
with rasterio.open(image) as src:
|
|
9596
|
+
image = src.read()
|
|
9597
|
+
|
|
9598
|
+
# Ensure numpy array
|
|
9599
|
+
if not isinstance(image, np.ndarray):
|
|
9600
|
+
try:
|
|
9601
|
+
image = image.cpu().numpy()
|
|
9602
|
+
except AttributeError:
|
|
9603
|
+
image = np.asarray(image)
|
|
9604
|
+
|
|
9605
|
+
# Validate input shape
|
|
9606
|
+
if image.ndim != 3:
|
|
9607
|
+
raise ValueError(
|
|
9608
|
+
f"Image must be a 3-D array (channels, height, width), "
|
|
9609
|
+
f"got shape {image.shape}"
|
|
9610
|
+
)
|
|
9611
|
+
|
|
9612
|
+
channels, height, width = image.shape
|
|
9613
|
+
|
|
9614
|
+
# Crop to nearest multiple of tile_size if requested
|
|
9615
|
+
if crop_to_multiple:
|
|
9616
|
+
new_height = (height // tile_size) * tile_size
|
|
9617
|
+
new_width = (width // tile_size) * tile_size
|
|
9618
|
+
if new_height == 0 or new_width == 0:
|
|
9619
|
+
raise ValueError(
|
|
9620
|
+
f"Image size ({height}x{width}) is smaller than "
|
|
9621
|
+
f"tile_size ({tile_size})"
|
|
9622
|
+
)
|
|
9623
|
+
if new_height < height or new_width < width:
|
|
9624
|
+
image = image[:, :new_height, :new_width]
|
|
9625
|
+
height, width = new_height, new_width
|
|
9626
|
+
|
|
9627
|
+
# Check if image is large enough for tiling
|
|
9628
|
+
if height < tile_size or width < tile_size:
|
|
9629
|
+
raise ValueError(
|
|
9630
|
+
f"Image size ({height}x{width}) is smaller than " f"tile_size ({tile_size})"
|
|
9631
|
+
)
|
|
9632
|
+
|
|
9633
|
+
tiles = []
|
|
9634
|
+
augmentation_indices = []
|
|
9635
|
+
|
|
9636
|
+
# Half-stride for overlapping tiles
|
|
9637
|
+
stride = tile_size // 2
|
|
9638
|
+
|
|
9639
|
+
# ------------------------------------------------------------------
|
|
9640
|
+
# Stage 1: Standard overlapping tiles with rotational augmentations
|
|
9641
|
+
# ------------------------------------------------------------------
|
|
9642
|
+
for idx_h, row in enumerate(range(0, height - tile_size + 1, stride)):
|
|
9643
|
+
for idx_w, col in enumerate(range(0, width - tile_size + 1, stride)):
|
|
9644
|
+
tile = image[:, row : row + tile_size, col : col + tile_size]
|
|
9645
|
+
|
|
9646
|
+
if idx_h % 2 == 0 and idx_w % 2 == 0:
|
|
9647
|
+
# Identity
|
|
9648
|
+
aug_tile = tile.copy()
|
|
9649
|
+
aug_idx = 0
|
|
9650
|
+
elif idx_h % 2 == 1 and idx_w % 2 == 0:
|
|
9651
|
+
# 180 degree rotation
|
|
9652
|
+
aug_tile = np.rot90(tile, 2, axes=(1, 2)).copy()
|
|
9653
|
+
aug_idx = 1
|
|
9654
|
+
elif idx_h % 2 == 0 and idx_w % 2 == 1:
|
|
9655
|
+
# 90 degree rotation
|
|
9656
|
+
aug_tile = np.rot90(tile, 1, axes=(1, 2)).copy()
|
|
9657
|
+
aug_idx = 2
|
|
9658
|
+
else:
|
|
9659
|
+
# 270 degree rotation
|
|
9660
|
+
aug_tile = np.rot90(tile, 3, axes=(1, 2)).copy()
|
|
9661
|
+
aug_idx = 3
|
|
9662
|
+
|
|
9663
|
+
tiles.append(aug_tile)
|
|
9664
|
+
augmentation_indices.append(aug_idx)
|
|
9665
|
+
|
|
9666
|
+
# ------------------------------------------------------------------
|
|
9667
|
+
# Stage 2: Inner offset tiles with flip + rotation augmentations
|
|
9668
|
+
# The inner image is inset by tile_size/2 from each edge, matching the
|
|
9669
|
+
# original FlipnSlide implementation.
|
|
9670
|
+
# ------------------------------------------------------------------
|
|
9671
|
+
inset = tile_size // 2
|
|
9672
|
+
if height - 2 * inset >= tile_size and width - 2 * inset >= tile_size:
|
|
9673
|
+
inner_image = image[:, inset : height - inset, inset : width - inset]
|
|
9674
|
+
inner_height = inner_image.shape[1]
|
|
9675
|
+
inner_width = inner_image.shape[2]
|
|
9676
|
+
|
|
9677
|
+
for idx_h, row in enumerate(range(0, inner_height - tile_size + 1, stride)):
|
|
9678
|
+
for idx_w, col in enumerate(range(0, inner_width - tile_size + 1, stride)):
|
|
9679
|
+
tile = inner_image[:, row : row + tile_size, col : col + tile_size]
|
|
9680
|
+
|
|
9681
|
+
if idx_h % 2 == 0 and idx_w % 2 == 0:
|
|
9682
|
+
# Horizontal flip
|
|
9683
|
+
aug_tile = tile[:, :, ::-1].copy()
|
|
9684
|
+
aug_idx = 4
|
|
9685
|
+
elif idx_h % 2 == 0 and idx_w % 2 == 1:
|
|
9686
|
+
# Vertical flip
|
|
9687
|
+
aug_tile = tile[:, ::-1, :].copy()
|
|
9688
|
+
aug_idx = 5
|
|
9689
|
+
elif idx_h % 2 == 1 and idx_w % 2 == 1:
|
|
9690
|
+
# 90 degree rotation + horizontal flip
|
|
9691
|
+
aug_tile = np.rot90(tile, 1, axes=(1, 2))
|
|
9692
|
+
aug_tile = aug_tile[:, :, ::-1].copy()
|
|
9693
|
+
aug_idx = 6
|
|
9694
|
+
else:
|
|
9695
|
+
# 90 degree rotation + vertical flip
|
|
9696
|
+
aug_tile = np.rot90(tile, 1, axes=(1, 2))
|
|
9697
|
+
aug_tile = aug_tile[:, ::-1, :].copy()
|
|
9698
|
+
aug_idx = 7
|
|
9699
|
+
|
|
9700
|
+
tiles.append(aug_tile)
|
|
9701
|
+
augmentation_indices.append(aug_idx)
|
|
9702
|
+
|
|
9703
|
+
# Stack into array
|
|
9704
|
+
tiles_array = np.stack(tiles, axis=0)
|
|
9705
|
+
|
|
9706
|
+
# Optionally convert to torch tensor
|
|
9707
|
+
if output_format == "torch":
|
|
9708
|
+
tiles_array = torch.from_numpy(tiles_array)
|
|
9709
|
+
elif output_format != "numpy":
|
|
9710
|
+
raise ValueError(
|
|
9711
|
+
f"output_format must be 'numpy' or 'torch', got '{output_format}'"
|
|
9712
|
+
)
|
|
9713
|
+
|
|
9714
|
+
return tiles_array, augmentation_indices
|
|
9715
|
+
|
|
9716
|
+
|
|
9717
|
+
def export_flipnslide_tiles(
|
|
9718
|
+
in_raster,
|
|
9719
|
+
out_folder,
|
|
9720
|
+
in_class_data=None,
|
|
9721
|
+
tile_size=256,
|
|
9722
|
+
output_format="tif",
|
|
9723
|
+
crop_to_multiple=True,
|
|
9724
|
+
quiet=False,
|
|
9725
|
+
):
|
|
9726
|
+
"""
|
|
9727
|
+
Export georeferenced tiles using the Flip-n-Slide augmentation strategy.
|
|
9728
|
+
|
|
9729
|
+
This function applies the Flip-n-Slide tiling algorithm to an image raster
|
|
9730
|
+
(and optionally a corresponding label/mask raster), preserving spatial
|
|
9731
|
+
relationships and geospatial information. Each tile is saved as an
|
|
9732
|
+
individual GeoTIFF file with proper CRS and geotransform.
|
|
9733
|
+
|
|
9734
|
+
Args:
|
|
9735
|
+
in_raster (str): Path to the input raster image.
|
|
9736
|
+
out_folder (str): Path to the output folder where tiles will be saved.
|
|
9737
|
+
in_class_data (str, optional): Path to a classification/mask file.
|
|
9738
|
+
Can be a raster file (GeoTIFF, etc.) or vector file (GeoJSON,
|
|
9739
|
+
Shapefile, etc.). When provided, matching mask tiles are generated
|
|
9740
|
+
with identical augmentations. Vector files are rasterized to match
|
|
9741
|
+
the input raster dimensions and CRS. Defaults to None.
|
|
9742
|
+
tile_size (int, optional): Size of each square tile in pixels.
|
|
9743
|
+
Defaults to 256.
|
|
9744
|
+
output_format (str, optional): File extension for the output tiles
|
|
9745
|
+
(e.g., ``"tif"``). Defaults to ``"tif"``.
|
|
9746
|
+
crop_to_multiple (bool, optional): If ``True``, crop the image to the
|
|
9747
|
+
nearest dimensions that are multiples of *tile_size* before tiling.
|
|
9748
|
+
Defaults to ``True``.
|
|
9749
|
+
quiet (bool, optional): If ``True``, suppress progress output.
|
|
9750
|
+
Defaults to ``False``.
|
|
9751
|
+
|
|
9752
|
+
Returns:
|
|
9753
|
+
dict: Statistics dictionary with keys:
|
|
9754
|
+
|
|
9755
|
+
- ``'total_tiles'`` -- Number of tiles generated.
|
|
9756
|
+
- ``'tile_size'`` -- Size of each tile.
|
|
9757
|
+
- ``'augmentation_counts'`` -- Count of each augmentation type.
|
|
9758
|
+
- ``'output_folder'`` -- Path to the output folder.
|
|
9759
|
+
- ``'has_labels'`` -- Whether label tiles were generated.
|
|
9760
|
+
|
|
9761
|
+
Example:
|
|
9762
|
+
>>> stats = export_flipnslide_tiles("image.tif", "output_tiles/")
|
|
9763
|
+
>>> print(f"Generated {stats['total_tiles']} tiles")
|
|
9764
|
+
|
|
9765
|
+
>>> stats = export_flipnslide_tiles(
|
|
9766
|
+
... "image.tif", "output_tiles/",
|
|
9767
|
+
... in_class_data="mask.tif", tile_size=512,
|
|
9768
|
+
... )
|
|
9769
|
+
|
|
9770
|
+
Notes:
|
|
9771
|
+
Both input raster and class data (if provided) must share the same CRS
|
|
9772
|
+
and spatial extent for proper alignment.
|
|
9773
|
+
"""
|
|
9774
|
+
import logging
|
|
9775
|
+
from collections import Counter
|
|
9776
|
+
|
|
9777
|
+
logging.getLogger("rasterio").setLevel(logging.ERROR)
|
|
9778
|
+
|
|
9779
|
+
# Create output directories
|
|
9780
|
+
os.makedirs(out_folder, exist_ok=True)
|
|
9781
|
+
image_dir = os.path.join(out_folder, "images")
|
|
9782
|
+
os.makedirs(image_dir, exist_ok=True)
|
|
9783
|
+
|
|
9784
|
+
has_labels = in_class_data is not None
|
|
9785
|
+
if has_labels:
|
|
9786
|
+
label_dir = os.path.join(out_folder, "labels")
|
|
9787
|
+
os.makedirs(label_dir, exist_ok=True)
|
|
9788
|
+
|
|
9789
|
+
with rasterio.open(in_raster) as src:
|
|
9790
|
+
if not quiet:
|
|
9791
|
+
print(f"Input raster: {in_raster}")
|
|
9792
|
+
print(f" CRS: {src.crs}")
|
|
9793
|
+
print(f" Dimensions: {src.width} x {src.height}")
|
|
9794
|
+
print(f" Bands: {src.count}")
|
|
9795
|
+
|
|
9796
|
+
image_data = src.read()
|
|
9797
|
+
|
|
9798
|
+
# Read class data if provided (handle both raster and vector)
|
|
9799
|
+
class_data = None
|
|
9800
|
+
if has_labels:
|
|
9801
|
+
# Detect if input is raster or vector
|
|
9802
|
+
is_class_data_raster = False
|
|
9803
|
+
file_ext = Path(in_class_data).suffix.lower()
|
|
9804
|
+
|
|
9805
|
+
# Common raster extensions
|
|
9806
|
+
if file_ext in [".tif", ".tiff", ".img", ".jp2", ".png", ".bmp", ".gif"]:
|
|
9807
|
+
try:
|
|
9808
|
+
with rasterio.open(in_class_data) as test_src:
|
|
9809
|
+
is_class_data_raster = True
|
|
9810
|
+
except Exception:
|
|
9811
|
+
is_class_data_raster = False
|
|
9812
|
+
else:
|
|
9813
|
+
# Common vector extensions
|
|
9814
|
+
vector_extensions = [
|
|
9815
|
+
".geojson",
|
|
9816
|
+
".json",
|
|
9817
|
+
".shp",
|
|
9818
|
+
".gpkg",
|
|
9819
|
+
".fgb",
|
|
9820
|
+
".parquet",
|
|
9821
|
+
".geoparquet",
|
|
9822
|
+
]
|
|
9823
|
+
if file_ext in vector_extensions:
|
|
9824
|
+
is_class_data_raster = False
|
|
9825
|
+
else:
|
|
9826
|
+
# Unknown extension - try raster first, then vector
|
|
9827
|
+
try:
|
|
9828
|
+
with rasterio.open(in_class_data) as test_src:
|
|
9829
|
+
is_class_data_raster = True
|
|
9830
|
+
except Exception:
|
|
9831
|
+
is_class_data_raster = False
|
|
9832
|
+
|
|
9833
|
+
if is_class_data_raster:
|
|
9834
|
+
# Handle raster class data
|
|
9835
|
+
with rasterio.open(in_class_data) as class_src:
|
|
9836
|
+
if class_src.crs != src.crs:
|
|
9837
|
+
raise ValueError(
|
|
9838
|
+
f"CRS mismatch: image ({src.crs}) vs mask ({class_src.crs})"
|
|
9839
|
+
)
|
|
9840
|
+
if (class_src.width != src.width) or (
|
|
9841
|
+
class_src.height != src.height
|
|
9842
|
+
):
|
|
9843
|
+
raise ValueError(
|
|
9844
|
+
f"Dimension mismatch: image "
|
|
9845
|
+
f"({src.width}x{src.height}) vs mask "
|
|
9846
|
+
f"({class_src.width}x{class_src.height})"
|
|
9847
|
+
)
|
|
9848
|
+
class_data = class_src.read()
|
|
9849
|
+
if not quiet:
|
|
9850
|
+
print(f" Class data (raster): {in_class_data}")
|
|
9851
|
+
else:
|
|
9852
|
+
# Handle vector class data
|
|
9853
|
+
try:
|
|
9854
|
+
gdf = gpd.read_file(in_class_data)
|
|
9855
|
+
if not quiet:
|
|
9856
|
+
print(f" Class data (vector): {in_class_data}")
|
|
9857
|
+
print(f" Loaded {len(gdf)} features")
|
|
9858
|
+
print(f" Vector CRS: {gdf.crs}")
|
|
9859
|
+
|
|
9860
|
+
# Reproject to match raster CRS if needed
|
|
9861
|
+
if gdf.crs != src.crs:
|
|
9862
|
+
if not quiet:
|
|
9863
|
+
print(f" Reprojecting from {gdf.crs} to {src.crs}")
|
|
9864
|
+
gdf = gdf.to_crs(src.crs)
|
|
9865
|
+
|
|
9866
|
+
# Rasterize vector data to match raster dimensions
|
|
9867
|
+
if len(gdf) > 0:
|
|
9868
|
+
# Create binary mask: 1 for features, 0 for background
|
|
9869
|
+
geometries = [
|
|
9870
|
+
(geom, 1) for geom in gdf.geometry if geom is not None
|
|
9871
|
+
]
|
|
9872
|
+
if geometries:
|
|
9873
|
+
rasterized = features.rasterize(
|
|
9874
|
+
geometries,
|
|
9875
|
+
out_shape=(src.height, src.width),
|
|
9876
|
+
transform=src.transform,
|
|
9877
|
+
fill=0,
|
|
9878
|
+
all_touched=True,
|
|
9879
|
+
dtype=np.uint8,
|
|
9880
|
+
)
|
|
9881
|
+
# Reshape to match expected format (bands, height, width)
|
|
9882
|
+
class_data = rasterized.reshape(1, src.height, src.width)
|
|
9883
|
+
else:
|
|
9884
|
+
# No valid geometries, create empty mask
|
|
9885
|
+
class_data = np.zeros(
|
|
9886
|
+
(1, src.height, src.width), dtype=np.uint8
|
|
9887
|
+
)
|
|
9888
|
+
else:
|
|
9889
|
+
# Empty geodataframe, create empty mask
|
|
9890
|
+
class_data = np.zeros(
|
|
9891
|
+
(1, src.height, src.width), dtype=np.uint8
|
|
9892
|
+
)
|
|
9893
|
+
|
|
9894
|
+
except Exception as e:
|
|
9895
|
+
raise ValueError(
|
|
9896
|
+
f"Could not read {in_class_data} as vector file: {e}"
|
|
9897
|
+
)
|
|
9898
|
+
|
|
9899
|
+
# Apply FlipnSlide augmentation
|
|
9900
|
+
image_tiles, aug_indices = flipnslide_augmentation(
|
|
9901
|
+
image_data, tile_size=tile_size, crop_to_multiple=crop_to_multiple
|
|
9902
|
+
)
|
|
9903
|
+
|
|
9904
|
+
class_tiles = None
|
|
9905
|
+
if has_labels:
|
|
9906
|
+
class_tiles, _ = flipnslide_augmentation(
|
|
9907
|
+
class_data, tile_size=tile_size, crop_to_multiple=crop_to_multiple
|
|
9908
|
+
)
|
|
9909
|
+
|
|
9910
|
+
if not quiet:
|
|
9911
|
+
print(
|
|
9912
|
+
f"\nGenerated {len(image_tiles)} tiles with "
|
|
9913
|
+
f"Flip-n-Slide augmentation"
|
|
9914
|
+
)
|
|
9915
|
+
|
|
9916
|
+
# Determine cropped dimensions for geo-transform calculation
|
|
9917
|
+
_channels, orig_h, orig_w = image_data.shape
|
|
9918
|
+
if crop_to_multiple:
|
|
9919
|
+
height = (orig_h // tile_size) * tile_size
|
|
9920
|
+
width = (orig_w // tile_size) * tile_size
|
|
9921
|
+
else:
|
|
9922
|
+
height, width = orig_h, orig_w
|
|
9923
|
+
|
|
9924
|
+
transform = src.transform
|
|
9925
|
+
stride = tile_size // 2
|
|
9926
|
+
inset = tile_size // 2
|
|
9927
|
+
|
|
9928
|
+
# Build tile positions mirroring flipnslide_augmentation order
|
|
9929
|
+
tile_positions = []
|
|
9930
|
+
|
|
9931
|
+
# Stage 1 - standard overlapping tiles
|
|
9932
|
+
for row in range(0, height - tile_size + 1, stride):
|
|
9933
|
+
for col in range(0, width - tile_size + 1, stride):
|
|
9934
|
+
tile_positions.append((row, col))
|
|
9935
|
+
|
|
9936
|
+
# Stage 2 - inner offset tiles
|
|
9937
|
+
inner_h = height - 2 * inset
|
|
9938
|
+
inner_w = width - 2 * inset
|
|
9939
|
+
if inner_h >= tile_size and inner_w >= tile_size:
|
|
9940
|
+
for row in range(0, inner_h - tile_size + 1, stride):
|
|
9941
|
+
for col in range(0, inner_w - tile_size + 1, stride):
|
|
9942
|
+
tile_positions.append((inset + row, inset + col))
|
|
9943
|
+
|
|
9944
|
+
# Augmentation label names for filenames
|
|
9945
|
+
aug_names = {
|
|
9946
|
+
0: "identity",
|
|
9947
|
+
1: "rot180",
|
|
9948
|
+
2: "rot90",
|
|
9949
|
+
3: "rot270",
|
|
9950
|
+
4: "hflip",
|
|
9951
|
+
5: "vflip",
|
|
9952
|
+
6: "rot90_hflip",
|
|
9953
|
+
7: "rot90_vflip",
|
|
9954
|
+
}
|
|
9955
|
+
|
|
9956
|
+
# Save tiles
|
|
9957
|
+
for i, (tile_row, tile_col) in enumerate(tile_positions):
|
|
9958
|
+
tile_transform = rasterio.transform.from_bounds(
|
|
9959
|
+
transform.c + tile_col * transform.a,
|
|
9960
|
+
transform.f + (tile_row + tile_size) * transform.e,
|
|
9961
|
+
transform.c + (tile_col + tile_size) * transform.a,
|
|
9962
|
+
transform.f + tile_row * transform.e,
|
|
9963
|
+
tile_size,
|
|
9964
|
+
tile_size,
|
|
9965
|
+
)
|
|
9966
|
+
|
|
9967
|
+
image_profile = src.profile.copy()
|
|
9968
|
+
image_profile.update(
|
|
9969
|
+
{
|
|
9970
|
+
"height": tile_size,
|
|
9971
|
+
"width": tile_size,
|
|
9972
|
+
"count": image_tiles.shape[1],
|
|
9973
|
+
"transform": tile_transform,
|
|
9974
|
+
}
|
|
9975
|
+
)
|
|
9976
|
+
|
|
9977
|
+
aug_label = aug_names.get(aug_indices[i], str(aug_indices[i]))
|
|
9978
|
+
fname = f"tile_{i:06d}_{aug_label}.{output_format}"
|
|
9979
|
+
|
|
9980
|
+
image_path = os.path.join(image_dir, fname)
|
|
9981
|
+
with rasterio.open(image_path, "w", **image_profile) as dst:
|
|
9982
|
+
dst.write(image_tiles[i])
|
|
9983
|
+
|
|
9984
|
+
if has_labels:
|
|
9985
|
+
class_profile = image_profile.copy()
|
|
9986
|
+
class_profile.update(
|
|
9987
|
+
{
|
|
9988
|
+
"count": class_tiles.shape[1],
|
|
9989
|
+
"dtype": class_tiles.dtype,
|
|
9990
|
+
}
|
|
9991
|
+
)
|
|
9992
|
+
class_path = os.path.join(label_dir, fname)
|
|
9993
|
+
with rasterio.open(class_path, "w", **class_profile) as dst:
|
|
9994
|
+
dst.write(class_tiles[i])
|
|
9995
|
+
|
|
9996
|
+
# Statistics
|
|
9997
|
+
aug_counts = Counter(aug_indices)
|
|
9998
|
+
stats = {
|
|
9999
|
+
"total_tiles": len(image_tiles),
|
|
10000
|
+
"tile_size": tile_size,
|
|
10001
|
+
"augmentation_counts": dict(aug_counts),
|
|
10002
|
+
"output_folder": out_folder,
|
|
10003
|
+
"has_labels": has_labels,
|
|
10004
|
+
}
|
|
10005
|
+
|
|
10006
|
+
if not quiet:
|
|
10007
|
+
print("\n--- Flip-n-Slide Export Summary ---")
|
|
10008
|
+
print(f" Total tiles : {stats['total_tiles']}")
|
|
10009
|
+
print(f" Tile size : {tile_size}x{tile_size}")
|
|
10010
|
+
print(f" Augmentations: {dict(aug_counts)}")
|
|
10011
|
+
if has_labels:
|
|
10012
|
+
print(" Exported both image and label tiles")
|
|
10013
|
+
else:
|
|
10014
|
+
print(" Exported image tiles only")
|
|
10015
|
+
|
|
10016
|
+
return stats
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: geoai-py
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.28.0
|
|
4
4
|
Summary: A Python package for using Artificial Intelligence (AI) with geospatial data
|
|
5
5
|
Author-email: Qiusheng Wu <giswqs@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -17,10 +17,8 @@ Requires-Python: >=3.10
|
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
License-File: LICENSE
|
|
19
19
|
Requires-Dist: albumentations
|
|
20
|
-
Requires-Dist: buildingregulariser
|
|
21
20
|
Requires-Dist: contextily
|
|
22
21
|
Requires-Dist: datasets>=3.0
|
|
23
|
-
Requires-Dist: ever-beta
|
|
24
22
|
Requires-Dist: geopandas
|
|
25
23
|
Requires-Dist: huggingface_hub
|
|
26
24
|
Requires-Dist: jupyter-server-proxy
|
|
@@ -31,7 +29,6 @@ Requires-Dist: maplibre
|
|
|
31
29
|
Requires-Dist: opencv-python-headless
|
|
32
30
|
Requires-Dist: overturemaps
|
|
33
31
|
Requires-Dist: planetary-computer
|
|
34
|
-
Requires-Dist: psutil
|
|
35
32
|
Requires-Dist: pyarrow
|
|
36
33
|
Requires-Dist: pystac-client
|
|
37
34
|
Requires-Dist: rasterio
|
|
@@ -53,12 +50,17 @@ Requires-Dist: lightly-train; extra == "extra"
|
|
|
53
50
|
Requires-Dist: multiclean; extra == "extra"
|
|
54
51
|
Requires-Dist: omnicloudmask; extra == "extra"
|
|
55
52
|
Requires-Dist: smoothify; extra == "extra"
|
|
53
|
+
Provides-Extra: building
|
|
54
|
+
Requires-Dist: buildingregulariser; extra == "building"
|
|
56
55
|
Provides-Extra: agents
|
|
57
56
|
Requires-Dist: strands-agents; extra == "agents"
|
|
58
57
|
Requires-Dist: strands-agents-tools; extra == "agents"
|
|
59
58
|
Requires-Dist: strands-agents[ollama]; extra == "agents"
|
|
60
59
|
Requires-Dist: strands-agents[anthropic]; extra == "agents"
|
|
61
60
|
Requires-Dist: strands-agents[openai]; extra == "agents"
|
|
61
|
+
Provides-Extra: onnx
|
|
62
|
+
Requires-Dist: onnx; extra == "onnx"
|
|
63
|
+
Requires-Dist: onnxruntime; extra == "onnx"
|
|
62
64
|
Provides-Extra: sr
|
|
63
65
|
Requires-Dist: opensr-model; extra == "sr"
|
|
64
66
|
Dynamic: license-file
|
|
@@ -71,7 +73,7 @@ Dynamic: license-file
|
|
|
71
73
|
[](https://anaconda.org/conda-forge/geoai)
|
|
72
74
|
[](https://github.com/conda-forge/geoai-py-feedstock)
|
|
73
75
|
[](https://opensource.org/licenses/MIT)
|
|
74
|
-
[](https://
|
|
76
|
+
[](https://www.youtube.com/playlist?list=PLAxJ4-o7ZoPcvENqwaPa_QwbbkZ5sctZE)
|
|
75
77
|
[](https://opengeoai.org/qgis_plugin)
|
|
76
78
|
|
|
77
79
|
[](https://github.com/opengeos/geoai/blob/master/docs/assets/logo.png)
|
|
@@ -91,7 +93,7 @@ The package provides five core capabilities:
|
|
|
91
93
|
5. Interactive visualization through integration with [Leafmap](https://github.com/opengeos/leafmap/) and [MapLibre](https://github.com/eoda-dev/py-maplibregl).
|
|
92
94
|
6. Seamless QGIS integration via a dedicated GeoAI plugin, enabling users to run AI-powered geospatial workflows directly within the QGIS desktop environment, without writing code.
|
|
93
95
|
|
|
94
|
-
GeoAI addresses the growing demand for accessible AI tools in geospatial research by providing high-level APIs that abstract complex machine learning workflows while maintaining flexibility for advanced users. The package supports multiple data formats (GeoTIFF, JPEG2000,GeoJSON, Shapefile, GeoPackage) and includes automatic device management for GPU acceleration when available. With over 10 modules and extensive notebook examples, GeoAI serves as both a research tool and educational resource for the geospatial AI community.
|
|
96
|
+
GeoAI addresses the growing demand for accessible AI tools in geospatial research by providing high-level APIs that abstract complex machine learning workflows while maintaining flexibility for advanced users. The package supports multiple data formats (GeoTIFF, JPEG2000, GeoJSON, Shapefile, GeoPackage) and includes automatic device management for GPU acceleration when available. With over 10 modules and extensive notebook examples, GeoAI serves as both a research tool and educational resource for the geospatial AI community.
|
|
95
97
|
|
|
96
98
|
## 📝 Statement of Need
|
|
97
99
|
|
|
@@ -130,7 +132,7 @@ If you find GeoAI useful in your research, please consider citing the following
|
|
|
130
132
|
|
|
131
133
|
- Integration with [PyTorch Segmentation Models](https://github.com/qubvel-org/segmentation_models.pytorch) for automatic feature extraction
|
|
132
134
|
- Specialized segmentation algorithms optimized for satellite and aerial imagery
|
|
133
|
-
- Streamlined workflows for segmenting buildings, water bodies, wetlands,solar panels, etc.
|
|
135
|
+
- Streamlined workflows for segmenting buildings, water bodies, wetlands, solar panels, etc.
|
|
134
136
|
- Export capabilities to standard geospatial formats (GeoJSON, Shapefile, GeoPackage, GeoParquet)
|
|
135
137
|
|
|
136
138
|
### 🔍 Image Classification
|