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

Files changed (2) hide show
  1. requirements.txt +1 -0
  2. 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
- """Simple feature extraction for HF Spaces."""
9
  try:
10
  logging.info(f"Extracting {feature_type} from {image_path}")
11
 
12
  with rasterio.open(image_path) as src:
13
- # Simple NDVI calculation
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
- mask = ndvi > 0.2
 
 
 
 
 
 
21
  else:
22
  band = src.read(1)
23
- mask = band > np.percentile(band, 60)
 
 
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
- # Create simple features
 
 
 
 
 
 
 
 
 
 
 
36
  features = []
37
  height, width = mask.shape
38
- grid_size = max(10, min(height, width) // 50)
 
39
 
40
- feature_id = 0
41
- for y in range(0, height, grid_size):
42
- for x in range(0, width, grid_size):
43
- cell = mask[y:y+grid_size, x:x+grid_size]
44
- if np.sum(cell) > grid_size * grid_size * 0.3:
45
-
46
- x_ratio = x / width
47
- y_ratio = y / height
48
-
49
- lon1 = west + x_ratio * (east - west)
50
- lat1 = north - y_ratio * (north - south)
51
-
52
- x2_ratio = min((x + grid_size) / width, 1.0)
53
- y2_ratio = min((y + grid_size) / height, 1.0)
54
-
55
- lon2 = west + x2_ratio * (east - west)
56
- lat2 = north - y2_ratio * (north - south)
57
-
58
- polygon_coords = [
59
- [lon1, lat1], [lon2, lat1], [lon2, lat2], [lon1, lat2], [lon1, lat1]
60
- ]
61
-
62
- feature = {
63
- "type": "Feature",
64
- "id": feature_id,
65
- "properties": {
66
- "feature_type": feature_type,
67
- "confidence": 0.8
68
- },
69
- "geometry": {
70
- "type": "Polygon",
71
- "coordinates": [polygon_coords]
72
- }
 
 
 
 
 
 
 
 
 
 
 
 
73
  }
74
-
75
- features.append(feature)
76
- feature_id += 1
 
 
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",