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/prithvi.py CHANGED
@@ -810,7 +810,7 @@ class PrithviProcessor:
810
810
  meta = src.meta
811
811
  try:
812
812
  coords = src.tags()
813
- except:
813
+ except Exception:
814
814
  coords = None
815
815
 
816
816
  return img, meta, coords
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
@@ -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.27.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