geoai-py 0.4.2__py2.py3-none-any.whl → 0.5.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/extract.py CHANGED
@@ -19,6 +19,7 @@ from torchvision.models.detection import (
19
19
  maskrcnn_resnet50_fpn,
20
20
  )
21
21
  from tqdm import tqdm
22
+ import time
22
23
 
23
24
  # Local Imports
24
25
  from .utils import get_raster_stats
@@ -2117,6 +2118,7 @@ class ObjectDetector:
2117
2118
  confidence_threshold=0.5,
2118
2119
  min_object_area=100,
2119
2120
  max_object_area=None,
2121
+ n_workers=None,
2120
2122
  **kwargs,
2121
2123
  ):
2122
2124
  """
@@ -2128,14 +2130,103 @@ class ObjectDetector:
2128
2130
  confidence_threshold: Minimum confidence score (0.0-1.0). Default: 0.5
2129
2131
  min_object_area: Minimum area in pixels to keep an object. Default: 100
2130
2132
  max_object_area: Maximum area in pixels to keep an object. Default: None
2133
+ n_workers: int, default=None
2134
+ The number of worker threads to use.
2135
+ "None" means single-threaded processing.
2136
+ "-1" means using all available CPU processors.
2137
+ Positive integer means using that specific number of threads.
2131
2138
  **kwargs: Additional parameters
2132
2139
 
2133
2140
  Returns:
2134
2141
  GeoDataFrame with car detections and confidence values
2135
2142
  """
2136
2143
 
2144
+ def _process_single_component(
2145
+ component_mask,
2146
+ conf_data,
2147
+ transform,
2148
+ confidence_threshold,
2149
+ min_object_area,
2150
+ max_object_area,
2151
+ ):
2152
+ # Get confidence value
2153
+ conf_region = conf_data[component_mask > 0]
2154
+ if len(conf_region) > 0:
2155
+ confidence = np.mean(conf_region) / 255.0
2156
+ else:
2157
+ confidence = 0.0
2158
+
2159
+ # Skip if confidence is below threshold
2160
+ if confidence < confidence_threshold:
2161
+ return None
2162
+
2163
+ # Find contours
2164
+ contours, _ = cv2.findContours(
2165
+ component_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
2166
+ )
2167
+
2168
+ results = []
2169
+
2170
+ for contour in contours:
2171
+ # Filter by size
2172
+ area = cv2.contourArea(contour)
2173
+ if area < min_object_area:
2174
+ continue
2175
+
2176
+ if max_object_area is not None and area > max_object_area:
2177
+ continue
2178
+
2179
+ # Get minimum area rectangle
2180
+ rect = cv2.minAreaRect(contour)
2181
+ box_points = cv2.boxPoints(rect)
2182
+
2183
+ # Convert to geographic coordinates
2184
+ geo_points = []
2185
+ for x, y in box_points:
2186
+ gx, gy = transform * (x, y)
2187
+ geo_points.append((gx, gy))
2188
+
2189
+ # Create polygon
2190
+ poly = Polygon(geo_points)
2191
+ results.append((poly, confidence, area))
2192
+
2193
+ return results
2194
+
2195
+ import concurrent.futures
2196
+ from functools import partial
2197
+
2198
+ def process_component(args):
2199
+ """
2200
+ Helper function to process a single component
2201
+ """
2202
+ (
2203
+ label,
2204
+ labeled_mask,
2205
+ conf_data,
2206
+ transform,
2207
+ confidence_threshold,
2208
+ min_object_area,
2209
+ max_object_area,
2210
+ ) = args
2211
+
2212
+ # Create mask for this component
2213
+ component_mask = (labeled_mask == label).astype(np.uint8)
2214
+
2215
+ return _process_single_component(
2216
+ component_mask,
2217
+ conf_data,
2218
+ transform,
2219
+ confidence_threshold,
2220
+ min_object_area,
2221
+ max_object_area,
2222
+ )
2223
+
2224
+ start_time = time.time()
2137
2225
  print(f"Processing masks from: {masks_path}")
2138
2226
 
2227
+ if n_workers == -1:
2228
+ n_workers = os.cpu_count()
2229
+
2139
2230
  with rasterio.open(masks_path) as src:
2140
2231
  # Read mask and confidence bands
2141
2232
  mask_data = src.read(1)
@@ -2155,56 +2246,68 @@ class ObjectDetector:
2155
2246
  confidences = []
2156
2247
  pixels = []
2157
2248
 
2158
- # Add progress bar
2159
- for label in tqdm(range(1, num_features + 1), desc="Processing components"):
2160
- # Create mask for this component
2161
- component_mask = (labeled_mask == label).astype(np.uint8)
2162
-
2163
- # Get confidence value (mean of non-zero values in this region)
2164
- conf_region = conf_data[component_mask > 0]
2165
- if len(conf_region) > 0:
2166
- confidence = (
2167
- np.mean(conf_region) / 255.0
2168
- ) # Convert back to 0-1 range
2169
- else:
2170
- confidence = 0.0
2171
-
2172
- # Skip if confidence is below threshold
2173
- if confidence < confidence_threshold:
2174
- continue
2175
-
2176
- # Find contours
2177
- contours, _ = cv2.findContours(
2178
- component_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
2249
+ if n_workers is None or n_workers == 1:
2250
+ print(
2251
+ "Using single-threaded processing, you can speed up processing by setting n_workers > 1"
2179
2252
  )
2253
+ # Add progress bar
2254
+ for label in tqdm(
2255
+ range(1, num_features + 1), desc="Processing components"
2256
+ ):
2257
+ # Create mask for this component
2258
+ component_mask = (labeled_mask == label).astype(np.uint8)
2259
+
2260
+ result = _process_single_component(
2261
+ component_mask,
2262
+ conf_data,
2263
+ transform,
2264
+ confidence_threshold,
2265
+ min_object_area,
2266
+ max_object_area,
2267
+ )
2180
2268
 
2181
- for contour in contours:
2182
- # Filter by size
2183
- area = cv2.contourArea(contour)
2184
- if area < min_object_area:
2185
- continue
2186
-
2187
- if max_object_area is not None:
2188
- if area > max_object_area:
2189
- continue
2190
-
2191
- # Get minimum area rectangle
2192
- rect = cv2.minAreaRect(contour)
2193
- box_points = cv2.boxPoints(rect)
2269
+ if result:
2270
+ for poly, confidence, area in result:
2271
+ # Add to lists
2272
+ polygons.append(poly)
2273
+ confidences.append(confidence)
2274
+ pixels.append(area)
2194
2275
 
2195
- # Convert to geographic coordinates
2196
- geo_points = []
2197
- for x, y in box_points:
2198
- gx, gy = transform * (x, y)
2199
- geo_points.append((gx, gy))
2200
-
2201
- # Create polygon
2202
- poly = Polygon(geo_points)
2276
+ else:
2277
+ # Process components in parallel
2278
+ print(f"Using {n_workers} workers for parallel processing")
2279
+
2280
+ process_args = [
2281
+ (
2282
+ label,
2283
+ labeled_mask,
2284
+ conf_data,
2285
+ transform,
2286
+ confidence_threshold,
2287
+ min_object_area,
2288
+ max_object_area,
2289
+ )
2290
+ for label in range(1, num_features + 1)
2291
+ ]
2292
+
2293
+ with concurrent.futures.ThreadPoolExecutor(
2294
+ max_workers=n_workers
2295
+ ) as executor:
2296
+ results = list(
2297
+ tqdm(
2298
+ executor.map(process_component, process_args),
2299
+ total=num_features,
2300
+ desc="Processing components",
2301
+ )
2302
+ )
2203
2303
 
2204
- # Add to lists
2205
- polygons.append(poly)
2206
- confidences.append(confidence)
2207
- pixels.append(area)
2304
+ for result in results:
2305
+ if result:
2306
+ for poly, confidence, area in result:
2307
+ # Add to lists
2308
+ polygons.append(poly)
2309
+ confidences.append(confidence)
2310
+ pixels.append(area)
2208
2311
 
2209
2312
  # Create GeoDataFrame
2210
2313
  if polygons:
@@ -2223,8 +2326,12 @@ class ObjectDetector:
2223
2326
  gdf.to_file(output_path, driver="GeoJSON")
2224
2327
  print(f"Saved {len(gdf)} objects with confidence to {output_path}")
2225
2328
 
2329
+ end_time = time.time()
2330
+ print(f"Total processing time: {end_time - start_time:.2f} seconds")
2226
2331
  return gdf
2227
2332
  else:
2333
+ end_time = time.time()
2334
+ print(f"Total processing time: {end_time - start_time:.2f} seconds")
2228
2335
  print("No valid polygons found")
2229
2336
  return None
2230
2337
 
geoai/geoai.py CHANGED
@@ -1,15 +1,29 @@
1
1
  """Main module."""
2
2
 
3
+ import leafmap
4
+
3
5
  from .download import (
4
6
  download_naip,
5
7
  download_overture_buildings,
6
8
  download_pc_stac_item,
7
9
  pc_collection_list,
10
+ pc_item_asset_list,
8
11
  pc_stac_search,
9
12
  pc_stac_download,
13
+ read_pc_item_asset,
14
+ view_pc_item,
10
15
  )
16
+ from .classify import train_classifier, classify_image, classify_images
11
17
  from .extract import *
12
18
  from .hf import *
13
19
  from .segment import *
14
20
  from .train import object_detection, train_MaskRCNN_model
15
21
  from .utils import *
22
+
23
+
24
+ class Map(leafmap.Map):
25
+ """A subclass of leafmap.Map for GeoAI applications."""
26
+
27
+ def __init__(self, *args, **kwargs):
28
+ """Initialize the Map class."""
29
+ super().__init__(*args, **kwargs)
geoai/utils.py CHANGED
@@ -6053,3 +6053,165 @@ def try_common_architectures(state_dict):
6053
6053
 
6054
6054
  except Exception as e:
6055
6055
  print(f"- {name}: Failed to load - {str(e)}")
6056
+
6057
+
6058
+ def mosaic_geotiffs(input_dir, output_file, mask_file=None):
6059
+ """Create a mosaic from all GeoTIFF files as a Cloud Optimized GeoTIFF (COG).
6060
+
6061
+ This function identifies all GeoTIFF files in the specified directory,
6062
+ creates a seamless mosaic with proper handling of nodata values, and saves
6063
+ as a Cloud Optimized GeoTIFF format. If a mask file is provided, the output
6064
+ will be clipped to the extent of the mask.
6065
+
6066
+ Args:
6067
+ input_dir (str): Path to the directory containing GeoTIFF files.
6068
+ output_file (str): Path to the output Cloud Optimized GeoTIFF file.
6069
+ mask_file (str, optional): Path to a mask file to clip the output.
6070
+ If provided, the output will be clipped to the extent of this mask.
6071
+ Defaults to None.
6072
+
6073
+ Returns:
6074
+ bool: True if the mosaic was created successfully, False otherwise.
6075
+
6076
+ Examples:
6077
+ >>> mosaic_geotiffs('naip', 'merged_naip.tif')
6078
+ True
6079
+ >>> mosaic_geotiffs('naip', 'merged_naip.tif', 'boundary.tif')
6080
+ True
6081
+ """
6082
+ import glob
6083
+ from osgeo import gdal
6084
+
6085
+ gdal.UseExceptions()
6086
+ # Get all tif files in the directory
6087
+ tif_files = glob.glob(os.path.join(input_dir, "*.tif"))
6088
+
6089
+ if not tif_files:
6090
+ print("No GeoTIFF files found in the specified directory.")
6091
+ return False
6092
+
6093
+ # Analyze the first input file to determine compression and nodata settings
6094
+ ds = gdal.Open(tif_files[0])
6095
+ if ds is None:
6096
+ print(f"Unable to open {tif_files[0]}")
6097
+ return False
6098
+
6099
+ # Get driver metadata from the first file
6100
+ driver = ds.GetDriver()
6101
+ creation_options = []
6102
+
6103
+ # Check compression type
6104
+ metadata = ds.GetMetadata("IMAGE_STRUCTURE")
6105
+ if "COMPRESSION" in metadata:
6106
+ compression = metadata["COMPRESSION"]
6107
+ creation_options.append(f"COMPRESS={compression}")
6108
+ else:
6109
+ # Default compression if none detected
6110
+ creation_options.append("COMPRESS=LZW")
6111
+
6112
+ # Add COG-specific creation options
6113
+ creation_options.extend(["TILED=YES", "BLOCKXSIZE=512", "BLOCKYSIZE=512"])
6114
+
6115
+ # Check for nodata value in the first band of the first file
6116
+ band = ds.GetRasterBand(1)
6117
+ has_nodata = band.GetNoDataValue() is not None
6118
+ nodata_value = band.GetNoDataValue() if has_nodata else None
6119
+
6120
+ # Close the dataset
6121
+ ds = None
6122
+
6123
+ # Create a temporary VRT (Virtual Dataset)
6124
+ vrt_path = os.path.join(input_dir, "temp_mosaic.vrt")
6125
+
6126
+ # Build VRT from input files with proper nodata handling
6127
+ vrt_options = gdal.BuildVRTOptions(
6128
+ resampleAlg="nearest",
6129
+ srcNodata=nodata_value if has_nodata else None,
6130
+ VRTNodata=nodata_value if has_nodata else None,
6131
+ )
6132
+ vrt_dataset = gdal.BuildVRT(vrt_path, tif_files, options=vrt_options)
6133
+
6134
+ # Close the VRT dataset to flush it to disk
6135
+ vrt_dataset = None
6136
+
6137
+ # Create temp mosaic
6138
+ temp_mosaic = output_file + ".temp.tif"
6139
+
6140
+ # Convert VRT to GeoTIFF with the same compression as input
6141
+ translate_options = gdal.TranslateOptions(
6142
+ format="GTiff",
6143
+ creationOptions=creation_options,
6144
+ noData=nodata_value if has_nodata else None,
6145
+ )
6146
+ gdal.Translate(temp_mosaic, vrt_path, options=translate_options)
6147
+
6148
+ # Apply mask if provided
6149
+ if mask_file and os.path.exists(mask_file):
6150
+ print(f"Clipping mosaic to mask: {mask_file}")
6151
+
6152
+ # Create a temporary clipped file
6153
+ clipped_mosaic = output_file + ".clipped.tif"
6154
+
6155
+ # Open mask file
6156
+ mask_ds = gdal.Open(mask_file)
6157
+ if mask_ds is None:
6158
+ print(f"Unable to open mask file: {mask_file}")
6159
+ # Continue without clipping
6160
+ else:
6161
+ # Get mask extent
6162
+ mask_geotransform = mask_ds.GetGeoTransform()
6163
+ mask_projection = mask_ds.GetProjection()
6164
+ mask_ulx = mask_geotransform[0]
6165
+ mask_uly = mask_geotransform[3]
6166
+ mask_lrx = mask_ulx + (mask_geotransform[1] * mask_ds.RasterXSize)
6167
+ mask_lry = mask_uly + (mask_geotransform[5] * mask_ds.RasterYSize)
6168
+
6169
+ # Close mask dataset
6170
+ mask_ds = None
6171
+
6172
+ # Use warp options to clip
6173
+ warp_options = gdal.WarpOptions(
6174
+ format="GTiff",
6175
+ outputBounds=[mask_ulx, mask_lry, mask_lrx, mask_uly],
6176
+ dstSRS=mask_projection,
6177
+ creationOptions=creation_options,
6178
+ srcNodata=nodata_value if has_nodata else None,
6179
+ dstNodata=nodata_value if has_nodata else None,
6180
+ )
6181
+
6182
+ # Apply clipping
6183
+ gdal.Warp(clipped_mosaic, temp_mosaic, options=warp_options)
6184
+
6185
+ # Remove the unclipped temp mosaic and use the clipped one
6186
+ os.remove(temp_mosaic)
6187
+ temp_mosaic = clipped_mosaic
6188
+
6189
+ # Create internal overviews for the temp mosaic
6190
+ ds = gdal.Open(temp_mosaic, gdal.GA_Update)
6191
+ overview_list = [2, 4, 8, 16, 32]
6192
+ ds.BuildOverviews("NEAREST", overview_list)
6193
+ ds = None # Close the dataset to ensure overviews are written
6194
+
6195
+ # Convert the temp mosaic to a proper COG
6196
+ cog_options = gdal.TranslateOptions(
6197
+ format="GTiff",
6198
+ creationOptions=[
6199
+ "TILED=YES",
6200
+ "COPY_SRC_OVERVIEWS=YES",
6201
+ "COMPRESS=DEFLATE",
6202
+ "PREDICTOR=2",
6203
+ "BLOCKXSIZE=512",
6204
+ "BLOCKYSIZE=512",
6205
+ ],
6206
+ noData=nodata_value if has_nodata else None,
6207
+ )
6208
+ gdal.Translate(output_file, temp_mosaic, options=cog_options)
6209
+
6210
+ # Clean up temporary files
6211
+ if os.path.exists(vrt_path):
6212
+ os.remove(vrt_path)
6213
+ if os.path.exists(temp_mosaic):
6214
+ os.remove(temp_mosaic)
6215
+
6216
+ print(f"Cloud Optimized GeoTIFF mosaic created successfully: {output_file}")
6217
+ return True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geoai-py
3
- Version: 0.4.2
3
+ Version: 0.5.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
@@ -0,0 +1,16 @@
1
+ geoai/__init__.py,sha256=jGJl23LMoJDPdWTrlWAPxXK8cjwWllGQcFQ5IJOJ0s0,3765
2
+ geoai/classify.py,sha256=_e-193QzAx3pIxUflPIsIs1qZevQx5ADu7i3bOL1G70,35055
3
+ geoai/download.py,sha256=lJ1GsJOZsKc2i6_dQyPV-XXIXmlADOpmSBo-wha4DEU,40892
4
+ geoai/extract.py,sha256=GocJufMmrwEWxNBL1J91EXXHL8AKcO8m_lmtUF5AKPw,119102
5
+ geoai/geoai.py,sha256=5zXt7WQ0FbiksXKQ9fBNnfa9dhJ3QVd8LXicMxyynTg,698
6
+ geoai/hf.py,sha256=mLKGxEAS5eHkxZLwuLpYc1o7e3-7QIXdBv-QUY-RkFk,17072
7
+ geoai/segment.py,sha256=g3YW17ftr--CKq6VB32TJEPY8owGQ7uQ0sg_tUT2ooE,13681
8
+ geoai/segmentation.py,sha256=AtPzCvguHAEeuyXafa4bzMFATvltEYcah1B8ZMfkM_s,11373
9
+ geoai/train.py,sha256=-l2j1leTxDnFDLaBslu1q6CobXjm3LEdiQwUWOU8P6M,40088
10
+ geoai/utils.py,sha256=xxhIkNFFtv-s_tzrlAyD4qIOaxo2JtKJEzG6vFgA5y8,238737
11
+ geoai_py-0.5.0.dist-info/licenses/LICENSE,sha256=vN2L5U7cZ6ZkOHFmc8WiGlsogWsZc5dllMeNxnKVOZg,1070
12
+ geoai_py-0.5.0.dist-info/METADATA,sha256=tzHz2Q1iTpCO25T8W3N2RtyaHdwdb9umS9YIgCciFdo,6049
13
+ geoai_py-0.5.0.dist-info/WHEEL,sha256=MAQBAzGbXNI3bUmkDsiV_duv8i-gcdnLzw7cfUFwqhU,109
14
+ geoai_py-0.5.0.dist-info/entry_points.txt,sha256=uGp3Az3HURIsRHP9v-ys0hIbUuBBNUfXv6VbYHIXeg4,41
15
+ geoai_py-0.5.0.dist-info/top_level.txt,sha256=1YkCUWu-ii-0qIex7kbwAvfei-gos9ycyDyUCJPNWHY,6
16
+ geoai_py-0.5.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (77.0.1)
2
+ Generator: setuptools (78.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py2-none-any
5
5
  Tag: py3-none-any
@@ -1,15 +0,0 @@
1
- geoai/__init__.py,sha256=kHB6b3uOdCoxMpIpfpXtPxg-KJQVsKJveb8zTw5Mxzs,3592
2
- geoai/download.py,sha256=SE81OrhH2XSH02xtAXQyh5ltBqA8K7ksGT1Lm6SMqx8,37593
3
- geoai/extract.py,sha256=bR8TkUXncW0rinsBO5oxlVFGsSVoj6xnm5xdDc-J1Xk,115490
4
- geoai/geoai.py,sha256=qY1HWmJQ0ZgIPtlgWd4gpuCGw6dovdg8D5pAtHqfr8U,334
5
- geoai/hf.py,sha256=mLKGxEAS5eHkxZLwuLpYc1o7e3-7QIXdBv-QUY-RkFk,17072
6
- geoai/segment.py,sha256=g3YW17ftr--CKq6VB32TJEPY8owGQ7uQ0sg_tUT2ooE,13681
7
- geoai/segmentation.py,sha256=AtPzCvguHAEeuyXafa4bzMFATvltEYcah1B8ZMfkM_s,11373
8
- geoai/train.py,sha256=-l2j1leTxDnFDLaBslu1q6CobXjm3LEdiQwUWOU8P6M,40088
9
- geoai/utils.py,sha256=Wg9jbMBKUZSGUmU8Vkp6v19QcDNg5KmcyZxuHqJvgnc,233016
10
- geoai_py-0.4.2.dist-info/licenses/LICENSE,sha256=vN2L5U7cZ6ZkOHFmc8WiGlsogWsZc5dllMeNxnKVOZg,1070
11
- geoai_py-0.4.2.dist-info/METADATA,sha256=Sc6m-MyCti-_Dnan5wp6GIy88LwuBAF_IYrFF34SRJE,6049
12
- geoai_py-0.4.2.dist-info/WHEEL,sha256=9bhjOwO--Rs91xaPcBdlYFUmIudhuXqFlPriQeYQITw,109
13
- geoai_py-0.4.2.dist-info/entry_points.txt,sha256=uGp3Az3HURIsRHP9v-ys0hIbUuBBNUfXv6VbYHIXeg4,41
14
- geoai_py-0.4.2.dist-info/top_level.txt,sha256=1YkCUWu-ii-0qIex7kbwAvfei-gos9ycyDyUCJPNWHY,6
15
- geoai_py-0.4.2.dist-info/RECORD,,