geoai-py 0.27.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 +15 -1
- geoai/extract.py +6 -6
- geoai/hf.py +3 -3
- geoai/moondream.py +2 -2
- geoai/onnx.py +1155 -0
- geoai/prithvi.py +1 -1
- geoai/train.py +1 -1
- geoai/utils.py +547 -0
- {geoai_py-0.27.0.dist-info → geoai_py-0.28.0.dist-info}/METADATA +6 -4
- {geoai_py-0.27.0.dist-info → geoai_py-0.28.0.dist-info}/RECORD +14 -13
- {geoai_py-0.27.0.dist-info → geoai_py-0.28.0.dist-info}/WHEEL +0 -0
- {geoai_py-0.27.0.dist-info → geoai_py-0.28.0.dist-info}/entry_points.txt +0 -0
- {geoai_py-0.27.0.dist-info → geoai_py-0.28.0.dist-info}/licenses/LICENSE +0 -0
- {geoai_py-0.27.0.dist-info → geoai_py-0.28.0.dist-info}/top_level.txt +0 -0
geoai/prithvi.py
CHANGED
geoai/train.py
CHANGED
geoai/utils.py
CHANGED
|
@@ -3094,6 +3094,7 @@ def export_geotiff_tiles(
|
|
|
3094
3094
|
apply_augmentation=False,
|
|
3095
3095
|
augmentation_count=3,
|
|
3096
3096
|
augmentation_transforms=None,
|
|
3097
|
+
tiling_strategy="grid",
|
|
3097
3098
|
):
|
|
3098
3099
|
"""
|
|
3099
3100
|
Export georeferenced GeoTIFF tiles and labels from raster and classification data.
|
|
@@ -3122,6 +3123,10 @@ def export_geotiff_tiles(
|
|
|
3122
3123
|
If None and apply_augmentation=True, uses default transforms from
|
|
3123
3124
|
get_default_augmentation_transforms(). Should be an albumentations.Compose object.
|
|
3124
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".
|
|
3125
3130
|
|
|
3126
3131
|
Returns:
|
|
3127
3132
|
None: Tiles and labels are saved to out_folder.
|
|
@@ -3144,12 +3149,50 @@ def export_geotiff_tiles(
|
|
|
3144
3149
|
... apply_augmentation=True,
|
|
3145
3150
|
... augmentation_count=5,
|
|
3146
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')
|
|
3147
3156
|
"""
|
|
3148
3157
|
|
|
3149
3158
|
import logging
|
|
3150
3159
|
|
|
3151
3160
|
logging.getLogger("rasterio").setLevel(logging.ERROR)
|
|
3152
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
|
+
|
|
3153
3196
|
# Initialize augmentation transforms if needed
|
|
3154
3197
|
if apply_augmentation:
|
|
3155
3198
|
if augmentation_transforms is None:
|
|
@@ -9467,3 +9510,507 @@ def smooth_vector(
|
|
|
9467
9510
|
if output_path is not None:
|
|
9468
9511
|
smoothed_vector_data.to_file(output_path)
|
|
9469
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
|