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/train.py CHANGED
@@ -2131,7 +2131,7 @@ def get_smp_model(
2131
2131
  "pan",
2132
2132
  "upernet",
2133
2133
  ]
2134
- except:
2134
+ except Exception:
2135
2135
  available_archs = [
2136
2136
  "unet",
2137
2137
  "fpn",
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.26.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
  [![Conda Downloads](https://img.shields.io/conda/dn/conda-forge/geoai.svg)](https://anaconda.org/conda-forge/geoai)
72
74
  [![Conda Recipe](https://img.shields.io/badge/recipe-geoai-green.svg)](https://github.com/conda-forge/geoai-py-feedstock)
73
75
  [![image](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
74
- [![image](https://img.shields.io/badge/YouTube-Tutorials-red)](https://tinyurl.com/GeoAI-Tutorials)
76
+ [![image](https://img.shields.io/badge/YouTube-Tutorials-red)](https://www.youtube.com/playlist?list=PLAxJ4-o7ZoPcvENqwaPa_QwbbkZ5sctZE)
75
77
  [![QGIS](https://img.shields.io/badge/QGIS-plugin-orange.svg)](https://opengeoai.org/qgis_plugin)
76
78
 
77
79
  [![logo](https://raw.githubusercontent.com/opengeos/geoai/master/docs/assets/logo_rect.png)](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