Spaces:
Sleeping
Sleeping
DynamicPacific
commited on
Commit
·
67cd09b
1
Parent(s):
dbdc03c
Improve tree extraction with contour-based detection
Browse files- Replace grid-based approach with contour detection
- Use adaptive NDVI thresholding based on image statistics
- Add morphological operations to clean up tree masks
- Implement area-based filtering (0.1% to 10% of image)
- Add adaptive confidence scoring
- Include area and confidence metadata in features
- Add OpenCV dependency for contour processing
- Results in more accurate tree shapes and better area coverage
- requirements.txt +1 -0
- utils/advanced_extraction.py +79 -42
requirements.txt
CHANGED
|
@@ -10,3 +10,4 @@ fiona>=1.9.0
|
|
| 10 |
matplotlib>=3.7.0
|
| 11 |
pandas>=2.0.0
|
| 12 |
scipy>=1.11.0
|
|
|
|
|
|
| 10 |
matplotlib>=3.7.0
|
| 11 |
pandas>=2.0.0
|
| 12 |
scipy>=1.11.0
|
| 13 |
+
opencv-python>=4.8.0
|
utils/advanced_extraction.py
CHANGED
|
@@ -3,24 +3,35 @@ import logging
|
|
| 3 |
import numpy as np
|
| 4 |
import rasterio
|
| 5 |
from rasterio.warp import transform_bounds
|
|
|
|
|
|
|
| 6 |
|
| 7 |
def extract_features_from_geotiff(image_path, output_folder, feature_type="trees"):
|
| 8 |
-
"""
|
| 9 |
try:
|
| 10 |
logging.info(f"Extracting {feature_type} from {image_path}")
|
| 11 |
|
| 12 |
with rasterio.open(image_path) as src:
|
| 13 |
-
#
|
| 14 |
if src.count >= 3:
|
| 15 |
red = src.read(1).astype(float)
|
| 16 |
green = src.read(2).astype(float)
|
| 17 |
nir = src.read(4).astype(float) if src.count >= 4 else green
|
| 18 |
|
|
|
|
| 19 |
ndvi = np.divide(nir - red, nir + red + 1e-10)
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
else:
|
| 22 |
band = src.read(1)
|
| 23 |
-
|
|
|
|
|
|
|
| 24 |
|
| 25 |
# Get bounds
|
| 26 |
bounds = src.bounds
|
|
@@ -32,48 +43,74 @@ def extract_features_from_geotiff(image_path, output_folder, feature_type="trees
|
|
| 32 |
else:
|
| 33 |
west, south, east, north = -74.1, 40.6, -73.9, 40.8
|
| 34 |
|
| 35 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
features = []
|
| 37 |
height, width = mask.shape
|
| 38 |
-
|
|
|
|
| 39 |
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
}
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
|
|
|
|
|
|
| 77 |
|
| 78 |
return {
|
| 79 |
"type": "FeatureCollection",
|
|
|
|
| 3 |
import numpy as np
|
| 4 |
import rasterio
|
| 5 |
from rasterio.warp import transform_bounds
|
| 6 |
+
import cv2
|
| 7 |
+
from scipy import ndimage
|
| 8 |
|
| 9 |
def extract_features_from_geotiff(image_path, output_folder, feature_type="trees"):
|
| 10 |
+
"""Improved feature extraction with contour-based tree detection."""
|
| 11 |
try:
|
| 12 |
logging.info(f"Extracting {feature_type} from {image_path}")
|
| 13 |
|
| 14 |
with rasterio.open(image_path) as src:
|
| 15 |
+
# Enhanced NDVI calculation
|
| 16 |
if src.count >= 3:
|
| 17 |
red = src.read(1).astype(float)
|
| 18 |
green = src.read(2).astype(float)
|
| 19 |
nir = src.read(4).astype(float) if src.count >= 4 else green
|
| 20 |
|
| 21 |
+
# Improved NDVI calculation
|
| 22 |
ndvi = np.divide(nir - red, nir + red + 1e-10)
|
| 23 |
+
|
| 24 |
+
# Adaptive thresholding based on image statistics
|
| 25 |
+
ndvi_mean = np.mean(ndvi)
|
| 26 |
+
ndvi_std = np.std(ndvi)
|
| 27 |
+
threshold = max(0.3, ndvi_mean + 0.5 * ndvi_std) # More adaptive threshold
|
| 28 |
+
|
| 29 |
+
mask = ndvi > threshold
|
| 30 |
else:
|
| 31 |
band = src.read(1)
|
| 32 |
+
# Adaptive threshold for single-band images
|
| 33 |
+
threshold = np.percentile(band, 70) # Increased from 60
|
| 34 |
+
mask = band > threshold
|
| 35 |
|
| 36 |
# Get bounds
|
| 37 |
bounds = src.bounds
|
|
|
|
| 43 |
else:
|
| 44 |
west, south, east, north = -74.1, 40.6, -73.9, 40.8
|
| 45 |
|
| 46 |
+
# Convert to uint8 for OpenCV processing
|
| 47 |
+
mask_uint8 = (mask * 255).astype(np.uint8)
|
| 48 |
+
|
| 49 |
+
# Morphological operations to clean up the mask
|
| 50 |
+
kernel = np.ones((3, 3), np.uint8)
|
| 51 |
+
mask_cleaned = cv2.morphologyEx(mask_uint8, cv2.MORPH_CLOSE, kernel)
|
| 52 |
+
mask_cleaned = cv2.morphologyEx(mask_cleaned, cv2.MORPH_OPEN, kernel)
|
| 53 |
+
|
| 54 |
+
# Find contours instead of using grid
|
| 55 |
+
contours, _ = cv2.findContours(mask_cleaned, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
| 56 |
+
|
| 57 |
+
# Filter contours by area and create features
|
| 58 |
features = []
|
| 59 |
height, width = mask.shape
|
| 60 |
+
min_area = (height * width) * 0.001 # Minimum 0.1% of image area
|
| 61 |
+
max_area = (height * width) * 0.1 # Maximum 10% of image area
|
| 62 |
|
| 63 |
+
for i, contour in enumerate(contours):
|
| 64 |
+
area = cv2.contourArea(contour)
|
| 65 |
+
|
| 66 |
+
# Filter by area
|
| 67 |
+
if area < min_area or area > max_area:
|
| 68 |
+
continue
|
| 69 |
+
|
| 70 |
+
# Simplify contour for better polygon shape
|
| 71 |
+
epsilon = 0.02 * cv2.arcLength(contour, True)
|
| 72 |
+
approx = cv2.approxPolyDP(contour, epsilon, True)
|
| 73 |
+
|
| 74 |
+
# Convert contour points to geographic coordinates
|
| 75 |
+
polygon_coords = []
|
| 76 |
+
for point in approx:
|
| 77 |
+
x, y = point[0]
|
| 78 |
+
|
| 79 |
+
# Convert pixel coordinates to geographic coordinates
|
| 80 |
+
x_ratio = x / width
|
| 81 |
+
y_ratio = y / height
|
| 82 |
+
|
| 83 |
+
lon = west + x_ratio * (east - west)
|
| 84 |
+
lat = north - y_ratio * (north - south)
|
| 85 |
+
|
| 86 |
+
polygon_coords.append([lon, lat])
|
| 87 |
+
|
| 88 |
+
# Close the polygon
|
| 89 |
+
if len(polygon_coords) > 2:
|
| 90 |
+
polygon_coords.append(polygon_coords[0])
|
| 91 |
+
|
| 92 |
+
# Calculate confidence based on area and shape
|
| 93 |
+
area_ratio = area / (height * width)
|
| 94 |
+
confidence = min(0.95, 0.5 + area_ratio * 10) # Adaptive confidence
|
| 95 |
+
|
| 96 |
+
feature = {
|
| 97 |
+
"type": "Feature",
|
| 98 |
+
"id": i,
|
| 99 |
+
"properties": {
|
| 100 |
+
"feature_type": feature_type,
|
| 101 |
+
"confidence": round(confidence, 2),
|
| 102 |
+
"area_pixels": int(area),
|
| 103 |
+
"area_ratio": round(area_ratio, 4)
|
| 104 |
+
},
|
| 105 |
+
"geometry": {
|
| 106 |
+
"type": "Polygon",
|
| 107 |
+
"coordinates": [polygon_coords]
|
| 108 |
}
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
features.append(feature)
|
| 112 |
+
|
| 113 |
+
logging.info(f"Extracted {len(features)} tree features")
|
| 114 |
|
| 115 |
return {
|
| 116 |
"type": "FeatureCollection",
|