geoai-py 0.13.2__py2.py3-none-any.whl → 0.14.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 +1 -1
- geoai/train.py +1 -1
- geoai/utils.py +192 -40
- {geoai_py-0.13.2.dist-info → geoai_py-0.14.0.dist-info}/METADATA +9 -5
- {geoai_py-0.13.2.dist-info → geoai_py-0.14.0.dist-info}/RECORD +9 -9
- {geoai_py-0.13.2.dist-info → geoai_py-0.14.0.dist-info}/WHEEL +0 -0
- {geoai_py-0.13.2.dist-info → geoai_py-0.14.0.dist-info}/entry_points.txt +0 -0
- {geoai_py-0.13.2.dist-info → geoai_py-0.14.0.dist-info}/licenses/LICENSE +0 -0
- {geoai_py-0.13.2.dist-info → geoai_py-0.14.0.dist-info}/top_level.txt +0 -0
geoai/__init__.py
CHANGED
geoai/train.py
CHANGED
@@ -433,7 +433,7 @@ def train_one_epoch(
|
|
433
433
|
elapsed_time = time.time() - start_time
|
434
434
|
if verbose:
|
435
435
|
print(
|
436
|
-
f"Epoch: {epoch}, Batch: {i}/{len(data_loader)}, Loss: {losses.item():.4f}, Time: {elapsed_time:.2f}s"
|
436
|
+
f"Epoch: {epoch + 1}, Batch: {i + 1}/{len(data_loader)}, Loss: {losses.item():.4f}, Time: {elapsed_time:.2f}s"
|
437
437
|
)
|
438
438
|
start_time = time.time()
|
439
439
|
|
geoai/utils.py
CHANGED
@@ -3115,8 +3115,9 @@ def export_geotiff_tiles(
|
|
3115
3115
|
|
3116
3116
|
def export_geotiff_tiles_batch(
|
3117
3117
|
images_folder,
|
3118
|
-
masks_folder,
|
3119
|
-
|
3118
|
+
masks_folder=None,
|
3119
|
+
masks_file=None,
|
3120
|
+
output_folder=None,
|
3120
3121
|
tile_size=256,
|
3121
3122
|
stride=128,
|
3122
3123
|
class_value_field="class",
|
@@ -3128,21 +3129,34 @@ def export_geotiff_tiles_batch(
|
|
3128
3129
|
skip_empty_tiles=False,
|
3129
3130
|
image_extensions=None,
|
3130
3131
|
mask_extensions=None,
|
3132
|
+
match_by_name=True,
|
3131
3133
|
) -> Dict[str, Any]:
|
3132
3134
|
"""
|
3133
|
-
Export georeferenced GeoTIFF tiles from
|
3135
|
+
Export georeferenced GeoTIFF tiles from images and masks.
|
3134
3136
|
|
3135
|
-
This function
|
3136
|
-
|
3137
|
-
|
3137
|
+
This function supports three mask input modes:
|
3138
|
+
1. Single vector file covering all images (masks_file parameter)
|
3139
|
+
2. Multiple vector files, one per image (masks_folder parameter)
|
3140
|
+
3. Multiple raster mask files (masks_folder parameter)
|
3138
3141
|
|
3139
|
-
|
3140
|
-
|
3142
|
+
For mode 1 (single vector file), specify masks_file path. The function will
|
3143
|
+
use spatial intersection to determine which features apply to each image.
|
3144
|
+
|
3145
|
+
For mode 2/3 (multiple mask files), specify masks_folder path. Images and masks
|
3146
|
+
are paired either by matching filenames (match_by_name=True) or by sorted order
|
3147
|
+
(match_by_name=False).
|
3148
|
+
|
3149
|
+
All image tiles are saved to a single 'images' folder and all mask tiles to a
|
3150
|
+
single 'masks' folder within the output directory.
|
3141
3151
|
|
3142
3152
|
Args:
|
3143
3153
|
images_folder (str): Path to folder containing raster images
|
3144
|
-
masks_folder (str): Path to folder containing classification masks/vectors
|
3145
|
-
|
3154
|
+
masks_folder (str, optional): Path to folder containing classification masks/vectors.
|
3155
|
+
Use this for multiple mask files (one per image or raster masks).
|
3156
|
+
masks_file (str, optional): Path to a single vector file covering all images.
|
3157
|
+
Use this for a single GeoJSON/Shapefile that covers multiple images.
|
3158
|
+
output_folder (str, optional): Path to output folder. If None, creates 'tiles'
|
3159
|
+
subfolder in images_folder.
|
3146
3160
|
tile_size (int): Size of tiles in pixels (square)
|
3147
3161
|
stride (int): Step size between tiles
|
3148
3162
|
class_value_field (str): Field containing class values (for vector data)
|
@@ -3154,18 +3168,61 @@ def export_geotiff_tiles_batch(
|
|
3154
3168
|
skip_empty_tiles (bool): If True, skip tiles with no features
|
3155
3169
|
image_extensions (list): List of image file extensions to process (default: common raster formats)
|
3156
3170
|
mask_extensions (list): List of mask file extensions to process (default: common raster/vector formats)
|
3171
|
+
match_by_name (bool): If True, match image and mask files by base filename.
|
3172
|
+
If False, match by sorted order (alphabetically). Only applies when masks_folder is used.
|
3157
3173
|
|
3158
3174
|
Returns:
|
3159
3175
|
Dict[str, Any]: Dictionary containing batch processing statistics
|
3160
3176
|
|
3161
3177
|
Raises:
|
3162
|
-
ValueError: If no images
|
3178
|
+
ValueError: If no images found, or if masks_folder and masks_file are both specified,
|
3179
|
+
or if neither is specified, or if counts don't match when using masks_folder with
|
3180
|
+
match_by_name=False.
|
3181
|
+
|
3182
|
+
Examples:
|
3183
|
+
# Single vector file covering all images
|
3184
|
+
>>> stats = export_geotiff_tiles_batch(
|
3185
|
+
... images_folder='data/images',
|
3186
|
+
... masks_file='data/buildings.geojson',
|
3187
|
+
... output_folder='output/tiles'
|
3188
|
+
... )
|
3189
|
+
|
3190
|
+
# Multiple vector files, matched by filename
|
3191
|
+
>>> stats = export_geotiff_tiles_batch(
|
3192
|
+
... images_folder='data/images',
|
3193
|
+
... masks_folder='data/masks',
|
3194
|
+
... output_folder='output/tiles',
|
3195
|
+
... match_by_name=True
|
3196
|
+
... )
|
3197
|
+
|
3198
|
+
# Multiple mask files, matched by sorted order
|
3199
|
+
>>> stats = export_geotiff_tiles_batch(
|
3200
|
+
... images_folder='data/images',
|
3201
|
+
... masks_folder='data/masks',
|
3202
|
+
... output_folder='output/tiles',
|
3203
|
+
... match_by_name=False
|
3204
|
+
... )
|
3163
3205
|
"""
|
3164
3206
|
|
3165
3207
|
import logging
|
3166
3208
|
|
3167
3209
|
logging.getLogger("rasterio").setLevel(logging.ERROR)
|
3168
3210
|
|
3211
|
+
# Validate input parameters
|
3212
|
+
if masks_folder is not None and masks_file is not None:
|
3213
|
+
raise ValueError(
|
3214
|
+
"Cannot specify both masks_folder and masks_file. Please use only one."
|
3215
|
+
)
|
3216
|
+
|
3217
|
+
if masks_folder is None and masks_file is None:
|
3218
|
+
raise ValueError(
|
3219
|
+
"Must specify either masks_folder or masks_file for mask data source."
|
3220
|
+
)
|
3221
|
+
|
3222
|
+
# Default output folder if not specified
|
3223
|
+
if output_folder is None:
|
3224
|
+
output_folder = os.path.join(images_folder, "tiles")
|
3225
|
+
|
3169
3226
|
# Default extensions if not provided
|
3170
3227
|
if image_extensions is None:
|
3171
3228
|
image_extensions = [".tif", ".tiff", ".jpg", ".jpeg", ".png", ".jp2", ".img"]
|
@@ -3202,30 +3259,88 @@ def export_geotiff_tiles_batch(
|
|
3202
3259
|
pattern = os.path.join(images_folder, f"*{ext}")
|
3203
3260
|
image_files.extend(glob.glob(pattern))
|
3204
3261
|
|
3205
|
-
# Get list of mask files
|
3206
|
-
mask_files = []
|
3207
|
-
for ext in mask_extensions:
|
3208
|
-
pattern = os.path.join(masks_folder, f"*{ext}")
|
3209
|
-
mask_files.extend(glob.glob(pattern))
|
3210
|
-
|
3211
3262
|
# Sort files for consistent processing
|
3212
3263
|
image_files.sort()
|
3213
|
-
mask_files.sort()
|
3214
3264
|
|
3215
3265
|
if not image_files:
|
3216
3266
|
raise ValueError(
|
3217
3267
|
f"No image files found in {images_folder} with extensions {image_extensions}"
|
3218
3268
|
)
|
3219
3269
|
|
3220
|
-
|
3221
|
-
|
3222
|
-
|
3223
|
-
|
3270
|
+
# Handle different mask input modes
|
3271
|
+
use_single_mask_file = masks_file is not None
|
3272
|
+
mask_files = []
|
3273
|
+
image_mask_pairs = []
|
3224
3274
|
|
3225
|
-
if
|
3226
|
-
|
3227
|
-
|
3228
|
-
|
3275
|
+
if use_single_mask_file:
|
3276
|
+
# Mode 1: Single vector file covering all images
|
3277
|
+
if not os.path.exists(masks_file):
|
3278
|
+
raise ValueError(f"Mask file not found: {masks_file}")
|
3279
|
+
|
3280
|
+
# Load the single mask file once - will be spatially filtered per image
|
3281
|
+
single_mask_gdf = gpd.read_file(masks_file)
|
3282
|
+
|
3283
|
+
if not quiet:
|
3284
|
+
print(f"Using single mask file: {masks_file}")
|
3285
|
+
print(
|
3286
|
+
f"Mask contains {len(single_mask_gdf)} features in CRS: {single_mask_gdf.crs}"
|
3287
|
+
)
|
3288
|
+
|
3289
|
+
# Create pairs with the same mask file for all images
|
3290
|
+
for image_file in image_files:
|
3291
|
+
image_mask_pairs.append((image_file, masks_file, single_mask_gdf))
|
3292
|
+
|
3293
|
+
else:
|
3294
|
+
# Mode 2/3: Multiple mask files (vector or raster)
|
3295
|
+
# Get list of mask files
|
3296
|
+
for ext in mask_extensions:
|
3297
|
+
pattern = os.path.join(masks_folder, f"*{ext}")
|
3298
|
+
mask_files.extend(glob.glob(pattern))
|
3299
|
+
|
3300
|
+
# Sort files for consistent processing
|
3301
|
+
mask_files.sort()
|
3302
|
+
|
3303
|
+
if not mask_files:
|
3304
|
+
raise ValueError(
|
3305
|
+
f"No mask files found in {masks_folder} with extensions {mask_extensions}"
|
3306
|
+
)
|
3307
|
+
|
3308
|
+
# Match images to masks
|
3309
|
+
if match_by_name:
|
3310
|
+
# Match by base filename
|
3311
|
+
image_dict = {
|
3312
|
+
os.path.splitext(os.path.basename(f))[0]: f for f in image_files
|
3313
|
+
}
|
3314
|
+
mask_dict = {
|
3315
|
+
os.path.splitext(os.path.basename(f))[0]: f for f in mask_files
|
3316
|
+
}
|
3317
|
+
|
3318
|
+
# Find matching pairs
|
3319
|
+
for img_base, img_path in image_dict.items():
|
3320
|
+
if img_base in mask_dict:
|
3321
|
+
image_mask_pairs.append((img_path, mask_dict[img_base], None))
|
3322
|
+
else:
|
3323
|
+
if not quiet:
|
3324
|
+
print(f"Warning: No mask found for image {img_base}")
|
3325
|
+
|
3326
|
+
if not image_mask_pairs:
|
3327
|
+
raise ValueError(
|
3328
|
+
"No matching image-mask pairs found when matching by filename. "
|
3329
|
+
"Check that image and mask files have matching base names."
|
3330
|
+
)
|
3331
|
+
|
3332
|
+
else:
|
3333
|
+
# Match by sorted order
|
3334
|
+
if len(image_files) != len(mask_files):
|
3335
|
+
raise ValueError(
|
3336
|
+
f"Number of image files ({len(image_files)}) does not match "
|
3337
|
+
f"number of mask files ({len(mask_files)}) when matching by sorted order. "
|
3338
|
+
f"Use match_by_name=True for filename-based matching."
|
3339
|
+
)
|
3340
|
+
|
3341
|
+
# Create pairs by sorted order
|
3342
|
+
for image_file, mask_file in zip(image_files, mask_files):
|
3343
|
+
image_mask_pairs.append((image_file, mask_file, None))
|
3229
3344
|
|
3230
3345
|
# Initialize batch statistics
|
3231
3346
|
batch_stats = {
|
@@ -3239,23 +3354,24 @@ def export_geotiff_tiles_batch(
|
|
3239
3354
|
}
|
3240
3355
|
|
3241
3356
|
if not quiet:
|
3242
|
-
|
3243
|
-
f"Found {len(image_files)} image files
|
3244
|
-
|
3245
|
-
|
3357
|
+
if use_single_mask_file:
|
3358
|
+
print(f"Found {len(image_files)} image files to process")
|
3359
|
+
print(f"Using single mask file: {masks_file}")
|
3360
|
+
else:
|
3361
|
+
print(f"Found {len(image_mask_pairs)} matching image-mask pairs to process")
|
3362
|
+
print(f"Processing batch from {images_folder} and {masks_folder}")
|
3246
3363
|
print(f"Output folder: {output_folder}")
|
3247
3364
|
print("-" * 60)
|
3248
3365
|
|
3249
3366
|
# Global tile counter for unique naming
|
3250
3367
|
global_tile_counter = 0
|
3251
3368
|
|
3252
|
-
# Process each image-mask pair
|
3253
|
-
for idx, (image_file, mask_file) in enumerate(
|
3369
|
+
# Process each image-mask pair
|
3370
|
+
for idx, (image_file, mask_file, mask_gdf) in enumerate(
|
3254
3371
|
tqdm(
|
3255
|
-
|
3372
|
+
image_mask_pairs,
|
3256
3373
|
desc="Processing image pairs",
|
3257
3374
|
disable=quiet,
|
3258
|
-
total=len(image_files),
|
3259
3375
|
)
|
3260
3376
|
):
|
3261
3377
|
batch_stats["total_image_pairs"] += 1
|
@@ -3267,9 +3383,12 @@ def export_geotiff_tiles_batch(
|
|
3267
3383
|
if not quiet:
|
3268
3384
|
print(f"\nProcessing: {base_name}")
|
3269
3385
|
print(f" Image: {os.path.basename(image_file)}")
|
3270
|
-
|
3386
|
+
if use_single_mask_file:
|
3387
|
+
print(f" Mask: {os.path.basename(mask_file)} (spatially filtered)")
|
3388
|
+
else:
|
3389
|
+
print(f" Mask: {os.path.basename(mask_file)}")
|
3271
3390
|
|
3272
|
-
# Process the image-mask pair
|
3391
|
+
# Process the image-mask pair
|
3273
3392
|
tiles_generated = _process_image_mask_pair(
|
3274
3393
|
image_file=image_file,
|
3275
3394
|
mask_file=mask_file,
|
@@ -3285,6 +3404,8 @@ def export_geotiff_tiles_batch(
|
|
3285
3404
|
all_touched=all_touched,
|
3286
3405
|
skip_empty_tiles=skip_empty_tiles,
|
3287
3406
|
quiet=quiet,
|
3407
|
+
mask_gdf=mask_gdf, # Pass pre-loaded GeoDataFrame if using single mask
|
3408
|
+
use_single_mask_file=use_single_mask_file,
|
3288
3409
|
)
|
3289
3410
|
|
3290
3411
|
# Update counters
|
@@ -3362,10 +3483,16 @@ def _process_image_mask_pair(
|
|
3362
3483
|
all_touched=True,
|
3363
3484
|
skip_empty_tiles=False,
|
3364
3485
|
quiet=False,
|
3486
|
+
mask_gdf=None,
|
3487
|
+
use_single_mask_file=False,
|
3365
3488
|
):
|
3366
3489
|
"""
|
3367
3490
|
Process a single image-mask pair and save tiles directly to output directories.
|
3368
3491
|
|
3492
|
+
Args:
|
3493
|
+
mask_gdf (GeoDataFrame, optional): Pre-loaded GeoDataFrame when using single mask file
|
3494
|
+
use_single_mask_file (bool): If True, spatially filter mask_gdf to image bounds
|
3495
|
+
|
3369
3496
|
Returns:
|
3370
3497
|
dict: Statistics for this image-mask pair
|
3371
3498
|
"""
|
@@ -3433,11 +3560,36 @@ def _process_image_mask_pair(
|
|
3433
3560
|
else:
|
3434
3561
|
# Load vector class data
|
3435
3562
|
try:
|
3436
|
-
|
3563
|
+
if use_single_mask_file and mask_gdf is not None:
|
3564
|
+
# Using pre-loaded single mask file - spatially filter to image bounds
|
3565
|
+
# Get image bounds
|
3566
|
+
image_bounds = box(*src.bounds)
|
3567
|
+
image_gdf = gpd.GeoDataFrame(
|
3568
|
+
{"geometry": [image_bounds]}, crs=src.crs
|
3569
|
+
)
|
3437
3570
|
|
3438
|
-
|
3439
|
-
|
3440
|
-
|
3571
|
+
# Reproject mask if needed
|
3572
|
+
if mask_gdf.crs != src.crs:
|
3573
|
+
mask_gdf_reprojected = mask_gdf.to_crs(src.crs)
|
3574
|
+
else:
|
3575
|
+
mask_gdf_reprojected = mask_gdf
|
3576
|
+
|
3577
|
+
# Spatially filter features that intersect with image bounds
|
3578
|
+
gdf = mask_gdf_reprojected[
|
3579
|
+
mask_gdf_reprojected.intersects(image_bounds)
|
3580
|
+
].copy()
|
3581
|
+
|
3582
|
+
if not quiet and len(gdf) > 0:
|
3583
|
+
print(
|
3584
|
+
f" Filtered to {len(gdf)} features intersecting image bounds"
|
3585
|
+
)
|
3586
|
+
else:
|
3587
|
+
# Load individual mask file
|
3588
|
+
gdf = gpd.read_file(mask_file)
|
3589
|
+
|
3590
|
+
# Always reproject to match raster CRS
|
3591
|
+
if gdf.crs != src.crs:
|
3592
|
+
gdf = gdf.to_crs(src.crs)
|
3441
3593
|
|
3442
3594
|
# Apply buffer if specified
|
3443
3595
|
if buffer_radius > 0:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: geoai-py
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.14.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
|
@@ -159,13 +159,17 @@ Comprehensive documentation is available at [https://opengeoai.org](https://open
|
|
159
159
|
|
160
160
|
## 📺 Video Tutorials
|
161
161
|
|
162
|
-
|
162
|
+
### GeoAI Made Easy: Learn the Python Package Step-by-Step (Beginner Friendly)
|
163
163
|
|
164
|
-
[](https://youtu.be/VIl29Rca6zE&list=PLAxJ4-o7ZoPcvENqwaPa_QwbbkZ5sctZE)
|
165
165
|
|
166
|
-
|
166
|
+
### GeoAI Workshop: Unlocking the Power of GeoAI with Python
|
167
167
|
|
168
|
-
[](https://youtu.be/jdK-cleFUkc&list=PLAxJ4-o7ZoPcvENqwaPa_QwbbkZ5sctZE)
|
169
|
+
|
170
|
+
### GeoAI Tutorials Playlist
|
171
|
+
|
172
|
+
[](https://www.youtube.com/playlist?list=PLAxJ4-o7ZoPcvENqwaPa_QwbbkZ5sctZE)
|
169
173
|
|
170
174
|
## 🤝 Contributing
|
171
175
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
geoai/__init__.py,sha256=
|
1
|
+
geoai/__init__.py,sha256=6XXPspLCl-xq93YsqtZKS9dzOI9nwDSzWGJvcxAZLT0,3851
|
2
2
|
geoai/change_detection.py,sha256=XkJjMEU1nD8uX3-nQy7NEmz8cukVeSaRxKJHlrv8xPM,59636
|
3
3
|
geoai/classify.py,sha256=0DcComVR6vKU4qWtH2oHVeXc7ZTcV0mFvdXRtlNmolo,35637
|
4
4
|
geoai/detectron2.py,sha256=dOOFM9M9-6PV8q2A4-mnIPrz7yTo-MpEvDiAW34nl0w,14610
|
@@ -11,14 +11,14 @@ geoai/map_widgets.py,sha256=QLmkILsztNaRXRULHKOd7Glb7S0pEWXSK9-P8S5AuzQ,5856
|
|
11
11
|
geoai/sam.py,sha256=O6S-kGiFn7YEcFbfWFItZZQOhnsm6-GlunxQLY0daEs,34345
|
12
12
|
geoai/segment.py,sha256=yBGTxA-ti8lBpk7WVaBOp6yP23HkaulKJQk88acrmZ0,43788
|
13
13
|
geoai/segmentation.py,sha256=7yEzBSKCyHW1dNssoK0rdvhxi2IXsIQIFSga817KdI4,11535
|
14
|
-
geoai/train.py,sha256=
|
15
|
-
geoai/utils.py,sha256=
|
14
|
+
geoai/train.py,sha256=3uS95OTo3_KPuE9OhPRyFJ2PHFTlYUC4RVzJcLyzt4k,141826
|
15
|
+
geoai/utils.py,sha256=4l5jewO4aRNRgeLDhrUFXwMA_CNBCSv8RkaCn3nN2p0,307795
|
16
16
|
geoai/agents/__init__.py,sha256=NndUtQ5-i8Zuim8CJftCZYKbCvrkDXj9iLVtiBtc_qE,178
|
17
17
|
geoai/agents/geo_agents.py,sha256=4tLntKBL_FgTQsUVzReP9acbYotnfjMRc5BYwW9WEyE,21431
|
18
18
|
geoai/agents/map_tools.py,sha256=OK5uB0VUHjjUnc-DYRy2CQ__kyUIARSCPBucGabO0Xw,60669
|
19
|
-
geoai_py-0.
|
20
|
-
geoai_py-0.
|
21
|
-
geoai_py-0.
|
22
|
-
geoai_py-0.
|
23
|
-
geoai_py-0.
|
24
|
-
geoai_py-0.
|
19
|
+
geoai_py-0.14.0.dist-info/licenses/LICENSE,sha256=vN2L5U7cZ6ZkOHFmc8WiGlsogWsZc5dllMeNxnKVOZg,1070
|
20
|
+
geoai_py-0.14.0.dist-info/METADATA,sha256=33j9XSDDCSdpU7PbuMP4zv7r50sTq-IdqJ4NnsSmjqo,10583
|
21
|
+
geoai_py-0.14.0.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
|
22
|
+
geoai_py-0.14.0.dist-info/entry_points.txt,sha256=uGp3Az3HURIsRHP9v-ys0hIbUuBBNUfXv6VbYHIXeg4,41
|
23
|
+
geoai_py-0.14.0.dist-info/top_level.txt,sha256=1YkCUWu-ii-0qIex7kbwAvfei-gos9ycyDyUCJPNWHY,6
|
24
|
+
geoai_py-0.14.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|