geoai-py 0.3.3__py2.py3-none-any.whl → 0.3.4__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/extract.py +382 -28
- geoai/utils.py +42 -17
- {geoai_py-0.3.3.dist-info → geoai_py-0.3.4.dist-info}/METADATA +1 -1
- geoai_py-0.3.4.dist-info/RECORD +13 -0
- geoai_py-0.3.3.dist-info/RECORD +0 -13
- {geoai_py-0.3.3.dist-info → geoai_py-0.3.4.dist-info}/LICENSE +0 -0
- {geoai_py-0.3.3.dist-info → geoai_py-0.3.4.dist-info}/WHEEL +0 -0
- {geoai_py-0.3.3.dist-info → geoai_py-0.3.4.dist-info}/entry_points.txt +0 -0
- {geoai_py-0.3.3.dist-info → geoai_py-0.3.4.dist-info}/top_level.txt +0 -0
geoai/__init__.py
CHANGED
geoai/extract.py
CHANGED
|
@@ -13,6 +13,7 @@ import rasterio
|
|
|
13
13
|
from rasterio.windows import Window
|
|
14
14
|
from rasterio.features import shapes
|
|
15
15
|
from huggingface_hub import hf_hub_download
|
|
16
|
+
import scipy.ndimage as ndimage
|
|
16
17
|
from .utils import get_raster_stats
|
|
17
18
|
|
|
18
19
|
try:
|
|
@@ -25,34 +26,74 @@ except ImportError as e:
|
|
|
25
26
|
|
|
26
27
|
class CustomDataset(NonGeoDataset):
|
|
27
28
|
"""
|
|
28
|
-
A TorchGeo dataset for object extraction.
|
|
29
|
-
|
|
29
|
+
A TorchGeo dataset for object extraction with overlapping tiles support.
|
|
30
|
+
|
|
31
|
+
This dataset class creates overlapping image tiles for object detection,
|
|
32
|
+
ensuring complete coverage of the input raster including right and bottom edges.
|
|
33
|
+
It inherits from NonGeoDataset to avoid spatial indexing issues.
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
raster_path: Path to the input raster file.
|
|
37
|
+
chip_size: Size of image chips to extract (height, width).
|
|
38
|
+
overlap: Amount of overlap between adjacent tiles (0.0-1.0).
|
|
39
|
+
transforms: Transforms to apply to the image.
|
|
40
|
+
verbose: Whether to print detailed processing information.
|
|
41
|
+
stride_x: Horizontal stride between tiles based on overlap.
|
|
42
|
+
stride_y: Vertical stride between tiles based on overlap.
|
|
43
|
+
row_starts: Starting Y positions for each row of tiles.
|
|
44
|
+
col_starts: Starting X positions for each column of tiles.
|
|
45
|
+
crs: Coordinate reference system of the raster.
|
|
46
|
+
transform: Affine transform of the raster.
|
|
47
|
+
height: Height of the raster in pixels.
|
|
48
|
+
width: Width of the raster in pixels.
|
|
49
|
+
count: Number of bands in the raster.
|
|
50
|
+
bounds: Geographic bounds of the raster (west, south, east, north).
|
|
51
|
+
roi: Shapely box representing the region of interest.
|
|
52
|
+
rows: Number of rows of tiles.
|
|
53
|
+
cols: Number of columns of tiles.
|
|
54
|
+
raster_stats: Statistics of the raster.
|
|
30
55
|
"""
|
|
31
56
|
|
|
32
57
|
def __init__(
|
|
33
|
-
self,
|
|
58
|
+
self,
|
|
59
|
+
raster_path,
|
|
60
|
+
chip_size=(512, 512),
|
|
61
|
+
overlap=0.5,
|
|
62
|
+
transforms=None,
|
|
63
|
+
verbose=False,
|
|
34
64
|
):
|
|
35
65
|
"""
|
|
36
|
-
Initialize the dataset.
|
|
66
|
+
Initialize the dataset with overlapping tiles.
|
|
37
67
|
|
|
38
68
|
Args:
|
|
39
|
-
raster_path: Path to the input raster file
|
|
40
|
-
chip_size: Size of image chips to extract (height, width)
|
|
41
|
-
|
|
42
|
-
|
|
69
|
+
raster_path: Path to the input raster file.
|
|
70
|
+
chip_size: Size of image chips to extract (height, width). Default is (512, 512).
|
|
71
|
+
overlap: Amount of overlap between adjacent tiles (0.0-1.0). Default is 0.5 (50%).
|
|
72
|
+
transforms: Transforms to apply to the image. Default is None.
|
|
73
|
+
verbose: Whether to print detailed processing information. Default is False.
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
ValueError: If overlap is too high resulting in non-positive stride.
|
|
43
77
|
"""
|
|
44
78
|
super().__init__()
|
|
45
79
|
|
|
46
80
|
# Initialize parameters
|
|
47
81
|
self.raster_path = raster_path
|
|
48
82
|
self.chip_size = chip_size
|
|
83
|
+
self.overlap = overlap
|
|
49
84
|
self.transforms = transforms
|
|
50
85
|
self.verbose = verbose
|
|
51
|
-
|
|
52
|
-
# For tracking warnings about multi-band images
|
|
53
86
|
self.warned_about_bands = False
|
|
54
87
|
|
|
55
|
-
#
|
|
88
|
+
# Calculate stride based on overlap
|
|
89
|
+
self.stride_x = int(chip_size[1] * (1 - overlap))
|
|
90
|
+
self.stride_y = int(chip_size[0] * (1 - overlap))
|
|
91
|
+
|
|
92
|
+
if self.stride_x <= 0 or self.stride_y <= 0:
|
|
93
|
+
raise ValueError(
|
|
94
|
+
f"Overlap {overlap} is too high, resulting in non-positive stride"
|
|
95
|
+
)
|
|
96
|
+
|
|
56
97
|
with rasterio.open(self.raster_path) as src:
|
|
57
98
|
self.crs = src.crs
|
|
58
99
|
self.transform = src.transform
|
|
@@ -63,43 +104,78 @@ class CustomDataset(NonGeoDataset):
|
|
|
63
104
|
# Define the bounds of the dataset
|
|
64
105
|
west, south, east, north = src.bounds
|
|
65
106
|
self.bounds = (west, south, east, north)
|
|
66
|
-
|
|
67
|
-
# Define the ROI for the dataset
|
|
68
107
|
self.roi = box(*self.bounds)
|
|
69
108
|
|
|
70
|
-
# Calculate
|
|
71
|
-
|
|
72
|
-
self.
|
|
73
|
-
|
|
109
|
+
# Calculate starting positions for each tile
|
|
110
|
+
self.row_starts = []
|
|
111
|
+
self.col_starts = []
|
|
112
|
+
|
|
113
|
+
# Normal row starts using stride
|
|
114
|
+
for r in range((self.height - 1) // self.stride_y):
|
|
115
|
+
self.row_starts.append(r * self.stride_y)
|
|
116
|
+
|
|
117
|
+
# Add a special last row that ensures we reach the bottom edge
|
|
118
|
+
if self.height > self.chip_size[0]:
|
|
119
|
+
self.row_starts.append(max(0, self.height - self.chip_size[0]))
|
|
120
|
+
else:
|
|
121
|
+
# If the image is smaller than chip size, just start at 0
|
|
122
|
+
if not self.row_starts:
|
|
123
|
+
self.row_starts.append(0)
|
|
124
|
+
|
|
125
|
+
# Normal column starts using stride
|
|
126
|
+
for c in range((self.width - 1) // self.stride_x):
|
|
127
|
+
self.col_starts.append(c * self.stride_x)
|
|
128
|
+
|
|
129
|
+
# Add a special last column that ensures we reach the right edge
|
|
130
|
+
if self.width > self.chip_size[1]:
|
|
131
|
+
self.col_starts.append(max(0, self.width - self.chip_size[1]))
|
|
132
|
+
else:
|
|
133
|
+
# If the image is smaller than chip size, just start at 0
|
|
134
|
+
if not self.col_starts:
|
|
135
|
+
self.col_starts.append(0)
|
|
136
|
+
|
|
137
|
+
# Update rows and cols based on actual starting positions
|
|
138
|
+
self.rows = len(self.row_starts)
|
|
139
|
+
self.cols = len(self.col_starts)
|
|
74
140
|
|
|
75
141
|
print(
|
|
76
142
|
f"Dataset initialized with {self.rows} rows and {self.cols} columns of chips"
|
|
77
143
|
)
|
|
78
144
|
print(f"Image dimensions: {self.width} x {self.height} pixels")
|
|
79
145
|
print(f"Chip size: {self.chip_size[1]} x {self.chip_size[0]} pixels")
|
|
146
|
+
print(
|
|
147
|
+
f"Overlap: {overlap*100}% (stride_x={self.stride_x}, stride_y={self.stride_y})"
|
|
148
|
+
)
|
|
80
149
|
if src.crs:
|
|
81
150
|
print(f"CRS: {src.crs}")
|
|
82
151
|
|
|
83
|
-
#
|
|
152
|
+
# Get raster stats
|
|
84
153
|
self.raster_stats = get_raster_stats(raster_path, divide_by=255)
|
|
85
154
|
|
|
86
155
|
def __getitem__(self, idx):
|
|
87
156
|
"""
|
|
88
157
|
Get an image chip from the dataset by index.
|
|
89
158
|
|
|
159
|
+
Retrieves an image tile with the specified overlap pattern, ensuring
|
|
160
|
+
proper coverage of the entire raster including edges.
|
|
161
|
+
|
|
90
162
|
Args:
|
|
91
|
-
idx: Index of the chip
|
|
163
|
+
idx: Index of the chip to retrieve.
|
|
92
164
|
|
|
93
165
|
Returns:
|
|
94
|
-
|
|
166
|
+
dict: Dictionary containing:
|
|
167
|
+
- image: Image tensor.
|
|
168
|
+
- bbox: Geographic bounding box for the window.
|
|
169
|
+
- coords: Pixel coordinates as tensor [i, j].
|
|
170
|
+
- window_size: Window size as tensor [width, height].
|
|
95
171
|
"""
|
|
96
172
|
# Convert flat index to grid position
|
|
97
173
|
row = idx // self.cols
|
|
98
174
|
col = idx % self.cols
|
|
99
175
|
|
|
100
|
-
#
|
|
101
|
-
|
|
102
|
-
|
|
176
|
+
# Get pre-calculated starting positions
|
|
177
|
+
j = self.row_starts[row]
|
|
178
|
+
i = self.col_starts[col]
|
|
103
179
|
|
|
104
180
|
# Read window from raster
|
|
105
181
|
with rasterio.open(self.raster_path) as src:
|
|
@@ -166,7 +242,12 @@ class CustomDataset(NonGeoDataset):
|
|
|
166
242
|
}
|
|
167
243
|
|
|
168
244
|
def __len__(self):
|
|
169
|
-
"""
|
|
245
|
+
"""
|
|
246
|
+
Return the number of samples in the dataset.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
int: Total number of tiles in the dataset.
|
|
250
|
+
"""
|
|
170
251
|
return self.rows * self.cols
|
|
171
252
|
|
|
172
253
|
|
|
@@ -761,7 +842,9 @@ class ObjectDetector:
|
|
|
761
842
|
print(f"- Edge buffer size: {edge_buffer} pixels")
|
|
762
843
|
|
|
763
844
|
# Create dataset
|
|
764
|
-
dataset = CustomDataset(
|
|
845
|
+
dataset = CustomDataset(
|
|
846
|
+
raster_path=raster_path, chip_size=chip_size, overlap=overlap
|
|
847
|
+
)
|
|
765
848
|
self.raster_stats = dataset.raster_stats
|
|
766
849
|
|
|
767
850
|
# Custom collate function to handle Shapely objects
|
|
@@ -969,6 +1052,7 @@ class ObjectDetector:
|
|
|
969
1052
|
)
|
|
970
1053
|
chip_size = kwargs.get("chip_size", self.chip_size)
|
|
971
1054
|
mask_threshold = kwargs.get("mask_threshold", self.mask_threshold)
|
|
1055
|
+
overlap = kwargs.get("overlap", self.overlap)
|
|
972
1056
|
|
|
973
1057
|
# Set default output path if not provided
|
|
974
1058
|
if output_path is None:
|
|
@@ -982,7 +1066,10 @@ class ObjectDetector:
|
|
|
982
1066
|
|
|
983
1067
|
# Create dataset
|
|
984
1068
|
dataset = CustomDataset(
|
|
985
|
-
raster_path=raster_path,
|
|
1069
|
+
raster_path=raster_path,
|
|
1070
|
+
chip_size=chip_size,
|
|
1071
|
+
overlap=overlap,
|
|
1072
|
+
verbose=verbose,
|
|
986
1073
|
)
|
|
987
1074
|
|
|
988
1075
|
# Store a flag to avoid repetitive messages
|
|
@@ -1857,8 +1944,7 @@ class CarDetector(ObjectDetector):
|
|
|
1857
1944
|
"""
|
|
1858
1945
|
Car detection using a pre-trained Mask R-CNN model.
|
|
1859
1946
|
|
|
1860
|
-
This class extends the
|
|
1861
|
-
`ObjectDetector` class with additional methods for car detection."
|
|
1947
|
+
This class extends the `ObjectDetector` class with additional methods for car detection.
|
|
1862
1948
|
"""
|
|
1863
1949
|
|
|
1864
1950
|
def __init__(
|
|
@@ -1877,6 +1963,274 @@ class CarDetector(ObjectDetector):
|
|
|
1877
1963
|
model_path=model_path, repo_id=repo_id, model=model, device=device
|
|
1878
1964
|
)
|
|
1879
1965
|
|
|
1966
|
+
def generate_masks(
|
|
1967
|
+
self,
|
|
1968
|
+
raster_path,
|
|
1969
|
+
output_path=None,
|
|
1970
|
+
confidence_threshold=None,
|
|
1971
|
+
mask_threshold=None,
|
|
1972
|
+
overlap=0.25,
|
|
1973
|
+
batch_size=4,
|
|
1974
|
+
verbose=False,
|
|
1975
|
+
):
|
|
1976
|
+
"""
|
|
1977
|
+
Save masks with confidence values as a multi-band GeoTIFF.
|
|
1978
|
+
|
|
1979
|
+
Args:
|
|
1980
|
+
raster_path: Path to input raster
|
|
1981
|
+
output_path: Path for output GeoTIFF
|
|
1982
|
+
confidence_threshold: Minimum confidence score (0.0-1.0)
|
|
1983
|
+
mask_threshold: Threshold for mask binarization (0.0-1.0)
|
|
1984
|
+
overlap: Overlap between tiles (0.0-1.0)
|
|
1985
|
+
batch_size: Batch size for processing
|
|
1986
|
+
verbose: Whether to print detailed processing information
|
|
1987
|
+
|
|
1988
|
+
Returns:
|
|
1989
|
+
Path to the saved GeoTIFF
|
|
1990
|
+
"""
|
|
1991
|
+
# Use provided thresholds or fall back to instance defaults
|
|
1992
|
+
if confidence_threshold is None:
|
|
1993
|
+
confidence_threshold = self.confidence_threshold
|
|
1994
|
+
if mask_threshold is None:
|
|
1995
|
+
mask_threshold = self.mask_threshold
|
|
1996
|
+
|
|
1997
|
+
# Default output path
|
|
1998
|
+
if output_path is None:
|
|
1999
|
+
output_path = os.path.splitext(raster_path)[0] + "_masks_conf.tif"
|
|
2000
|
+
|
|
2001
|
+
# Process the raster to get individual masks with confidence
|
|
2002
|
+
with rasterio.open(raster_path) as src:
|
|
2003
|
+
# Create dataset with the specified overlap
|
|
2004
|
+
dataset = CustomDataset(
|
|
2005
|
+
raster_path=raster_path,
|
|
2006
|
+
chip_size=self.chip_size,
|
|
2007
|
+
overlap=overlap,
|
|
2008
|
+
verbose=verbose,
|
|
2009
|
+
)
|
|
2010
|
+
|
|
2011
|
+
# Create output profile
|
|
2012
|
+
output_profile = src.profile.copy()
|
|
2013
|
+
output_profile.update(
|
|
2014
|
+
dtype=rasterio.uint8,
|
|
2015
|
+
count=2, # Two bands: mask and confidence
|
|
2016
|
+
compress="lzw",
|
|
2017
|
+
nodata=0,
|
|
2018
|
+
)
|
|
2019
|
+
|
|
2020
|
+
# Initialize mask and confidence arrays
|
|
2021
|
+
mask_array = np.zeros((src.height, src.width), dtype=np.uint8)
|
|
2022
|
+
conf_array = np.zeros((src.height, src.width), dtype=np.uint8)
|
|
2023
|
+
|
|
2024
|
+
# Define custom collate function to handle Shapely objects
|
|
2025
|
+
def custom_collate(batch):
|
|
2026
|
+
"""
|
|
2027
|
+
Custom collate function that handles Shapely geometries
|
|
2028
|
+
by keeping them as Python objects rather than trying to collate them.
|
|
2029
|
+
"""
|
|
2030
|
+
elem = batch[0]
|
|
2031
|
+
if isinstance(elem, dict):
|
|
2032
|
+
result = {}
|
|
2033
|
+
for key in elem:
|
|
2034
|
+
if key == "bbox":
|
|
2035
|
+
# Don't collate shapely objects, keep as list
|
|
2036
|
+
result[key] = [d[key] for d in batch]
|
|
2037
|
+
else:
|
|
2038
|
+
# For tensors and other collatable types
|
|
2039
|
+
try:
|
|
2040
|
+
result[key] = (
|
|
2041
|
+
torch.utils.data._utils.collate.default_collate(
|
|
2042
|
+
[d[key] for d in batch]
|
|
2043
|
+
)
|
|
2044
|
+
)
|
|
2045
|
+
except TypeError:
|
|
2046
|
+
# Fall back to list for non-collatable types
|
|
2047
|
+
result[key] = [d[key] for d in batch]
|
|
2048
|
+
return result
|
|
2049
|
+
else:
|
|
2050
|
+
# Default collate for non-dict types
|
|
2051
|
+
return torch.utils.data._utils.collate.default_collate(batch)
|
|
2052
|
+
|
|
2053
|
+
# Create dataloader with custom collate function
|
|
2054
|
+
dataloader = torch.utils.data.DataLoader(
|
|
2055
|
+
dataset,
|
|
2056
|
+
batch_size=batch_size,
|
|
2057
|
+
shuffle=False,
|
|
2058
|
+
num_workers=0,
|
|
2059
|
+
collate_fn=custom_collate,
|
|
2060
|
+
)
|
|
2061
|
+
|
|
2062
|
+
# Process batches
|
|
2063
|
+
print(f"Processing raster with {len(dataloader)} batches")
|
|
2064
|
+
for batch in tqdm(dataloader):
|
|
2065
|
+
# Move images to device
|
|
2066
|
+
images = batch["image"].to(self.device)
|
|
2067
|
+
coords = batch["coords"] # Tensor of shape [batch_size, 2]
|
|
2068
|
+
|
|
2069
|
+
# Run inference
|
|
2070
|
+
with torch.no_grad():
|
|
2071
|
+
predictions = self.model(images)
|
|
2072
|
+
|
|
2073
|
+
# Process predictions
|
|
2074
|
+
for idx, prediction in enumerate(predictions):
|
|
2075
|
+
masks = prediction["masks"].cpu().numpy()
|
|
2076
|
+
scores = prediction["scores"].cpu().numpy()
|
|
2077
|
+
|
|
2078
|
+
# Filter by confidence threshold
|
|
2079
|
+
valid_indices = scores >= confidence_threshold
|
|
2080
|
+
masks = masks[valid_indices]
|
|
2081
|
+
scores = scores[valid_indices]
|
|
2082
|
+
|
|
2083
|
+
# Skip if no valid predictions
|
|
2084
|
+
if len(masks) == 0:
|
|
2085
|
+
continue
|
|
2086
|
+
|
|
2087
|
+
# Get window coordinates
|
|
2088
|
+
i, j = coords[idx].cpu().numpy()
|
|
2089
|
+
|
|
2090
|
+
# Process each mask
|
|
2091
|
+
for mask_idx, mask in enumerate(masks):
|
|
2092
|
+
# Convert to binary mask
|
|
2093
|
+
binary_mask = (mask[0] > mask_threshold).astype(np.uint8) * 255
|
|
2094
|
+
conf_value = int(scores[mask_idx] * 255) # Scale to 0-255
|
|
2095
|
+
|
|
2096
|
+
# Update the mask and confidence arrays
|
|
2097
|
+
h, w = binary_mask.shape
|
|
2098
|
+
valid_h = min(h, src.height - j)
|
|
2099
|
+
valid_w = min(w, src.width - i)
|
|
2100
|
+
|
|
2101
|
+
if valid_h > 0 and valid_w > 0:
|
|
2102
|
+
# Use maximum for overlapping regions in the mask
|
|
2103
|
+
mask_array[j : j + valid_h, i : i + valid_w] = np.maximum(
|
|
2104
|
+
mask_array[j : j + valid_h, i : i + valid_w],
|
|
2105
|
+
binary_mask[:valid_h, :valid_w],
|
|
2106
|
+
)
|
|
2107
|
+
|
|
2108
|
+
# For confidence, only update where mask is positive
|
|
2109
|
+
# and confidence is higher than existing
|
|
2110
|
+
mask_region = binary_mask[:valid_h, :valid_w] > 0
|
|
2111
|
+
if np.any(mask_region):
|
|
2112
|
+
# Only update where mask is positive and new confidence is higher
|
|
2113
|
+
current_conf = conf_array[
|
|
2114
|
+
j : j + valid_h, i : i + valid_w
|
|
2115
|
+
]
|
|
2116
|
+
|
|
2117
|
+
# Where to update confidence (mask positive & higher confidence)
|
|
2118
|
+
update_mask = np.logical_and(
|
|
2119
|
+
mask_region,
|
|
2120
|
+
np.logical_or(
|
|
2121
|
+
current_conf == 0, current_conf < conf_value
|
|
2122
|
+
),
|
|
2123
|
+
)
|
|
2124
|
+
|
|
2125
|
+
if np.any(update_mask):
|
|
2126
|
+
conf_array[j : j + valid_h, i : i + valid_w][
|
|
2127
|
+
update_mask
|
|
2128
|
+
] = conf_value
|
|
2129
|
+
|
|
2130
|
+
# Write to GeoTIFF
|
|
2131
|
+
with rasterio.open(output_path, "w", **output_profile) as dst:
|
|
2132
|
+
dst.write(mask_array, 1)
|
|
2133
|
+
dst.write(conf_array, 2)
|
|
2134
|
+
|
|
2135
|
+
print(f"Masks with confidence values saved to {output_path}")
|
|
2136
|
+
return output_path
|
|
2137
|
+
|
|
2138
|
+
def vectorize_masks(self, masks_path, output_path=None, **kwargs):
|
|
2139
|
+
"""
|
|
2140
|
+
Convert masks with confidence to vector polygons.
|
|
2141
|
+
|
|
2142
|
+
Args:
|
|
2143
|
+
masks_path: Path to masks GeoTIFF with confidence band
|
|
2144
|
+
output_path: Path for output GeoJSON
|
|
2145
|
+
**kwargs: Additional parameters
|
|
2146
|
+
|
|
2147
|
+
Returns:
|
|
2148
|
+
GeoDataFrame with car detections and confidence values
|
|
2149
|
+
"""
|
|
2150
|
+
|
|
2151
|
+
print(f"Processing masks from: {masks_path}")
|
|
2152
|
+
|
|
2153
|
+
with rasterio.open(masks_path) as src:
|
|
2154
|
+
# Read mask and confidence bands
|
|
2155
|
+
mask_data = src.read(1)
|
|
2156
|
+
conf_data = src.read(2)
|
|
2157
|
+
transform = src.transform
|
|
2158
|
+
crs = src.crs
|
|
2159
|
+
|
|
2160
|
+
# Convert to binary mask
|
|
2161
|
+
binary_mask = mask_data > 0
|
|
2162
|
+
|
|
2163
|
+
# Find connected components
|
|
2164
|
+
labeled_mask, num_features = ndimage.label(binary_mask)
|
|
2165
|
+
print(f"Found {num_features} connected components")
|
|
2166
|
+
|
|
2167
|
+
# Process each component
|
|
2168
|
+
car_polygons = []
|
|
2169
|
+
car_confidences = []
|
|
2170
|
+
|
|
2171
|
+
# Add progress bar
|
|
2172
|
+
for label in tqdm(range(1, num_features + 1), desc="Processing components"):
|
|
2173
|
+
# Create mask for this component
|
|
2174
|
+
component_mask = (labeled_mask == label).astype(np.uint8)
|
|
2175
|
+
|
|
2176
|
+
# Get confidence value (mean of non-zero values in this region)
|
|
2177
|
+
conf_region = conf_data[component_mask > 0]
|
|
2178
|
+
if len(conf_region) > 0:
|
|
2179
|
+
confidence = (
|
|
2180
|
+
np.mean(conf_region) / 255.0
|
|
2181
|
+
) # Convert back to 0-1 range
|
|
2182
|
+
else:
|
|
2183
|
+
confidence = 0.0
|
|
2184
|
+
|
|
2185
|
+
# Find contours
|
|
2186
|
+
contours, _ = cv2.findContours(
|
|
2187
|
+
component_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
|
|
2188
|
+
)
|
|
2189
|
+
|
|
2190
|
+
for contour in contours:
|
|
2191
|
+
# Filter by size
|
|
2192
|
+
area = cv2.contourArea(contour)
|
|
2193
|
+
if area < kwargs.get("min_object_area", 100):
|
|
2194
|
+
continue
|
|
2195
|
+
|
|
2196
|
+
# Get minimum area rectangle
|
|
2197
|
+
rect = cv2.minAreaRect(contour)
|
|
2198
|
+
box_points = cv2.boxPoints(rect)
|
|
2199
|
+
|
|
2200
|
+
# Convert to geographic coordinates
|
|
2201
|
+
geo_points = []
|
|
2202
|
+
for x, y in box_points:
|
|
2203
|
+
gx, gy = transform * (x, y)
|
|
2204
|
+
geo_points.append((gx, gy))
|
|
2205
|
+
|
|
2206
|
+
# Create polygon
|
|
2207
|
+
poly = Polygon(geo_points)
|
|
2208
|
+
|
|
2209
|
+
# Add to lists
|
|
2210
|
+
car_polygons.append(poly)
|
|
2211
|
+
car_confidences.append(confidence)
|
|
2212
|
+
|
|
2213
|
+
# Create GeoDataFrame
|
|
2214
|
+
if car_polygons:
|
|
2215
|
+
gdf = gpd.GeoDataFrame(
|
|
2216
|
+
{
|
|
2217
|
+
"geometry": car_polygons,
|
|
2218
|
+
"confidence": car_confidences,
|
|
2219
|
+
"class": [1] * len(car_polygons),
|
|
2220
|
+
},
|
|
2221
|
+
crs=crs,
|
|
2222
|
+
)
|
|
2223
|
+
|
|
2224
|
+
# Save to file if requested
|
|
2225
|
+
if output_path:
|
|
2226
|
+
gdf.to_file(output_path, driver="GeoJSON")
|
|
2227
|
+
print(f"Saved {len(gdf)} cars with confidence to {output_path}")
|
|
2228
|
+
|
|
2229
|
+
return gdf
|
|
2230
|
+
else:
|
|
2231
|
+
print("No valid car polygons found")
|
|
2232
|
+
return None
|
|
2233
|
+
|
|
1880
2234
|
|
|
1881
2235
|
class ShipDetector(ObjectDetector):
|
|
1882
2236
|
"""
|
geoai/utils.py
CHANGED
|
@@ -54,6 +54,7 @@ def view_raster(
|
|
|
54
54
|
array_args: Optional[Dict] = {},
|
|
55
55
|
client_args: Optional[Dict] = {"cors_all": False},
|
|
56
56
|
basemap: Optional[str] = "OpenStreetMap",
|
|
57
|
+
backend: Optional[str] = "folium",
|
|
57
58
|
**kwargs,
|
|
58
59
|
):
|
|
59
60
|
"""
|
|
@@ -81,28 +82,52 @@ def view_raster(
|
|
|
81
82
|
leafmap.Map: The map object with the raster layer added.
|
|
82
83
|
"""
|
|
83
84
|
|
|
85
|
+
if backend == "folium":
|
|
86
|
+
import leafmap.foliumap as leafmap
|
|
87
|
+
|
|
84
88
|
m = leafmap.Map(basemap=basemap)
|
|
85
89
|
|
|
86
90
|
if isinstance(source, dict):
|
|
87
91
|
source = dict_to_image(source)
|
|
88
92
|
|
|
89
|
-
|
|
90
|
-
source
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
93
|
+
if (
|
|
94
|
+
isinstance(source, str)
|
|
95
|
+
and source.lower().endswith(".tif")
|
|
96
|
+
and source.startswith("http")
|
|
97
|
+
):
|
|
98
|
+
if indexes is not None:
|
|
99
|
+
kwargs["bidx"] = indexes
|
|
100
|
+
if colormap is not None:
|
|
101
|
+
kwargs["colormap_name"] = colormap
|
|
102
|
+
if attribution is None:
|
|
103
|
+
attribution = "TiTiler"
|
|
104
|
+
|
|
105
|
+
m.add_cog_layer(
|
|
106
|
+
source,
|
|
107
|
+
name=layer_name,
|
|
108
|
+
opacity=opacity,
|
|
109
|
+
attribution=attribution,
|
|
110
|
+
zoom_to_layer=zoom_to_layer,
|
|
111
|
+
**kwargs,
|
|
112
|
+
)
|
|
113
|
+
else:
|
|
114
|
+
m.add_raster(
|
|
115
|
+
source=source,
|
|
116
|
+
indexes=indexes,
|
|
117
|
+
colormap=colormap,
|
|
118
|
+
vmin=vmin,
|
|
119
|
+
vmax=vmax,
|
|
120
|
+
nodata=nodata,
|
|
121
|
+
attribution=attribution,
|
|
122
|
+
layer_name=layer_name,
|
|
123
|
+
layer_index=layer_index,
|
|
124
|
+
zoom_to_layer=zoom_to_layer,
|
|
125
|
+
visible=visible,
|
|
126
|
+
opacity=opacity,
|
|
127
|
+
array_args=array_args,
|
|
128
|
+
client_args=client_args,
|
|
129
|
+
**kwargs,
|
|
130
|
+
)
|
|
106
131
|
return m
|
|
107
132
|
|
|
108
133
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
geoai/__init__.py,sha256=cZwfZotU5b9Qc6MAxNA35RT3NtFtncGMM10SCvNzwgI,923
|
|
2
|
+
geoai/download.py,sha256=4GiDmLrp2wKslgfm507WeZrwOdYcMekgQXxWGbl5cBw,13094
|
|
3
|
+
geoai/extract.py,sha256=cATZ7J9PEFIOc96DCDnHcj-AjKI3MYyMxU1CWfhouAI,90745
|
|
4
|
+
geoai/geoai.py,sha256=7sbZ2LCZXaO0Io4Y7UH5tcQMFZH-sjYu_NENRKfyL5o,64
|
|
5
|
+
geoai/preprocess.py,sha256=ddUZUZ2fLNjzSIpXby7-MszM16GZt_gE9BMX4jdUZMw,119217
|
|
6
|
+
geoai/segmentation.py,sha256=Vcymnhwl_xikt4v9x8CYJq_vId9R1gB7-YzLfwg-F9M,11372
|
|
7
|
+
geoai/utils.py,sha256=5uFdWBgfw2_MTKsBObBXq3-nyY5TEQYPMJh4ap4eEag,191638
|
|
8
|
+
geoai_py-0.3.4.dist-info/LICENSE,sha256=vN2L5U7cZ6ZkOHFmc8WiGlsogWsZc5dllMeNxnKVOZg,1070
|
|
9
|
+
geoai_py-0.3.4.dist-info/METADATA,sha256=LU3GKiKaCcqQ_yBCgvYutzWO3EBgpvXtBJw4InficrU,6059
|
|
10
|
+
geoai_py-0.3.4.dist-info/WHEEL,sha256=rF4EZyR2XVS6irmOHQIJx2SUqXLZKRMUrjsg8UwN-XQ,109
|
|
11
|
+
geoai_py-0.3.4.dist-info/entry_points.txt,sha256=uGp3Az3HURIsRHP9v-ys0hIbUuBBNUfXv6VbYHIXeg4,41
|
|
12
|
+
geoai_py-0.3.4.dist-info/top_level.txt,sha256=1YkCUWu-ii-0qIex7kbwAvfei-gos9ycyDyUCJPNWHY,6
|
|
13
|
+
geoai_py-0.3.4.dist-info/RECORD,,
|
geoai_py-0.3.3.dist-info/RECORD
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
geoai/__init__.py,sha256=WBRcLvq3h_J4SoGXJN2Nx99ENQkPAHq3PtGMW-eQrGw,923
|
|
2
|
-
geoai/download.py,sha256=4GiDmLrp2wKslgfm507WeZrwOdYcMekgQXxWGbl5cBw,13094
|
|
3
|
-
geoai/extract.py,sha256=hcGFKwEn5a82sLM0C_nzBdJjV2hauv51saILaTT_EN0,76249
|
|
4
|
-
geoai/geoai.py,sha256=7sbZ2LCZXaO0Io4Y7UH5tcQMFZH-sjYu_NENRKfyL5o,64
|
|
5
|
-
geoai/preprocess.py,sha256=ddUZUZ2fLNjzSIpXby7-MszM16GZt_gE9BMX4jdUZMw,119217
|
|
6
|
-
geoai/segmentation.py,sha256=Vcymnhwl_xikt4v9x8CYJq_vId9R1gB7-YzLfwg-F9M,11372
|
|
7
|
-
geoai/utils.py,sha256=f4uib-ZiJfQyJyVHvcZRWl0_HISISTkSiRTj2nJbS4I,190888
|
|
8
|
-
geoai_py-0.3.3.dist-info/LICENSE,sha256=vN2L5U7cZ6ZkOHFmc8WiGlsogWsZc5dllMeNxnKVOZg,1070
|
|
9
|
-
geoai_py-0.3.3.dist-info/METADATA,sha256=ihYj9AVDicaea_G_yqsiRZW3lblmgBQ4QO1Mh5DVDDk,6059
|
|
10
|
-
geoai_py-0.3.3.dist-info/WHEEL,sha256=rF4EZyR2XVS6irmOHQIJx2SUqXLZKRMUrjsg8UwN-XQ,109
|
|
11
|
-
geoai_py-0.3.3.dist-info/entry_points.txt,sha256=uGp3Az3HURIsRHP9v-ys0hIbUuBBNUfXv6VbYHIXeg4,41
|
|
12
|
-
geoai_py-0.3.3.dist-info/top_level.txt,sha256=1YkCUWu-ii-0qIex7kbwAvfei-gos9ycyDyUCJPNWHY,6
|
|
13
|
-
geoai_py-0.3.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|