DynamicPacific commited on
Commit
3c0c6f0
·
2 Parent(s): e383754 211f96a

Resolve merge conflicts and add multi-format image support

Browse files

- Fixed merge conflict in utils/advanced_extraction.py
- Added support for PNG, JPG, JPEG, BMP, GIF formats
- Updated app.py to handle both GeoTIFF and regular image formats
- GeoTIFF files use NDVI analysis, other formats use contour detection
- Updated UI labels and instructions for broader format support
- Maintained HF Spaces compatibility with lightweight dependencies

.gitattributes ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ *.tif filter=lfs diff=lfs merge=lfs -text
2
+ *.png filter=lfs diff=lfs merge=lfs -text
3
+ *.jpg filter=lfs diff=lfs merge=lfs -text
4
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
5
+ *.gif filter=lfs diff=lfs merge=lfs -text
6
+ *.bmp filter=lfs diff=lfs merge=lfs -text
7
+ *.ico filter=lfs diff=lfs merge=lfs -text
8
+ *.zip filter=lfs diff=lfs merge=lfs -text
9
+ *.tiff filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: ForestAI Tree Detection
3
+ emoji: 🌲
4
+ colorFrom: green
5
+ colorTo: yellow
6
+ sdk: gradio
7
+ sdk_version: 5.34.0
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ ---
12
+
13
+ # ForestAI - Tree Detection from Satellite Imagery
14
+
15
+ Upload a GeoTIFF file to detect and map trees using AI-powered imagery analysis. This application provides:
16
+
17
+ - 🌲 Automated tree detection from satellite imagery
18
+ - 🗺️ Interactive split-view map visualization
19
+ - 📊 Feature extraction and analysis
20
+ - 🎯 Multiple feature types (trees, buildings, water, roads)
21
+
22
+ ## How to Use
23
+
24
+ 1. Upload a GeoTIFF file
25
+ 2. Select feature type to detect
26
+ 3. Click "Analyze Image"
27
+ 4. Explore the interactive split-view map
28
+ 5. Use the slider to compare base map and satellite imagery
29
+
30
+ ## Technology
31
+
32
+ Built with:
33
+ - Gradio for the web interface
34
+ - GeoPandas and Rasterio for geospatial processing
35
+ - Folium for interactive mapping
36
+ - AI-powered feature extraction algorithms
37
+
38
+ ## Migration Notes
39
+
40
+ This version has been migrated and optimized from a local development version for Hugging Face Spaces deployment while preserving core functionality.
app.py CHANGED
@@ -1,147 +1,413 @@
1
  import os
2
- import logging
3
- import uuid
4
- from flask import Flask, render_template, request, jsonify, send_from_directory
 
 
 
5
  import json
6
- from werkzeug.utils import secure_filename
7
- from utils.image_processing import process_image
8
- from utils.geospatial import process_image_to_geojson
9
- from utils.advanced_extraction import extract_features_from_geotiff
10
-
11
- # Configure logging
12
- logging.basicConfig(level=logging.DEBUG)
13
-
14
- # Create Flask app
15
- app = Flask(__name__)
16
- app.secret_key = os.environ.get("SESSION_SECRET", "dev_secret_key")
17
- app.config['MAX_CONTENT_LENGTH'] = 30 * 1024 * 1024 # Limit uploads to 30MB
18
- app.config['UPLOAD_TIMEOUT'] = 120 # Increase upload timeout
19
-
20
- # Configure upload folder
21
- UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads')
22
- PROCESSED_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'processed')
23
- ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'tif', 'tiff'}
24
-
25
- # Create directories if they don't exist
26
- os.makedirs(UPLOAD_FOLDER, exist_ok=True)
27
- os.makedirs(PROCESSED_FOLDER, exist_ok=True)
28
-
29
- # Check allowed file extensions
30
- def allowed_file(filename):
31
- return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
32
-
33
- @app.route('/')
34
- def index():
35
- return render_template('index.html')
36
-
37
- @app.route('/upload', methods=['POST'])
38
- def upload_file():
39
- # Check if a file was uploaded
40
- if 'file' not in request.files:
41
- return jsonify({'error': 'No file part'}), 400
42
-
43
- file = request.files['file']
44
-
45
- # Check if a file was selected
46
- if file.filename == '':
47
- return jsonify({'error': 'No file selected'}), 400
48
-
49
- # Get feature type, default to buildings if not specified
50
- feature_type = request.form.get('feature_type', 'buildings')
51
- logging.info(f"Processing image for feature type: {feature_type}")
52
-
53
- # Check if the file is an allowed type
54
- if file and allowed_file(file.filename):
55
- # Generate a unique filename to prevent collisions
56
- original_filename = secure_filename(file.filename)
57
- file_extension = original_filename.rsplit('.', 1)[1].lower()
58
- unique_filename = f"{uuid.uuid4().hex}.{file_extension}"
59
-
60
- # Save the uploaded file
61
- file_path = os.path.join(UPLOAD_FOLDER, unique_filename)
62
- file.save(file_path)
63
-
64
- try:
65
- # Process the image
66
- processed_image_path = process_image(file_path, PROCESSED_FOLDER)
67
-
68
- # Log the original file path for debugging
69
- logging.info(f"Original file path: {file_path}")
70
-
71
- # Extract coordinates directly from the original file for debugging
72
- try:
73
- import rasterio
74
- from rasterio.warp import transform_bounds
75
-
76
- logging.info(f"Attempting to read coordinates directly from {file_path}")
77
- with rasterio.open(file_path) as src:
78
- if src.crs is not None:
79
- bounds = src.bounds
80
- logging.info(f"Raw bounds from rasterio: {bounds}")
81
- logging.info(f"CRS: {src.crs}")
82
-
83
- # Transform bounds to WGS84 (lat/lon) if needed
84
- if src.crs.to_epsg() != 4326:
85
- west, south, east, north = transform_bounds(
86
- src.crs, 'EPSG:4326',
87
- bounds.left, bounds.bottom, bounds.right, bounds.top
88
- )
89
- logging.info(f"Transformed bounds (WGS84): W:{west}, S:{south}, E:{east}, N:{north}")
90
- else:
91
- west, south, east, north = bounds
92
- logging.info(f"Bounds already in WGS84: W:{west}, S:{south}, E:{east}, N:{north}")
93
- else:
94
- logging.warning(f"No CRS found in the file {file_path}")
95
- except Exception as e:
96
- logging.error(f"Error extracting coordinates directly: {str(e)}")
97
-
98
- # Check if the file is a GeoTIFF for advanced processing
99
- is_geotiff = file_path.lower().endswith(('.tif', '.tiff'))
100
-
101
- if is_geotiff:
102
- # Use advanced extraction for GeoTIFF files
103
- logging.info(f"Using advanced extraction for GeoTIFF file with feature type: {feature_type}")
104
- geojson_data = extract_features_from_geotiff(file_path, PROCESSED_FOLDER, feature_type=feature_type)
105
  else:
106
- # Fall back to basic processing for non-GeoTIFF files
107
- logging.info(f"Using basic processing for non-GeoTIFF file with feature type: {feature_type}")
108
- geojson_data = process_image_to_geojson(processed_image_path, feature_type=feature_type, original_file_path=file_path)
109
-
110
- # Save GeoJSON to file
111
- geojson_filename = f"{uuid.uuid4().hex}.geojson"
112
- geojson_path = os.path.join(PROCESSED_FOLDER, geojson_filename)
113
-
114
- with open(geojson_path, 'w') as f:
115
- json.dump(geojson_data, f)
116
-
117
- return jsonify({
118
- 'success': True,
119
- 'filename': unique_filename,
120
- 'geojson_filename': geojson_filename,
121
- 'feature_type': feature_type,
122
- 'geojson': geojson_data
123
- })
124
-
125
- except Exception as e:
126
- logging.error(f"Error processing file: {str(e)}")
127
- return jsonify({'error': f'Error processing file: {str(e)}'}), 500
128
-
129
- return jsonify({'error': 'File type not allowed'}), 400
130
-
131
- @app.route('/download/<filename>')
132
- def download_file(filename):
133
- return send_from_directory(PROCESSED_FOLDER, filename, as_attachment=True)
134
-
135
- # Serve the processed GeoJSON data
136
- @app.route('/geojson/<filename>')
137
- def get_geojson(filename):
138
  try:
139
- with open(os.path.join(PROCESSED_FOLDER, filename), 'r') as f:
140
- geojson_data = json.load(f)
141
- return jsonify(geojson_data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  except Exception as e:
143
- logging.error(f"Error loading GeoJSON: {str(e)}")
144
- return jsonify({'error': 'Error loading GeoJSON'}), 500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
 
146
- if __name__ == '__main__':
147
- app.run(host='0.0.0.0', port=5000, debug=True)
 
 
 
1
  import os
2
+ import gradio as gr
3
+ import folium
4
+ from folium import plugins
5
+ import geopandas as gpd
6
+ import rasterio
7
+ from rasterio.warp import transform_bounds
8
  import json
9
+ import tempfile
10
+ import shutil
11
+ import uuid
12
+ import logging
13
+ import traceback
14
+ import numpy as np
15
+ from PIL import Image
16
+
17
+ # Configure logging for HF Spaces
18
+ logging.basicConfig(
19
+ level=logging.INFO,
20
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
21
+ handlers=[logging.StreamHandler()]
22
+ )
23
+ logger = logging.getLogger('forestai')
24
+
25
+ # ================================
26
+ # CONFIGURATIONS
27
+ # ================================
28
+
29
+ # Feature styles for trees only
30
+ FEATURE_STYLES = {
31
+ 'trees': {"color": "green", "fillColor": "yellow", "fillOpacity": 0.3, "weight": 2}
32
+ }
33
+
34
+ # Example file path
35
+ EXAMPLE_FILE_PATH = "example.tif"
36
+
37
+ # ================================
38
+ # TEMP DIRECTORY SETUP
39
+ # ================================
40
+
41
+ def setup_temp_dirs():
42
+ """Create temporary directories."""
43
+ temp_base = tempfile.mkdtemp(prefix="forestai_")
44
+ dirs = {
45
+ 'uploads': os.path.join(temp_base, 'uploads'),
46
+ 'processed': os.path.join(temp_base, 'processed'),
47
+ 'static': os.path.join(temp_base, 'static')
48
+ }
49
+
50
+ for dir_path in dirs.values():
51
+ os.makedirs(dir_path, exist_ok=True)
52
+
53
+ return dirs
54
+
55
+ # Global temp directories
56
+ TEMP_DIRS = setup_temp_dirs()
57
+
58
+ # ================================
59
+ # CORE FUNCTIONS
60
+ # ================================
61
+
62
+ def get_bounds_from_geotiff(geotiff_path):
63
+ """Extract bounds from GeoTIFF and convert to WGS84."""
64
+ try:
65
+ with rasterio.open(geotiff_path) as src:
66
+ bounds = src.bounds
67
+ if src.crs:
68
+ west, south, east, north = transform_bounds(
69
+ src.crs, 'EPSG:4326',
70
+ bounds.left, bounds.bottom, bounds.right, bounds.top
71
+ )
72
+ return west, south, east, north
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  else:
74
+ return -74.1, 40.6, -73.9, 40.8
75
+ except Exception as e:
76
+ logger.error(f"Error extracting bounds: {str(e)}")
77
+ return -74.1, 40.6, -73.9, 40.8
78
+
79
+ def create_split_view_map(geojson_data, bounds):
80
+ """Create split-view map with detected trees."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  try:
82
+ west, south, east, north = bounds
83
+ center = [(south + north) / 2, (west + east) / 2]
84
+
85
+ # Calculate zoom level
86
+ lat_diff = north - south
87
+ lon_diff = east - west
88
+ max_diff = max(lat_diff, lon_diff)
89
+
90
+ if max_diff < 0.01:
91
+ zoom = 16
92
+ elif max_diff < 0.05:
93
+ zoom = 14
94
+ elif max_diff < 0.1:
95
+ zoom = 12
96
+ else:
97
+ zoom = 10
98
+
99
+ # Create base map
100
+ m = folium.Map(location=center, zoom_start=zoom)
101
+
102
+ # Create tile layers
103
+ left_layer = folium.TileLayer(
104
+ tiles='OpenStreetMap',
105
+ name='OpenStreetMap',
106
+ overlay=False,
107
+ control=False
108
+ )
109
+
110
+ right_layer = folium.TileLayer(
111
+ tiles='https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
112
+ attr='Esri',
113
+ name='Satellite',
114
+ overlay=False,
115
+ control=False
116
+ )
117
+
118
+ left_layer.add_to(m)
119
+ right_layer.add_to(m)
120
+
121
+ # Add detected trees
122
+ if geojson_data and 'features' in geojson_data and geojson_data['features']:
123
+ style = FEATURE_STYLES['trees']
124
+
125
+ geojson_layer = folium.GeoJson(
126
+ geojson_data,
127
+ name='Detected Trees',
128
+ style_function=lambda x: style,
129
+ popup=folium.GeoJsonPopup(
130
+ fields=['confidence'] if 'confidence' in str(geojson_data) else [],
131
+ aliases=['Confidence:'] if 'confidence' in str(geojson_data) else [],
132
+ localize=True
133
+ )
134
+ )
135
+ geojson_layer.add_to(m)
136
+
137
+ # Add split view plugin
138
+ plugins.SideBySideLayers(
139
+ layer_left=left_layer,
140
+ layer_right=right_layer
141
+ ).add_to(m)
142
+
143
+ # Add layer control
144
+ folium.LayerControl().add_to(m)
145
+
146
+ # Fit bounds
147
+ m.fit_bounds([[south, west], [north, east]], padding=(20, 20))
148
+
149
+ return m
150
+
151
  except Exception as e:
152
+ logger.error(f"Error creating map: {str(e)}")
153
+ # Return basic map on error
154
+ m = folium.Map(location=[40.7, -74.0], zoom_start=10)
155
+ return m
156
+
157
+ def process_image_file(image_file):
158
+ """Process uploaded image file for tree detection."""
159
+ if image_file is None:
160
+ return None, "Please upload an image file or use the example file"
161
+
162
+ try:
163
+ # Create unique ID
164
+ unique_id = str(uuid.uuid4().hex)[:8]
165
+
166
+ # Handle file upload
167
+ if hasattr(image_file, 'name'):
168
+ filename = os.path.basename(image_file.name)
169
+ else:
170
+ filename = os.path.basename(image_file)
171
+
172
+ # Save uploaded file
173
+ image_path = os.path.join(TEMP_DIRS['uploads'], f"{unique_id}_{filename}")
174
+
175
+ if hasattr(image_file, 'read'):
176
+ file_content = image_file.read()
177
+ with open(image_path, "wb") as f:
178
+ f.write(file_content)
179
+ else:
180
+ shutil.copy(image_file, image_path)
181
+
182
+ logger.info(f"File saved to {image_path}")
183
+
184
+ # Check if it's a GeoTIFF file for advanced processing
185
+ if filename.lower().endswith(('.tif', '.tiff')):
186
+ # Use advanced extraction for GeoTIFF files
187
+ from utils.advanced_extraction import extract_features_from_geotiff
188
+
189
+ logger.info("Extracting tree features from GeoTIFF...")
190
+ geojson_data = extract_features_from_geotiff(image_path, TEMP_DIRS['processed'], "trees")
191
+ else:
192
+ # Use general image processing for other formats
193
+ from utils.geospatial import process_image_to_geojson
194
+ from utils.image_processing import process_image
195
+
196
+ logger.info("Processing regular image for tree detection...")
197
+ processed_image_path = process_image(image_path, TEMP_DIRS['processed'])
198
+ geojson_data = process_image_to_geojson(processed_image_path, feature_type="trees", original_file_path=image_path)
199
+
200
+ if not geojson_data or not geojson_data.get('features'):
201
+ return None, "No trees detected in the image"
202
+
203
+ # Get bounds and create map
204
+ if filename.lower().endswith(('.tif', '.tiff')):
205
+ bounds = get_bounds_from_geotiff(image_path)
206
+ else:
207
+ # For regular images, use default bounds or extract from metadata
208
+ bounds = get_bounds_from_geotiff(image_path) # This will use defaults for non-GeoTIFF files
209
+
210
+ map_obj = create_split_view_map(geojson_data, bounds)
211
+
212
+ if map_obj:
213
+ # Save map
214
+ html_path = os.path.join(TEMP_DIRS['static'], f"map_{unique_id}.html")
215
+ map_obj.save(html_path)
216
+
217
+ # Read HTML content
218
+ with open(html_path, 'r', encoding='utf-8') as f:
219
+ html_content = f.read()
220
+
221
+ # Create iframe
222
+ iframe_html = f'''
223
+ <div style="width:100%; height:600px; border:1px solid #ddd; border-radius:8px; overflow:hidden;">
224
+ <iframe srcdoc="{html_content.replace('"', '&quot;')}"
225
+ width="100%" height="600px" style="border:none;"></iframe>
226
+ </div>
227
+ '''
228
+
229
+ num_features = len(geojson_data['features'])
230
+ return iframe_html, f"✅ Detected {num_features} tree areas in {filename}"
231
+ else:
232
+ return None, "Failed to create map"
233
+
234
+ except Exception as e:
235
+ logger.error(f"Error processing file: {str(e)}")
236
+ return None, f"❌ Error: {str(e)}"
237
+
238
+ def load_example_file():
239
+ """Load the example.tif file and return it for processing."""
240
+ try:
241
+ if os.path.exists(EXAMPLE_FILE_PATH):
242
+ logger.info("Loading example file...")
243
+ return EXAMPLE_FILE_PATH
244
+ else:
245
+ logger.warning("Example file not found")
246
+ return None
247
+ except Exception as e:
248
+ logger.error(f"Error loading example file: {str(e)}")
249
+ return None
250
+
251
+ def process_example_file():
252
+ """Process the example file and return results."""
253
+ example_file = load_example_file()
254
+ if example_file:
255
+ return process_geotiff_file(example_file)
256
+ else:
257
+ return None, "❌ Example file (example.tif) not found in the root directory"
258
+
259
+ def check_example_file_exists():
260
+ """Check if example file exists and return appropriate message."""
261
+ if os.path.exists(EXAMPLE_FILE_PATH):
262
+ return f"✅ Example file found: {EXAMPLE_FILE_PATH}"
263
+ else:
264
+ return f"⚠️ Example file not found: {EXAMPLE_FILE_PATH}"
265
+
266
+ # ================================
267
+ # GRADIO INTERFACE
268
+ # ================================
269
+
270
+ def create_gradio_interface():
271
+ """Create the Gradio interface for tree detection."""
272
+
273
+ css = """
274
+ .gradio-container {
275
+ max-width: 100% !important;
276
+ width: 100% !important;
277
+ margin: 0 !important;
278
+ padding: 10px !important;
279
+ }
280
+ .map-container {
281
+ border-radius: 8px;
282
+ overflow: hidden;
283
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
284
+ width: 100% !important;
285
+ }
286
+ body {
287
+ margin: 0 !important;
288
+ padding: 0 !important;
289
+ }
290
+ .contain {
291
+ max-width: none !important;
292
+ padding: 0 !important;
293
+ }
294
+ .example-button {
295
+ background: linear-gradient(135deg, #28a745 0%, #20c997 100%) !important;
296
+ border: none !important;
297
+ color: white !important;
298
+ }
299
+ """
300
+
301
+ with gr.Blocks(title="🌲 ForestAI - Tree Detection", css=css, theme=gr.themes.Soft()) as app:
302
+
303
+ # Simple header
304
+ gr.HTML("""
305
+ <div style="text-align: center; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 10px; margin-bottom: 20px;">
306
+ <h1 style="color: white; margin: 0; font-size: 2.5em;">🌲 ForestAI</h1>
307
+ <p style="color: white; margin: 10px 0 0 0; font-size: 1.2em;">Tree Detection from Satellite & Aerial Imagery</p>
308
+ </div>
309
+ """)
310
+
311
+ with gr.Row():
312
+ with gr.Column(scale=1):
313
+ gr.Markdown("### Upload GeoTIFF File")
314
+
315
+ file_input = gr.File(
316
+ label="Select Image File",
317
+ file_types=[".tif", ".tiff", ".png", ".jpg", ".jpeg", ".bmp", ".gif"],
318
+ type="filepath"
319
+ )
320
+
321
+ with gr.Row():
322
+ analyze_btn = gr.Button(
323
+ "🔍 Detect Trees",
324
+ variant="primary",
325
+ size="lg",
326
+ scale=2
327
+ )
328
+
329
+ example_btn = gr.Button(
330
+ "📁 Use Example File",
331
+ variant="secondary",
332
+ size="lg",
333
+ scale=1,
334
+ elem_classes=["example-button"]
335
+ )
336
+
337
+ # Example file status
338
+ example_status = gr.Textbox(
339
+ label="Example File Status",
340
+ value=check_example_file_exists(),
341
+ interactive=False,
342
+ lines=1
343
+ )
344
+
345
+ gr.Markdown("### Status")
346
+ status_output = gr.Textbox(
347
+ label="Processing Status",
348
+ interactive=False,
349
+ placeholder="Upload a file and click 'Detect Trees' or use the example file...",
350
+ lines=3
351
+ )
352
+
353
+ with gr.Column(scale=2):
354
+ gr.Markdown("### Results Map")
355
+
356
+ map_output = gr.HTML(
357
+ value='''
358
+ <div style="width:100%; height:600px; border:1px solid #ddd; border-radius:8px;
359
+ display:flex; align-items:center; justify-content:center;
360
+ background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);">
361
+ <div style="text-align:center; color:#666;">
362
+ <h3>🌲 Upload an image file or use example to see detected trees</h3>
363
+ <p>Interactive map will appear here</p>
364
+ </div>
365
+ </div>
366
+ ''',
367
+ elem_classes=["map-container"]
368
+ )
369
+
370
+ # Event handlers
371
+ analyze_btn.click(
372
+ fn=process_image_file,
373
+ inputs=[file_input],
374
+ outputs=[map_output, status_output],
375
+ show_progress=True
376
+ )
377
+
378
+ example_btn.click(
379
+ fn=process_example_file,
380
+ inputs=[],
381
+ outputs=[map_output, status_output],
382
+ show_progress=True
383
+ )
384
+
385
+ # Simple instructions
386
+ gr.Markdown("""
387
+ ### How to Use:
388
+ 1. **Upload** an image file (GeoTIFF, PNG, JPG, etc.) OR click "Use Example File" to try with the included sample
389
+ 2. **Click** "Detect Trees" to analyze your uploaded image
390
+ 3. **Explore** the interactive map with detected tree areas
391
+ 4. **Use** the split-view slider to compare base map and satellite imagery
392
+
393
+ ### Supported Formats:
394
+ - **GeoTIFF (.tif, .tiff)**: Best for satellite imagery with geographic data
395
+ - **Regular Images (.png, .jpg, .jpeg, .bmp, .gif)**: For general image analysis
396
+ - **Processing**: GeoTIFF files use advanced NDVI analysis, other formats use general image processing
397
+
398
+ ### Map Controls:
399
+ - **Split View**: Drag the vertical slider to compare layers
400
+ - **Zoom**: Scroll to zoom in/out, drag to pan
401
+ - **Layers**: Use layer control to toggle trees on/off
402
+
403
+ ### Example File:
404
+ - The example file should be named `example.tif` and placed in the same directory as this application
405
+ - Click "Use Example File" to quickly test the tree detection without uploading your own file
406
+ """)
407
+
408
+ return app
409
 
410
+ if __name__ == "__main__":
411
+ logger.info("🌲 Starting ForestAI Tree Detection")
412
+ app = create_gradio_interface()
413
+ app.launch()
example.tif ADDED

Git LFS Details

  • SHA256: ba9abc7060c9c7faf5336e31dfae041304b63cfdb36709bc6b2ccc6cb33ee29a
  • Pointer size: 132 Bytes
  • Size of remote file: 4.12 MB
packages.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ gdal-bin
2
+ libgdal-dev
3
+ libproj-dev
4
+ libgeos-dev
5
+ libspatialindex-dev
6
+ libspatialite7
7
+ libsqlite3-mod-spatialite
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio>=4.0.0
2
+ folium>=0.14.0
3
+ geopandas>=0.14.0
4
+ rasterio>=1.3.0
5
+ numpy>=1.24.0
6
+ Pillow>=10.0.0
7
+ shapely>=2.0.0
8
+ pyproj>=3.6.0
9
+ fiona>=1.9.0
10
+ matplotlib>=3.7.0
11
+ pandas>=2.0.0
12
+ scipy>=1.11.0
utils/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (168 Bytes). View file
 
utils/__pycache__/advanced_extraction.cpython-310.pyc ADDED
Binary file (2.03 kB). View file
 
utils/__pycache__/geospatial.cpython-310.pyc ADDED
Binary file (11.9 kB). View file
 
utils/__pycache__/image_processing.cpython-310.pyc ADDED
Binary file (1.76 kB). View file
 
utils/__pycache__/segmentation.cpython-310.pyc ADDED
Binary file (5.81 kB). View file
 
utils/advanced_extraction.py CHANGED
@@ -1,230 +1,86 @@
1
- """
2
- Advanced feature extraction using geoai-py package.
3
- This module provides integration with the geoai-py package for more accurate
4
- feature extraction from geospatial imagery.
5
- """
6
-
7
  import os
8
  import logging
9
- import geoai
10
- import json
11
- from shapely.geometry import shape
12
-
13
- def extract_buildings_from_geotiff(image_path, output_folder, confidence_threshold=0.5, mask_threshold=0.5):
14
- """
15
- Extract building footprints from a GeoTIFF image using geoai-py.
16
-
17
- Args:
18
- image_path (str): Path to the input GeoTIFF image
19
- output_folder (str): Directory to save output files
20
- confidence_threshold (float): Confidence threshold for detection (0.0-1.0)
21
- mask_threshold (float): Mask threshold for segmentation (0.0-1.0)
22
 
23
- Returns:
24
- str: Path to the generated GeoJSON file
25
- """
26
  try:
27
- logging.info(f"Extracting buildings from {image_path} using geoai-py")
28
-
29
- # Initialize the building footprint extractor
30
- extractor = geoai.BuildingFootprintExtractor()
31
-
32
- # Generate a unique output path for the GeoJSON
33
- base_name = os.path.splitext(os.path.basename(image_path))[0]
34
- geojson_path = os.path.join(output_folder, f"{base_name}_buildings.geojson")
35
-
36
- # Process the raster to extract building footprints
37
- gdf = extractor.process_raster(
38
- image_path,
39
- output_path=geojson_path,
40
- batch_size=4,
41
- confidence_threshold=confidence_threshold,
42
- overlap=0.25,
43
- nms_iou_threshold=0.5,
44
- min_object_area=100,
45
- max_object_area=None,
46
- mask_threshold=mask_threshold,
47
- simplify_tolerance=1.0,
48
- )
49
-
50
- # Regularize the building footprints for more rectangular shapes
51
- gdf_regularized = extractor.regularize_buildings(
52
- gdf=gdf,
53
- min_area=100,
54
- angle_threshold=15,
55
- orthogonality_threshold=0.3,
56
- rectangularity_threshold=0.7,
57
- )
58
-
59
- # Ensure the GeoDataFrame is in WGS84 (EPSG:4326) for web mapping
60
- try:
61
- # Check if the GeoDataFrame has a CRS
62
- if gdf_regularized.crs is not None and gdf_regularized.crs != 'EPSG:4326':
63
- logging.info(f"Converting GeoDataFrame from {gdf_regularized.crs} to WGS84 (EPSG:4326)")
64
- # Reproject to WGS84
65
- gdf_regularized = gdf_regularized.to_crs('EPSG:4326')
66
- elif gdf_regularized.crs is None:
67
- # Try to get CRS from the original image
68
- import rasterio
69
- with rasterio.open(image_path) as src:
70
- if src.crs is not None:
71
- logging.info(f"Setting CRS from image: {src.crs}")
72
- gdf_regularized.crs = src.crs
73
- # Reproject to WGS84
74
- gdf_regularized = gdf_regularized.to_crs('EPSG:4326')
75
- except Exception as e:
76
- logging.warning(f"Error reprojecting to WGS84: {str(e)}")
77
-
78
- # Save the regularized buildings to GeoJSON
79
- regularized_geojson_path = os.path.join(output_folder, f"{base_name}_buildings_regularized.geojson")
80
- gdf_regularized.to_file(regularized_geojson_path, driver="GeoJSON")
81
-
82
- logging.info(f"Successfully extracted {len(gdf_regularized)} buildings")
83
-
84
- # Return the path to the regularized GeoJSON
85
- return regularized_geojson_path
86
-
87
- except Exception as e:
88
- logging.error(f"Error extracting buildings with geoai-py: {str(e)}")
89
- raise
90
-
91
- def extract_trees_from_geotiff(image_path, output_folder, confidence_threshold=0.5, mask_threshold=0.5):
92
- """
93
- Extract tree/vegetation cover from a GeoTIFF image.
94
- This is a placeholder for future implementation.
95
-
96
- Args:
97
- image_path (str): Path to the input GeoTIFF image
98
- output_folder (str): Directory to save output files
99
- confidence_threshold (float): Confidence threshold for detection (0.0-1.0)
100
- mask_threshold (float): Mask threshold for segmentation (0.0-1.0)
101
-
102
- Returns:
103
- str: Path to the generated GeoJSON file
104
- """
105
- # This would be implemented in the future
106
- # For now, we'll use our existing segmentation approach
107
- from utils.geospatial import process_image_to_geojson
108
- from utils.image_processing import process_image
109
-
110
- processed_image_path = process_image(image_path, output_folder)
111
- geojson_data = process_image_to_geojson(processed_image_path, feature_type="trees", original_file_path=image_path)
112
-
113
- # Save the GeoJSON to a file
114
- base_name = os.path.splitext(os.path.basename(image_path))[0]
115
- geojson_path = os.path.join(output_folder, f"{base_name}_trees.geojson")
116
-
117
- with open(geojson_path, 'w') as f:
118
- json.dump(geojson_data, f)
119
-
120
- return geojson_path
121
-
122
- def geojson_to_app_format(geojson_path):
123
- """
124
- Convert a GeoJSON file from geoai-py to the format expected by our application.
125
-
126
- Args:
127
- geojson_path (str): Path to the GeoJSON file
128
-
129
- Returns:
130
- dict: GeoJSON data in the format expected by our application
131
- """
132
- try:
133
- # Read the GeoJSON file
134
- with open(geojson_path, 'r') as f:
135
- geojson_data = json.load(f)
136
-
137
- # Log the GeoJSON data for debugging
138
- logging.info(f"GeoJSON data loaded from {geojson_path}")
139
- if geojson_data and 'features' in geojson_data and geojson_data['features']:
140
- first_feature = geojson_data['features'][0]
141
- if 'geometry' in first_feature and 'coordinates' in first_feature['geometry']:
142
- try:
143
- if first_feature['geometry']['type'] == 'Polygon':
144
- coords = first_feature['geometry']['coordinates'][0][0]
145
- else: # MultiPolygon
146
- coords = first_feature['geometry']['coordinates'][0][0][0]
147
- logging.info(f"First feature coordinates: {coords}")
148
- except Exception as e:
149
- logging.warning(f"Error extracting coordinates from first feature: {str(e)}")
150
-
151
- # Our application expects a specific format, so we'll convert if needed
152
- if 'features' not in geojson_data:
153
- # Create a new GeoJSON FeatureCollection
154
- converted_geojson = {
155
- "type": "FeatureCollection",
156
- "features": []
157
- }
158
-
159
- # Add each feature to the collection
160
- for i, feature in enumerate(geojson_data):
161
- converted_geojson["features"].append({
162
- "type": "Feature",
163
- "geometry": feature["geometry"],
164
- "properties": feature.get("properties", {"id": i})
165
- })
166
-
167
- logging.info(f"Converted GeoJSON to FeatureCollection with {len(converted_geojson['features'])} features")
168
- return converted_geojson
169
-
170
- # If it's already in the right format, return as is
171
- logging.info(f"GeoJSON already in FeatureCollection format with {len(geojson_data['features'])} features")
172
- return geojson_data
173
-
174
- except Exception as e:
175
- logging.error(f"Error converting GeoJSON format: {str(e)}")
176
- # Return an empty GeoJSON if there's an error
177
- return {"type": "FeatureCollection", "features": []}
178
-
179
- def extract_features_from_geotiff(image_path, output_folder, feature_type="buildings"):
180
- """
181
- Extract features from a GeoTIFF image based on the feature type.
182
-
183
- Args:
184
- image_path (str): Path to the input GeoTIFF image
185
- output_folder (str): Directory to save output files
186
- feature_type (str): Type of features to extract ("buildings", "trees", "water", "roads")
187
-
188
- Returns:
189
- dict: GeoJSON data in the format expected by our application
190
- """
191
- try:
192
- if feature_type.lower() == "buildings":
193
- # Use the advanced building extraction
194
- geojson_path = extract_buildings_from_geotiff(image_path, output_folder)
195
- elif feature_type.lower() == "trees" or feature_type.lower() == "vegetation":
196
- # Use the tree extraction (placeholder for now)
197
- geojson_path = extract_trees_from_geotiff(image_path, output_folder)
198
- else:
199
- # For other feature types, use our existing approach
200
- from utils.geospatial import process_image_to_geojson
201
- from utils.image_processing import process_image
202
-
203
- processed_image_path = process_image(image_path, output_folder)
204
- geojson_data = process_image_to_geojson(processed_image_path, feature_type=feature_type, original_file_path=image_path)
205
-
206
- # Save the GeoJSON to a file
207
- base_name = os.path.splitext(os.path.basename(image_path))[0]
208
- geojson_path = os.path.join(output_folder, f"{base_name}_{feature_type}.geojson")
209
-
210
- with open(geojson_path, 'w') as f:
211
- json.dump(geojson_data, f)
212
-
213
- # Add feature type to the GeoJSON data
214
- geojson_data['feature_type'] = feature_type
215
-
216
- # Return the data directly since it's already in our format
217
- return geojson_data
218
-
219
- # Convert the GeoJSON to our application format
220
- result = geojson_to_app_format(geojson_path)
221
-
222
- # Add feature type to the GeoJSON data
223
- result['feature_type'] = feature_type
224
-
225
- return result
226
-
227
  except Exception as e:
228
  logging.error(f"Error extracting features: {str(e)}")
229
- # Return an empty GeoJSON if there's an error
230
  return {"type": "FeatureCollection", "features": []}
 
 
 
 
 
 
 
1
  import os
2
  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
27
+ if src.crs:
28
+ west, south, east, north = transform_bounds(
29
+ src.crs, 'EPSG:4326',
30
+ bounds.left, bounds.bottom, bounds.right, bounds.top
31
+ )
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",
80
+ "features": features,
81
+ "feature_type": feature_type
82
+ }
83
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  except Exception as e:
85
  logging.error(f"Error extracting features: {str(e)}")
 
86
  return {"type": "FeatureCollection", "features": []}