Yang Cao commited on
Commit
e383754
·
1 Parent(s): b8778f9

with geoai for building extraction

Browse files
.gitignore ADDED
File without changes
app.py CHANGED
@@ -6,6 +6,7 @@ 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
 
10
  # Configure logging
11
  logging.basicConfig(level=logging.DEBUG)
@@ -38,42 +39,81 @@ def upload_file():
38
  # Check if a file was uploaded
39
  if 'file' not in request.files:
40
  return jsonify({'error': 'No file part'}), 400
41
-
42
  file = request.files['file']
43
-
44
  # Check if a file was selected
45
  if file.filename == '':
46
  return jsonify({'error': 'No file selected'}), 400
47
-
48
  # Get feature type, default to buildings if not specified
49
  feature_type = request.form.get('feature_type', 'buildings')
50
  logging.info(f"Processing image for feature type: {feature_type}")
51
-
52
  # Check if the file is an allowed type
53
  if file and allowed_file(file.filename):
54
  # Generate a unique filename to prevent collisions
55
  original_filename = secure_filename(file.filename)
56
  file_extension = original_filename.rsplit('.', 1)[1].lower()
57
  unique_filename = f"{uuid.uuid4().hex}.{file_extension}"
58
-
59
  # Save the uploaded file
60
  file_path = os.path.join(UPLOAD_FOLDER, unique_filename)
61
  file.save(file_path)
62
-
63
  try:
64
  # Process the image
65
  processed_image_path = process_image(file_path, PROCESSED_FOLDER)
66
-
67
- # Convert processed image to GeoJSON using improved processing with feature type
68
- geojson_data = process_image_to_geojson(processed_image_path, feature_type=feature_type)
69
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  # Save GeoJSON to file
71
  geojson_filename = f"{uuid.uuid4().hex}.geojson"
72
  geojson_path = os.path.join(PROCESSED_FOLDER, geojson_filename)
73
-
74
  with open(geojson_path, 'w') as f:
75
  json.dump(geojson_data, f)
76
-
77
  return jsonify({
78
  'success': True,
79
  'filename': unique_filename,
@@ -81,11 +121,11 @@ def upload_file():
81
  'feature_type': feature_type,
82
  'geojson': geojson_data
83
  })
84
-
85
  except Exception as e:
86
  logging.error(f"Error processing file: {str(e)}")
87
  return jsonify({'error': f'Error processing file: {str(e)}'}), 500
88
-
89
  return jsonify({'error': 'File type not allowed'}), 400
90
 
91
  @app.route('/download/<filename>')
 
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)
 
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,
 
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>')
static/js/map.js CHANGED
@@ -8,27 +8,31 @@ let map = null;
8
  let currentFeatureType = 'buildings';
9
 
10
  // Initialize the map with default settings
11
- function initMap() {
12
  // If map already exists, remove it and create a new one
13
  if (map !== null) {
14
  map.remove();
15
  }
16
 
17
- // Default to Rio de Janeiro, Brazil (location of our sample data)
18
- // This helps users see where the extracted features should appear
19
- map = L.map('map').setView([-22.96, -43.38], 13);
20
-
21
- // Attempt to detect Brazil imagery based on coordinates in the URL
22
- if (window.location.search.includes('region=brazil')) {
23
- map.setView([-22.96, -43.38], 13);
 
24
  }
25
 
 
 
 
26
  // Define tile layers
27
  const osmLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
28
  attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
29
  maxZoom: 19
30
  });
31
-
32
  const satelliteLayer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
33
  attribution: 'Imagery &copy; Esri',
34
  maxZoom: 19
@@ -36,13 +40,13 @@ function initMap() {
36
 
37
  // Add OpenStreetMap layer by default
38
  osmLayer.addTo(map);
39
-
40
  // Add layer control
41
  const baseLayers = {
42
  "OpenStreetMap": osmLayer,
43
  "Satellite": satelliteLayer
44
  };
45
-
46
  L.control.layers(baseLayers, null, {position: 'topright'}).addTo(map);
47
 
48
  // Add a scale control
@@ -53,30 +57,37 @@ function initMap() {
53
 
54
  // Display GeoJSON data on the map
55
  function displayGeoJSON(geojsonData) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  if (!map) {
57
- initMap();
58
  }
59
 
60
- // Check if this appears to be Brazil data
61
- let isBrazilData = false;
62
  if (geojsonData && geojsonData.features && geojsonData.features.length > 0) {
63
- // Check the first feature's coordinates - if they're near Rio de Janeiro
64
- const firstFeature = geojsonData.features[0];
65
- if (firstFeature.geometry && firstFeature.geometry.coordinates) {
66
- const coords = firstFeature.geometry.coordinates[0][0];
67
- if (coords) {
68
- const [lon, lat] = coords;
69
- // Check if coordinates are in Brazil (roughly)
70
- if (lat < -20 && lat > -25 && lon < -40 && lon > -45) {
71
- isBrazilData = true;
72
- console.log("Detected Brazil coordinates in data");
73
- // Also switch to the satellite view for better context
74
- document.querySelectorAll('.leaflet-control-layers-base input')[1].click();
75
- }
76
- }
77
  }
78
  }
79
-
80
  // Update feature type if available in the data
81
  if (geojsonData && geojsonData.feature_type) {
82
  currentFeatureType = geojsonData.feature_type;
@@ -145,7 +156,7 @@ function displayGeoJSON(geojsonData) {
145
  opacity: 1,
146
  fillOpacity: 0.8
147
  };
148
-
149
  // Set color based on feature type
150
  switch(currentFeatureType) {
151
  case 'buildings':
@@ -163,14 +174,14 @@ function displayGeoJSON(geojsonData) {
163
  default:
164
  pointStyle.fillColor = getRandomColor();
165
  }
166
-
167
  return L.circleMarker(latlng, pointStyle);
168
  },
169
  onEachFeature: function(feature, layer) {
170
  // Add popups to show feature properties
171
  if (feature.properties) {
172
  let popupContent = '<div class="feature-popup">';
173
-
174
  // Set title based on feature type
175
  let title = 'Feature';
176
  switch(currentFeatureType) {
@@ -187,15 +198,15 @@ function displayGeoJSON(geojsonData) {
187
  title = 'Road';
188
  break;
189
  }
190
-
191
  popupContent += `<h5>${title} Properties</h5>`;
192
-
193
  for (const [key, value] of Object.entries(feature.properties)) {
194
  popupContent += `<strong>${key}:</strong> ${value}<br>`;
195
  }
196
-
197
  popupContent += '</div>';
198
-
199
  layer.bindPopup(popupContent);
200
  }
201
  }
@@ -203,7 +214,11 @@ function displayGeoJSON(geojsonData) {
203
 
204
  // Zoom to fit the GeoJSON data bounds
205
  if (geojsonLayer.getBounds().isValid()) {
206
- map.fitBounds(geojsonLayer.getBounds());
 
 
 
 
207
  }
208
  }
209
 
@@ -221,6 +236,72 @@ function formatGeoJSON(geojson) {
221
  return JSON.stringify(geojson, null, 2);
222
  }
223
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  // Initialize map when the DOM is loaded
225
  document.addEventListener('DOMContentLoaded', function() {
226
  // The map will be initialized when results are available
 
8
  let currentFeatureType = 'buildings';
9
 
10
  // Initialize the map with default settings
11
+ function initMap(initialCoords) {
12
  // If map already exists, remove it and create a new one
13
  if (map !== null) {
14
  map.remove();
15
  }
16
 
17
+ // Default center coordinates (will be overridden by GeoJSON data)
18
+ let center = [0, 0];
19
+ let zoom = 2;
20
+
21
+ // If coordinates are provided, use them
22
+ if (initialCoords && initialCoords.lat !== undefined && initialCoords.lng !== undefined) {
23
+ center = [initialCoords.lat, initialCoords.lng];
24
+ zoom = initialCoords.zoom || 13;
25
  }
26
 
27
+ // Initialize the map with the center coordinates
28
+ map = L.map('map').setView(center, zoom);
29
+
30
  // Define tile layers
31
  const osmLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
32
  attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
33
  maxZoom: 19
34
  });
35
+
36
  const satelliteLayer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
37
  attribution: 'Imagery &copy; Esri',
38
  maxZoom: 19
 
40
 
41
  // Add OpenStreetMap layer by default
42
  osmLayer.addTo(map);
43
+
44
  // Add layer control
45
  const baseLayers = {
46
  "OpenStreetMap": osmLayer,
47
  "Satellite": satelliteLayer
48
  };
49
+
50
  L.control.layers(baseLayers, null, {position: 'topright'}).addTo(map);
51
 
52
  // Add a scale control
 
57
 
58
  // Display GeoJSON data on the map
59
  function displayGeoJSON(geojsonData) {
60
+ // Log the GeoJSON data for debugging
61
+ console.log('GeoJSON data received:', geojsonData);
62
+
63
+ if (geojsonData && geojsonData.features && geojsonData.features.length > 0) {
64
+ console.log('First feature:', geojsonData.features[0]);
65
+ if (geojsonData.features[0].geometry && geojsonData.features[0].geometry.coordinates) {
66
+ console.log('First feature coordinates:',
67
+ geojsonData.features[0].geometry.type === 'Polygon' ?
68
+ geojsonData.features[0].geometry.coordinates[0][0] :
69
+ geojsonData.features[0].geometry.coordinates[0][0][0]);
70
+ }
71
+ }
72
+
73
+ // Calculate center coordinates from GeoJSON data
74
+ let initialCoords = calculateCenterFromGeoJSON(geojsonData);
75
+ console.log('Calculated center coordinates:', initialCoords);
76
+
77
  if (!map) {
78
+ initMap(initialCoords);
79
  }
80
 
81
+ // Switch to satellite view for better context when viewing features
 
82
  if (geojsonData && geojsonData.features && geojsonData.features.length > 0) {
83
+ // Switch to satellite view for better visualization
84
+ try {
85
+ document.querySelectorAll('.leaflet-control-layers-base input')[1].click();
86
+ } catch (e) {
87
+ console.warn('Could not switch to satellite view:', e);
 
 
 
 
 
 
 
 
 
88
  }
89
  }
90
+
91
  // Update feature type if available in the data
92
  if (geojsonData && geojsonData.feature_type) {
93
  currentFeatureType = geojsonData.feature_type;
 
156
  opacity: 1,
157
  fillOpacity: 0.8
158
  };
159
+
160
  // Set color based on feature type
161
  switch(currentFeatureType) {
162
  case 'buildings':
 
174
  default:
175
  pointStyle.fillColor = getRandomColor();
176
  }
177
+
178
  return L.circleMarker(latlng, pointStyle);
179
  },
180
  onEachFeature: function(feature, layer) {
181
  // Add popups to show feature properties
182
  if (feature.properties) {
183
  let popupContent = '<div class="feature-popup">';
184
+
185
  // Set title based on feature type
186
  let title = 'Feature';
187
  switch(currentFeatureType) {
 
198
  title = 'Road';
199
  break;
200
  }
201
+
202
  popupContent += `<h5>${title} Properties</h5>`;
203
+
204
  for (const [key, value] of Object.entries(feature.properties)) {
205
  popupContent += `<strong>${key}:</strong> ${value}<br>`;
206
  }
207
+
208
  popupContent += '</div>';
209
+
210
  layer.bindPopup(popupContent);
211
  }
212
  }
 
214
 
215
  // Zoom to fit the GeoJSON data bounds
216
  if (geojsonLayer.getBounds().isValid()) {
217
+ const bounds = geojsonLayer.getBounds();
218
+ console.log('GeoJSON bounds:', bounds);
219
+ map.fitBounds(bounds);
220
+ } else {
221
+ console.warn('GeoJSON bounds not valid');
222
  }
223
  }
224
 
 
236
  return JSON.stringify(geojson, null, 2);
237
  }
238
 
239
+ // Calculate center coordinates from GeoJSON data
240
+ function calculateCenterFromGeoJSON(geojsonData) {
241
+ if (!geojsonData || !geojsonData.features || geojsonData.features.length === 0) {
242
+ return { lat: 0, lng: 0, zoom: 2 }; // Default to world view
243
+ }
244
+
245
+ try {
246
+ // Create a temporary GeoJSON layer to calculate bounds
247
+ const tempLayer = L.geoJSON(geojsonData);
248
+ const bounds = tempLayer.getBounds();
249
+
250
+ if (bounds.isValid()) {
251
+ const center = bounds.getCenter();
252
+ // Calculate appropriate zoom level based on bounds size
253
+ const zoom = getBoundsZoomLevel(bounds);
254
+ return { lat: center.lat, lng: center.lng, zoom: zoom };
255
+ }
256
+ } catch (e) {
257
+ console.warn('Error calculating center from GeoJSON:', e);
258
+ }
259
+
260
+ // If we can't calculate from features, try to get center from the first feature
261
+ try {
262
+ const firstFeature = geojsonData.features[0];
263
+ if (firstFeature.geometry && firstFeature.geometry.coordinates) {
264
+ let coords;
265
+
266
+ // Handle different geometry types
267
+ if (firstFeature.geometry.type === 'Point') {
268
+ coords = firstFeature.geometry.coordinates;
269
+ return { lat: coords[1], lng: coords[0], zoom: 15 };
270
+ } else if (firstFeature.geometry.type === 'Polygon') {
271
+ coords = firstFeature.geometry.coordinates[0][0];
272
+ return { lat: coords[1], lng: coords[0], zoom: 13 };
273
+ } else if (firstFeature.geometry.type === 'MultiPolygon') {
274
+ coords = firstFeature.geometry.coordinates[0][0][0];
275
+ return { lat: coords[1], lng: coords[0], zoom: 13 };
276
+ }
277
+ }
278
+ } catch (e) {
279
+ console.warn('Error getting coordinates from first feature:', e);
280
+ }
281
+
282
+ // Default fallback
283
+ return { lat: 0, lng: 0, zoom: 2 };
284
+ }
285
+
286
+ // Calculate appropriate zoom level based on bounds size
287
+ function getBoundsZoomLevel(bounds) {
288
+ const WORLD_DIM = { height: 256, width: 256 };
289
+ const ZOOM_MAX = 18;
290
+
291
+ const ne = bounds.getNorthEast();
292
+ const sw = bounds.getSouthWest();
293
+
294
+ const latFraction = (ne.lat - sw.lat) / 180;
295
+ const lngFraction = (ne.lng - sw.lng) / 360;
296
+
297
+ const latZoom = Math.floor(Math.log(1 / latFraction) / Math.LN2);
298
+ const lngZoom = Math.floor(Math.log(1 / lngFraction) / Math.LN2);
299
+
300
+ const zoom = Math.min(latZoom, lngZoom, ZOOM_MAX);
301
+
302
+ return zoom > 0 ? zoom - 1 : 0; // Zoom out slightly for better context
303
+ }
304
+
305
  // Initialize map when the DOM is loaded
306
  document.addEventListener('DOMContentLoaded', function() {
307
  // The map will be initialized when results are available
static/js/upload.js CHANGED
@@ -19,33 +19,33 @@ const downloadBtn = document.getElementById('downloadBtn');
19
  // Handle form submission
20
  uploadForm.addEventListener('submit', function(event) {
21
  event.preventDefault();
22
-
23
  // Get the selected file
24
  const file = imageFileInput.files[0];
25
-
26
  // Check if a file was selected
27
  if (!file) {
28
  showError('Please select an image file to upload');
29
  return;
30
  }
31
-
32
  // Check file type
33
  const validImageTypes = ['image/png', 'image/jpeg', 'image/tiff', 'image/tif'];
34
  if (!validImageTypes.includes(file.type)) {
35
  showError('Please select a valid image file (PNG, JPG, or TIFF)');
36
  return;
37
  }
38
-
39
  // Show processing status and hide error message
40
  processingStatus.classList.remove('d-none');
41
  errorMessage.classList.add('d-none');
42
  resultsSection.classList.add('d-none');
43
-
44
  // Create FormData object for file upload
45
  const formData = new FormData();
46
  formData.append('file', file);
47
  formData.append('feature_type', featureTypeSelect.value);
48
-
49
  // Upload the file - add error handling for network issues
50
  fetch('/upload', {
51
  method: 'POST',
@@ -75,10 +75,10 @@ uploadForm.addEventListener('submit', function(event) {
75
  .then(data => {
76
  // Hide processing status
77
  processingStatus.classList.add('d-none');
78
-
79
  // Store the GeoJSON filename for download
80
  currentGeoJsonFilename = data.geojson_filename;
81
-
82
  // Display the results
83
  displayResults(data);
84
  })
@@ -93,12 +93,15 @@ uploadForm.addEventListener('submit', function(event) {
93
  function displayResults(data) {
94
  // Show the results section
95
  resultsSection.classList.remove('d-none');
96
-
 
 
 
97
  // Initialize the map if not already done
98
  if (!map) {
99
- initMap();
100
  }
101
-
102
  // Update the header to show the feature type
103
  const featureType = data.feature_type || 'buildings';
104
  const featureTypeName = {
@@ -107,19 +110,23 @@ function displayResults(data) {
107
  'water': 'Water Bodies',
108
  'roads': 'Roads'
109
  }[featureType] || 'Features';
110
-
111
  // Update the card header text
112
  const resultsHeader = document.querySelector('#resultsSection .card-header h3');
113
  if (resultsHeader) {
114
  resultsHeader.innerHTML = `<i class="fas fa-map"></i> ${featureTypeName} Extraction Results`;
115
  }
116
-
 
 
 
 
117
  // Display the GeoJSON on the map
118
- displayGeoJSON(data.geojson);
119
-
120
  // Format and display the GeoJSON in the text area
121
  geojsonDisplay.textContent = formatGeoJSON(data.geojson);
122
-
123
  // Scroll to the results section
124
  resultsSection.scrollIntoView({ behavior: 'smooth' });
125
  }
 
19
  // Handle form submission
20
  uploadForm.addEventListener('submit', function(event) {
21
  event.preventDefault();
22
+
23
  // Get the selected file
24
  const file = imageFileInput.files[0];
25
+
26
  // Check if a file was selected
27
  if (!file) {
28
  showError('Please select an image file to upload');
29
  return;
30
  }
31
+
32
  // Check file type
33
  const validImageTypes = ['image/png', 'image/jpeg', 'image/tiff', 'image/tif'];
34
  if (!validImageTypes.includes(file.type)) {
35
  showError('Please select a valid image file (PNG, JPG, or TIFF)');
36
  return;
37
  }
38
+
39
  // Show processing status and hide error message
40
  processingStatus.classList.remove('d-none');
41
  errorMessage.classList.add('d-none');
42
  resultsSection.classList.add('d-none');
43
+
44
  // Create FormData object for file upload
45
  const formData = new FormData();
46
  formData.append('file', file);
47
  formData.append('feature_type', featureTypeSelect.value);
48
+
49
  // Upload the file - add error handling for network issues
50
  fetch('/upload', {
51
  method: 'POST',
 
75
  .then(data => {
76
  // Hide processing status
77
  processingStatus.classList.add('d-none');
78
+
79
  // Store the GeoJSON filename for download
80
  currentGeoJsonFilename = data.geojson_filename;
81
+
82
  // Display the results
83
  displayResults(data);
84
  })
 
93
  function displayResults(data) {
94
  // Show the results section
95
  resultsSection.classList.remove('d-none');
96
+
97
+ // Calculate center coordinates from GeoJSON data
98
+ let initialCoords = calculateCenterFromGeoJSON(data.geojson);
99
+
100
  // Initialize the map if not already done
101
  if (!map) {
102
+ initMap(initialCoords);
103
  }
104
+
105
  // Update the header to show the feature type
106
  const featureType = data.feature_type || 'buildings';
107
  const featureTypeName = {
 
110
  'water': 'Water Bodies',
111
  'roads': 'Roads'
112
  }[featureType] || 'Features';
113
+
114
  // Update the card header text
115
  const resultsHeader = document.querySelector('#resultsSection .card-header h3');
116
  if (resultsHeader) {
117
  resultsHeader.innerHTML = `<i class="fas fa-map"></i> ${featureTypeName} Extraction Results`;
118
  }
119
+
120
+ // Add feature type to GeoJSON data for styling
121
+ const geojsonWithType = data.geojson;
122
+ geojsonWithType.feature_type = data.feature_type;
123
+
124
  // Display the GeoJSON on the map
125
+ displayGeoJSON(geojsonWithType);
126
+
127
  // Format and display the GeoJSON in the text area
128
  geojsonDisplay.textContent = formatGeoJSON(data.geojson);
129
+
130
  // Scroll to the results section
131
  resultsSection.scrollIntoView({ behavior: 'smooth' });
132
  }
utils/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (142 Bytes). View file
 
utils/__pycache__/advanced_extraction.cpython-312.pyc ADDED
Binary file (9.56 kB). View file
 
utils/__pycache__/geospatial.cpython-312.pyc ADDED
Binary file (18.8 kB). View file
 
utils/__pycache__/image_processing.cpython-312.pyc ADDED
Binary file (2.87 kB). View file
 
utils/__pycache__/segmentation.cpython-312.pyc ADDED
Binary file (8.92 kB). View file
 
utils/advanced_extraction.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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": []}
utils/geospatial.py CHANGED
@@ -19,12 +19,12 @@ def extract_contours(image_path, min_area=50, epsilon_factor=0.002):
19
  """
20
  Extract contours from an image and convert them to polygons.
21
  Uses OpenCV's contour detection with douglas-peucker simplification.
22
-
23
  Args:
24
  image_path (str): Path to the processed image
25
  min_area (int): Minimum contour area to keep
26
  epsilon_factor (float): Simplification factor for douglas-peucker algorithm
27
-
28
  Returns:
29
  list: List of polygon objects
30
  """
@@ -35,42 +35,42 @@ def extract_contours(image_path, min_area=50, epsilon_factor=0.002):
35
  # Try using PIL if OpenCV fails
36
  pil_img = Image.open(image_path).convert('L')
37
  img = np.array(pil_img)
38
-
39
  # Apply threshold if needed
40
  _, thresh = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
41
-
42
  # Find contours
43
  contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
44
-
45
  polygons = []
46
  for contour in contours:
47
  # Filter small contours
48
  area = cv2.contourArea(contour)
49
  if area < min_area:
50
  continue
51
-
52
  # Apply Douglas-Peucker algorithm to simplify contours
53
  epsilon = epsilon_factor * cv2.arcLength(contour, True)
54
  approx = cv2.approxPolyDP(contour, epsilon, True)
55
-
56
  # Convert to polygon
57
  if len(approx) >= 3: # At least 3 points needed for a polygon
58
  polygon_points = []
59
  for point in approx:
60
  x, y = point[0]
61
  polygon_points.append((float(x), float(y)))
62
-
63
  # Create a valid polygon (close it if needed)
64
  if polygon_points[0] != polygon_points[-1]:
65
  polygon_points.append(polygon_points[0])
66
-
67
  # Create shapely polygon
68
  polygon = Polygon(polygon_points)
69
  if polygon.is_valid:
70
  polygons.append(polygon)
71
-
72
  return polygons
73
-
74
  except Exception as e:
75
  logging.error(f"Error extracting contours: {str(e)}")
76
  return []
@@ -78,11 +78,11 @@ def extract_contours(image_path, min_area=50, epsilon_factor=0.002):
78
  def simplify_polygons(polygons, tolerance=1.0):
79
  """
80
  Apply polygon simplification to reduce the number of vertices.
81
-
82
  Args:
83
  polygons (list): List of shapely Polygon objects
84
  tolerance (float): Simplification tolerance
85
-
86
  Returns:
87
  list: List of simplified polygons
88
  """
@@ -92,16 +92,16 @@ def simplify_polygons(polygons, tolerance=1.0):
92
  simp = polygon.simplify(tolerance, preserve_topology=True)
93
  if simp.is_valid and not simp.is_empty:
94
  simplified.append(simp)
95
-
96
  return simplified
97
 
98
  def regularize_polygons(polygons):
99
  """
100
  Regularize polygons to make them more rectangular when appropriate.
101
-
102
  Args:
103
  polygons (list): List of shapely Polygon objects
104
-
105
  Returns:
106
  list: List of regularized polygons
107
  """
@@ -113,13 +113,13 @@ def regularize_polygons(polygons):
113
  width = bounds[2] - bounds[0]
114
  height = bounds[3] - bounds[1]
115
  area_ratio = polygon.area / (width * height)
116
-
117
  # If it's at least 80% similar to a rectangle, make it rectangular
118
  if area_ratio > 0.8:
119
  # Replace with the minimum bounding rectangle
120
  minx, miny, maxx, maxy = polygon.bounds
121
  regularized.append(Polygon([
122
- (minx, miny), (maxx, miny),
123
  (maxx, maxy), (minx, maxy), (minx, miny)
124
  ]))
125
  else:
@@ -127,29 +127,29 @@ def regularize_polygons(polygons):
127
  except Exception as e:
128
  logging.warning(f"Error regularizing polygon: {str(e)}")
129
  regularized.append(polygon)
130
-
131
  return regularized
132
 
133
  def merge_nearby_polygons(polygons, distance_threshold=5.0):
134
  """
135
  Merge polygons that are close to each other to reduce the polygon count.
136
-
137
  Args:
138
  polygons (list): List of shapely Polygon objects
139
  distance_threshold (float): Distance threshold for merging
140
-
141
  Returns:
142
  list: List of merged polygons
143
  """
144
  if not polygons:
145
  return []
146
-
147
  # Buffer polygons slightly to create overlaps for nearby polygons
148
  buffered = [polygon.buffer(distance_threshold) for polygon in polygons]
149
-
150
  # Union all buffered polygons
151
  union = ops.unary_union(buffered)
152
-
153
  # Convert the result to a list of polygons
154
  if isinstance(union, Polygon):
155
  return [union]
@@ -161,35 +161,59 @@ def merge_nearby_polygons(polygons, distance_threshold=5.0):
161
  def extract_geo_coordinates_from_image(image_path):
162
  """
163
  Extract geographic coordinates from image metadata (EXIF, GeoTIFF).
164
-
 
165
  Args:
166
  image_path (str): Path to the image file
167
-
168
  Returns:
169
  tuple: (min_lat, min_lon, max_lat, max_lon) or None if not found
170
  """
171
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  img = Image.open(image_path)
173
-
174
  # Check if it's a TIFF image with geospatial data
175
  if hasattr(img, 'tag') and img.tag:
176
  logging.info(f"Detected image with tags, checking for geospatial metadata")
177
-
178
  # Try to extract ModelPixelScaleTag (33550) and ModelTiepointTag (33922)
179
  pixel_scale_tag = None
180
  tiepoint_tag = None
181
-
182
  # Check for tags
183
  tag_dict = img.tag.items() if hasattr(img.tag, 'items') else {}
184
- # For the trees_brazil.tif specific case - fallback to direct inspection of tags
185
- # Check if this is our Brazil image using any clue in the filename
186
- brazil_indicators = ['brazil', 'trees_brazil', 'trees']
187
  is_brazil_image = False
188
- for indicator in brazil_indicators:
189
- if indicator.lower() in image_path.lower():
190
- is_brazil_image = True
191
- break
192
-
193
  if not tag_dict and is_brazil_image:
194
  logging.info(f"Special case for Brazil image detected in: {image_path}")
195
  # Hard code Brazil coordinates for the specific sample
@@ -201,90 +225,74 @@ def extract_geo_coordinates_from_image(image_path):
201
  max_lon = -43.36
202
  logging.info(f"Using known Brazil coordinates: {min_lon},{min_lat} to {max_lon},{max_lat}")
203
  return min_lat, min_lon, max_lat, max_lon
204
-
205
  for tag_id, value in tag_dict:
206
  tag_name = TiffTags.TAGS.get(tag_id, str(tag_id))
207
  logging.debug(f"TIFF tag: {tag_name} ({tag_id}): {value}")
208
-
209
  if tag_id == 33550: # ModelPixelScaleTag
210
  pixel_scale_tag = value
211
  elif tag_id == 33922: # ModelTiepointTag
212
  tiepoint_tag = value
213
-
214
  # Supplementary check for the log output we can see (raw detection)
215
  # Look for any GeoTIFF tag indicators in the output
216
  geotiff_indicators = ['ModelPixelScale', 'ModelTiepoint', 'GeoKey', 'GeoAscii']
217
  has_geotiff_indicators = False
218
-
219
  for indicator in geotiff_indicators:
220
  if indicator in str(img.tag):
221
  has_geotiff_indicators = True
222
  logging.info(f"Found GeoTIFF indicator: {indicator}")
223
  break
224
-
225
  # Look for any TIFF tag containing geographic info
226
  log_pattern = r"ModelPixelScaleTag.*?value: b'(.*?)'"
227
  log_matches = re.findall(log_pattern, str(img.tag))
228
-
229
  # If we detect any GeoTIFF indicators or raw tags, consider it a Brazil image
230
  if (log_matches or has_geotiff_indicators) and not pixel_scale_tag:
231
  logging.info(f"GeoTIFF indicators detected in image")
232
-
233
- # If Brazil indicators found in the filename, use Brazil coordinates
234
- if is_brazil_image or 'Brazil' in str(img.tag) or 'brazil' in str(img.tag):
235
- # More precise Rio de Janeiro coordinates
236
- min_lat = -22.980 # Southern Brazil (Rio de Janeiro)
237
- min_lon = -43.400
238
- max_lat = -22.920
239
- max_lon = -43.300
240
- logging.info(f"Using precise Rio de Janeiro, Brazil coordinates: {min_lon},{min_lat} to {max_lon},{max_lat}")
241
- return min_lat, min_lon, max_lat, max_lon
242
- else:
243
- # Try to extract values from raw tag data if possible
244
- try:
245
- # Parse the modelPixelScale if available
246
- if log_matches:
247
- logging.info(f"Found raw pixel scale data: {log_matches[0]}")
248
-
249
- # Fallback to Brazil coordinates for now - this is the sample data location
250
- min_lat = -22.980 # Southern Brazil (Rio de Janeiro)
251
- min_lon = -43.400
252
- max_lat = -22.920
253
- max_lon = -43.300
254
- logging.info(f"Using Brazil coordinates from detected GeoTIFF: {min_lon},{min_lat} to {max_lon},{max_lat}")
255
- return min_lat, min_lon, max_lat, max_lon
256
- except Exception as e:
257
- logging.error(f"Error parsing raw tag data: {str(e)}")
258
-
259
  if pixel_scale_tag and tiepoint_tag:
260
  # Extract pixel scale (x, y)
261
  x_scale = float(pixel_scale_tag[0])
262
  y_scale = float(pixel_scale_tag[1])
263
-
264
  # Extract model tiepoint (raster origin)
265
  i, j, k = float(tiepoint_tag[0]), float(tiepoint_tag[1]), float(tiepoint_tag[2])
266
  x, y, z = float(tiepoint_tag[3]), float(tiepoint_tag[4]), float(tiepoint_tag[5])
267
-
268
  # Calculate bounds based on image dimensions
269
  width, height = img.size
270
-
271
  # Calculate bounds
272
  min_lon = x
273
  max_lat = y
274
  max_lon = x + width * x_scale
275
  min_lat = y - height * y_scale
276
-
277
  logging.info(f"Extracted geo bounds: {min_lon},{min_lat} to {max_lon},{max_lat}")
278
  return min_lat, min_lon, max_lat, max_lon
279
-
280
  logging.info("No valid geospatial metadata found in TIFF")
281
-
282
  # Check for EXIF GPS data (typically in JPEG)
283
  elif hasattr(img, '_getexif') and img._getexif():
284
  exif = img._getexif()
285
  if exif and 34853 in exif: # 34853 is the GPS Info tag
286
  gps_info = exif[34853]
287
-
288
  # Extract GPS data
289
  if 1 in gps_info and 2 in gps_info and 3 in gps_info and 4 in gps_info:
290
  # Latitude
@@ -293,36 +301,38 @@ def extract_geo_coordinates_from_image(image_path):
293
  lat_val = lat[0][0]/lat[0][1] + lat[1][0]/(lat[1][1]*60) + lat[2][0]/(lat[2][1]*3600)
294
  if lat_ref == 'S':
295
  lat_val = -lat_val
296
-
297
  # Longitude
298
  lon_ref = gps_info[3] # 'E' or 'W'
299
  lon = gps_info[4]
300
  lon_val = lon[0][0]/lon[0][1] + lon[1][0]/(lon[1][1]*60) + lon[2][0]/(lon[2][1]*3600)
301
  if lon_ref == 'W':
302
  lon_val = -lon_val
303
-
304
  # Create a small region around the point
305
  delta = 0.01 # ~1km at the equator
306
  min_lat = lat_val - delta
307
  min_lon = lon_val - delta
308
  max_lat = lat_val + delta
309
  max_lon = lon_val + delta
310
-
311
  logging.info(f"Extracted EXIF GPS bounds: {min_lon},{min_lat} to {max_lon},{max_lat}")
312
  return min_lat, min_lon, max_lat, max_lon
313
-
314
  logging.info("No valid GPS metadata found in EXIF")
315
-
 
 
316
  return None
317
  except Exception as e:
318
  logging.error(f"Error extracting geo coordinates: {str(e)}")
319
  return None
320
 
321
- def convert_to_geojson_with_transform(polygons, image_height, image_width,
322
  min_lat=None, min_lon=None, max_lat=None, max_lon=None):
323
  """
324
  Convert polygons to GeoJSON with proper geographic transformation.
325
-
326
  Args:
327
  polygons (list): List of shapely Polygon objects
328
  image_height (int): Height of the source image
@@ -331,22 +341,23 @@ def convert_to_geojson_with_transform(polygons, image_height, image_width,
331
  min_lon (float, optional): Minimum longitude for geographic bounds
332
  max_lat (float, optional): Maximum latitude for geographic bounds
333
  max_lon (float, optional): Maximum longitude for geographic bounds
334
-
335
  Returns:
336
  dict: GeoJSON object
337
  """
338
  # Set default geographic bounds if not provided
339
  if None in (min_lon, min_lat, max_lon, max_lat):
 
340
  # Default to somewhere neutral (not in New York)
341
  min_lon, min_lat = -98.0, 32.0 # Central US
342
  max_lon, max_lat = -96.0, 34.0
343
-
344
  # Create a GeoJSON feature collection
345
  geojson = {
346
  "type": "FeatureCollection",
347
  "features": []
348
  }
349
-
350
  # Function to transform pixel coordinates to geographic coordinates
351
  def transform_point(x, y):
352
  # Linear interpolation
@@ -354,21 +365,21 @@ def convert_to_geojson_with_transform(polygons, image_height, image_width,
354
  # Invert y-axis for geographic coordinates
355
  lat = max_lat - (y / image_height) * (max_lat - min_lat)
356
  return lon, lat
357
-
358
  # Convert each polygon to a GeoJSON feature
359
  for i, polygon in enumerate(polygons):
360
  # Extract coordinates
361
  coords = list(polygon.exterior.coords)
362
-
363
  # Transform coordinates to geographic space
364
  geo_coords = [transform_point(x, y) for x, y in coords]
365
-
366
  # Create GeoJSON geometry
367
  geometry = {
368
  "type": "Polygon",
369
  "coordinates": [geo_coords]
370
  }
371
-
372
  # Create GeoJSON feature
373
  feature = {
374
  "type": "Feature",
@@ -378,19 +389,20 @@ def convert_to_geojson_with_transform(polygons, image_height, image_width,
378
  },
379
  "geometry": geometry
380
  }
381
-
382
  geojson["features"].append(feature)
383
-
384
  return geojson
385
 
386
- def process_image_to_geojson(image_path, feature_type="buildings"):
387
  """
388
  Complete pipeline to convert an image to a simplified GeoJSON.
389
-
390
  Args:
391
  image_path (str): Path to the processed image
392
  feature_type (str): Type of features to extract ("buildings", "trees", "water", "roads")
393
-
 
394
  Returns:
395
  dict: GeoJSON object
396
  """
@@ -398,27 +410,29 @@ def process_image_to_geojson(image_path, feature_type="buildings"):
398
  # Open image to get dimensions
399
  img = Image.open(image_path)
400
  width, height = img.size
401
-
402
  # Import segmentation module here to avoid circular imports
403
  from utils.segmentation import segment_and_extract_features
404
-
405
  # Extract features using advanced segmentation
406
  _, polygons = segment_and_extract_features(
407
- image_path,
408
  output_mask_path=None,
409
  feature_type=feature_type,
410
- min_area=50,
411
  simplify_tolerance=2.0,
412
  merge_distance=5.0
413
  )
414
-
415
  if not polygons:
416
  logging.warning("No polygons found in the image after segmentation")
417
  return {"type": "FeatureCollection", "features": []}
418
-
419
- # Try to extract coordinates from the original image
420
- original_image_path = None
421
- if "_processed" in image_path:
 
 
422
  original_image_path = image_path.replace("_processed", "")
423
  # Try the original image path but replace the extension with common formats
424
  if not os.path.exists(original_image_path):
@@ -427,34 +441,62 @@ def process_image_to_geojson(image_path, feature_type="buildings"):
427
  if os.path.exists(base_path + ext):
428
  original_image_path = base_path + ext
429
  break
430
-
 
 
431
  # Extract bounds from image if possible
432
  coords = None
433
  if original_image_path and os.path.exists(original_image_path):
434
  logging.info(f"Checking original image for geospatial data: {original_image_path}")
435
  coords = extract_geo_coordinates_from_image(original_image_path)
436
-
437
  if not coords:
438
  logging.info("Checking processed image for geospatial data")
439
  coords = extract_geo_coordinates_from_image(image_path)
440
-
441
  # Use extracted coordinates or defaults
442
  if coords:
443
  min_lat, min_lon, max_lat, max_lon = coords
 
444
  else:
445
- logging.info("No coordinates found in image, using default location in Central US")
446
- min_lat, min_lon = 32.0, -98.0 # Central US
447
- max_lat, max_lon = 34.0, -96.0
448
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
  # Convert to GeoJSON with proper transformation
450
  geojson = convert_to_geojson_with_transform(
451
  polygons, height, width,
452
  min_lat=min_lat, min_lon=min_lon,
453
  max_lat=max_lat, max_lon=max_lon
454
  )
455
-
456
  return geojson
457
-
458
  except Exception as e:
459
  logging.error(f"Error in GeoJSON processing: {str(e)}")
460
  return {"type": "FeatureCollection", "features": []}
 
19
  """
20
  Extract contours from an image and convert them to polygons.
21
  Uses OpenCV's contour detection with douglas-peucker simplification.
22
+
23
  Args:
24
  image_path (str): Path to the processed image
25
  min_area (int): Minimum contour area to keep
26
  epsilon_factor (float): Simplification factor for douglas-peucker algorithm
27
+
28
  Returns:
29
  list: List of polygon objects
30
  """
 
35
  # Try using PIL if OpenCV fails
36
  pil_img = Image.open(image_path).convert('L')
37
  img = np.array(pil_img)
38
+
39
  # Apply threshold if needed
40
  _, thresh = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
41
+
42
  # Find contours
43
  contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
44
+
45
  polygons = []
46
  for contour in contours:
47
  # Filter small contours
48
  area = cv2.contourArea(contour)
49
  if area < min_area:
50
  continue
51
+
52
  # Apply Douglas-Peucker algorithm to simplify contours
53
  epsilon = epsilon_factor * cv2.arcLength(contour, True)
54
  approx = cv2.approxPolyDP(contour, epsilon, True)
55
+
56
  # Convert to polygon
57
  if len(approx) >= 3: # At least 3 points needed for a polygon
58
  polygon_points = []
59
  for point in approx:
60
  x, y = point[0]
61
  polygon_points.append((float(x), float(y)))
62
+
63
  # Create a valid polygon (close it if needed)
64
  if polygon_points[0] != polygon_points[-1]:
65
  polygon_points.append(polygon_points[0])
66
+
67
  # Create shapely polygon
68
  polygon = Polygon(polygon_points)
69
  if polygon.is_valid:
70
  polygons.append(polygon)
71
+
72
  return polygons
73
+
74
  except Exception as e:
75
  logging.error(f"Error extracting contours: {str(e)}")
76
  return []
 
78
  def simplify_polygons(polygons, tolerance=1.0):
79
  """
80
  Apply polygon simplification to reduce the number of vertices.
81
+
82
  Args:
83
  polygons (list): List of shapely Polygon objects
84
  tolerance (float): Simplification tolerance
85
+
86
  Returns:
87
  list: List of simplified polygons
88
  """
 
92
  simp = polygon.simplify(tolerance, preserve_topology=True)
93
  if simp.is_valid and not simp.is_empty:
94
  simplified.append(simp)
95
+
96
  return simplified
97
 
98
  def regularize_polygons(polygons):
99
  """
100
  Regularize polygons to make them more rectangular when appropriate.
101
+
102
  Args:
103
  polygons (list): List of shapely Polygon objects
104
+
105
  Returns:
106
  list: List of regularized polygons
107
  """
 
113
  width = bounds[2] - bounds[0]
114
  height = bounds[3] - bounds[1]
115
  area_ratio = polygon.area / (width * height)
116
+
117
  # If it's at least 80% similar to a rectangle, make it rectangular
118
  if area_ratio > 0.8:
119
  # Replace with the minimum bounding rectangle
120
  minx, miny, maxx, maxy = polygon.bounds
121
  regularized.append(Polygon([
122
+ (minx, miny), (maxx, miny),
123
  (maxx, maxy), (minx, maxy), (minx, miny)
124
  ]))
125
  else:
 
127
  except Exception as e:
128
  logging.warning(f"Error regularizing polygon: {str(e)}")
129
  regularized.append(polygon)
130
+
131
  return regularized
132
 
133
  def merge_nearby_polygons(polygons, distance_threshold=5.0):
134
  """
135
  Merge polygons that are close to each other to reduce the polygon count.
136
+
137
  Args:
138
  polygons (list): List of shapely Polygon objects
139
  distance_threshold (float): Distance threshold for merging
140
+
141
  Returns:
142
  list: List of merged polygons
143
  """
144
  if not polygons:
145
  return []
146
+
147
  # Buffer polygons slightly to create overlaps for nearby polygons
148
  buffered = [polygon.buffer(distance_threshold) for polygon in polygons]
149
+
150
  # Union all buffered polygons
151
  union = ops.unary_union(buffered)
152
+
153
  # Convert the result to a list of polygons
154
  if isinstance(union, Polygon):
155
  return [union]
 
161
  def extract_geo_coordinates_from_image(image_path):
162
  """
163
  Extract geographic coordinates from image metadata (EXIF, GeoTIFF).
164
+ Uses rasterio for more reliable GeoTIFF handling.
165
+
166
  Args:
167
  image_path (str): Path to the image file
168
+
169
  Returns:
170
  tuple: (min_lat, min_lon, max_lat, max_lon) or None if not found
171
  """
172
  try:
173
+ # First try using rasterio for GeoTIFF files
174
+ if image_path.lower().endswith(('.tif', '.tiff')):
175
+ try:
176
+ import rasterio
177
+ from rasterio.warp import transform_bounds
178
+
179
+ logging.info(f"Using rasterio to extract coordinates from {image_path}")
180
+
181
+ with rasterio.open(image_path) as src:
182
+ # Check if the file has a valid CRS
183
+ if src.crs is not None:
184
+ # Get bounds in the source CRS
185
+ bounds = src.bounds
186
+
187
+ # Transform bounds to WGS84 (lat/lon)
188
+ if src.crs.to_epsg() != 4326:
189
+ west, south, east, north = transform_bounds(
190
+ src.crs, 'EPSG:4326',
191
+ bounds.left, bounds.bottom, bounds.right, bounds.top
192
+ )
193
+ else:
194
+ west, south, east, north = bounds
195
+
196
+ logging.info(f"Extracted coordinates from GeoTIFF: {west},{south} to {east},{north}")
197
+ return south, west, north, east # min_lat, min_lon, max_lat, max_lon
198
+ except Exception as e:
199
+ logging.warning(f"Rasterio extraction failed: {str(e)}, falling back to PIL")
200
+
201
+ # Fallback to PIL for other image types or if rasterio fails
202
  img = Image.open(image_path)
203
+
204
  # Check if it's a TIFF image with geospatial data
205
  if hasattr(img, 'tag') and img.tag:
206
  logging.info(f"Detected image with tags, checking for geospatial metadata")
207
+
208
  # Try to extract ModelPixelScaleTag (33550) and ModelTiepointTag (33922)
209
  pixel_scale_tag = None
210
  tiepoint_tag = None
211
+
212
  # Check for tags
213
  tag_dict = img.tag.items() if hasattr(img.tag, 'items') else {}
214
+ # Remove hardcoded Brazil detection
 
 
215
  is_brazil_image = False
216
+
 
 
 
 
217
  if not tag_dict and is_brazil_image:
218
  logging.info(f"Special case for Brazil image detected in: {image_path}")
219
  # Hard code Brazil coordinates for the specific sample
 
225
  max_lon = -43.36
226
  logging.info(f"Using known Brazil coordinates: {min_lon},{min_lat} to {max_lon},{max_lat}")
227
  return min_lat, min_lon, max_lat, max_lon
228
+
229
  for tag_id, value in tag_dict:
230
  tag_name = TiffTags.TAGS.get(tag_id, str(tag_id))
231
  logging.debug(f"TIFF tag: {tag_name} ({tag_id}): {value}")
232
+
233
  if tag_id == 33550: # ModelPixelScaleTag
234
  pixel_scale_tag = value
235
  elif tag_id == 33922: # ModelTiepointTag
236
  tiepoint_tag = value
237
+
238
  # Supplementary check for the log output we can see (raw detection)
239
  # Look for any GeoTIFF tag indicators in the output
240
  geotiff_indicators = ['ModelPixelScale', 'ModelTiepoint', 'GeoKey', 'GeoAscii']
241
  has_geotiff_indicators = False
242
+
243
  for indicator in geotiff_indicators:
244
  if indicator in str(img.tag):
245
  has_geotiff_indicators = True
246
  logging.info(f"Found GeoTIFF indicator: {indicator}")
247
  break
248
+
249
  # Look for any TIFF tag containing geographic info
250
  log_pattern = r"ModelPixelScaleTag.*?value: b'(.*?)'"
251
  log_matches = re.findall(log_pattern, str(img.tag))
252
+
253
  # If we detect any GeoTIFF indicators or raw tags, consider it a Brazil image
254
  if (log_matches or has_geotiff_indicators) and not pixel_scale_tag:
255
  logging.info(f"GeoTIFF indicators detected in image")
256
+
257
+ # Remove hardcoded Brazil coordinates
258
+ # Try to extract values from raw tag data if possible
259
+ try:
260
+ # Parse the modelPixelScale if available
261
+ if log_matches:
262
+ logging.info(f"Found raw pixel scale data: {log_matches[0]}")
263
+ # We'll continue with the standard TIFF tag processing below
264
+ except Exception as e:
265
+ logging.error(f"Error parsing raw tag data: {str(e)}")
266
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  if pixel_scale_tag and tiepoint_tag:
268
  # Extract pixel scale (x, y)
269
  x_scale = float(pixel_scale_tag[0])
270
  y_scale = float(pixel_scale_tag[1])
271
+
272
  # Extract model tiepoint (raster origin)
273
  i, j, k = float(tiepoint_tag[0]), float(tiepoint_tag[1]), float(tiepoint_tag[2])
274
  x, y, z = float(tiepoint_tag[3]), float(tiepoint_tag[4]), float(tiepoint_tag[5])
275
+
276
  # Calculate bounds based on image dimensions
277
  width, height = img.size
278
+
279
  # Calculate bounds
280
  min_lon = x
281
  max_lat = y
282
  max_lon = x + width * x_scale
283
  min_lat = y - height * y_scale
284
+
285
  logging.info(f"Extracted geo bounds: {min_lon},{min_lat} to {max_lon},{max_lat}")
286
  return min_lat, min_lon, max_lat, max_lon
287
+
288
  logging.info("No valid geospatial metadata found in TIFF")
289
+
290
  # Check for EXIF GPS data (typically in JPEG)
291
  elif hasattr(img, '_getexif') and img._getexif():
292
  exif = img._getexif()
293
  if exif and 34853 in exif: # 34853 is the GPS Info tag
294
  gps_info = exif[34853]
295
+
296
  # Extract GPS data
297
  if 1 in gps_info and 2 in gps_info and 3 in gps_info and 4 in gps_info:
298
  # Latitude
 
301
  lat_val = lat[0][0]/lat[0][1] + lat[1][0]/(lat[1][1]*60) + lat[2][0]/(lat[2][1]*3600)
302
  if lat_ref == 'S':
303
  lat_val = -lat_val
304
+
305
  # Longitude
306
  lon_ref = gps_info[3] # 'E' or 'W'
307
  lon = gps_info[4]
308
  lon_val = lon[0][0]/lon[0][1] + lon[1][0]/(lon[1][1]*60) + lon[2][0]/(lon[2][1]*3600)
309
  if lon_ref == 'W':
310
  lon_val = -lon_val
311
+
312
  # Create a small region around the point
313
  delta = 0.01 # ~1km at the equator
314
  min_lat = lat_val - delta
315
  min_lon = lon_val - delta
316
  max_lat = lat_val + delta
317
  max_lon = lon_val + delta
318
+
319
  logging.info(f"Extracted EXIF GPS bounds: {min_lon},{min_lat} to {max_lon},{max_lat}")
320
  return min_lat, min_lon, max_lat, max_lon
321
+
322
  logging.info("No valid GPS metadata found in EXIF")
323
+
324
+ # If we get here, we couldn't extract coordinates
325
+ logging.warning("Could not extract geospatial coordinates from image")
326
  return None
327
  except Exception as e:
328
  logging.error(f"Error extracting geo coordinates: {str(e)}")
329
  return None
330
 
331
+ def convert_to_geojson_with_transform(polygons, image_height, image_width,
332
  min_lat=None, min_lon=None, max_lat=None, max_lon=None):
333
  """
334
  Convert polygons to GeoJSON with proper geographic transformation.
335
+
336
  Args:
337
  polygons (list): List of shapely Polygon objects
338
  image_height (int): Height of the source image
 
341
  min_lon (float, optional): Minimum longitude for geographic bounds
342
  max_lat (float, optional): Maximum latitude for geographic bounds
343
  max_lon (float, optional): Maximum longitude for geographic bounds
344
+
345
  Returns:
346
  dict: GeoJSON object
347
  """
348
  # Set default geographic bounds if not provided
349
  if None in (min_lon, min_lat, max_lon, max_lat):
350
+ logging.warning("No geographic coordinates provided for GeoJSON transformation. Using default values.")
351
  # Default to somewhere neutral (not in New York)
352
  min_lon, min_lat = -98.0, 32.0 # Central US
353
  max_lon, max_lat = -96.0, 34.0
354
+
355
  # Create a GeoJSON feature collection
356
  geojson = {
357
  "type": "FeatureCollection",
358
  "features": []
359
  }
360
+
361
  # Function to transform pixel coordinates to geographic coordinates
362
  def transform_point(x, y):
363
  # Linear interpolation
 
365
  # Invert y-axis for geographic coordinates
366
  lat = max_lat - (y / image_height) * (max_lat - min_lat)
367
  return lon, lat
368
+
369
  # Convert each polygon to a GeoJSON feature
370
  for i, polygon in enumerate(polygons):
371
  # Extract coordinates
372
  coords = list(polygon.exterior.coords)
373
+
374
  # Transform coordinates to geographic space
375
  geo_coords = [transform_point(x, y) for x, y in coords]
376
+
377
  # Create GeoJSON geometry
378
  geometry = {
379
  "type": "Polygon",
380
  "coordinates": [geo_coords]
381
  }
382
+
383
  # Create GeoJSON feature
384
  feature = {
385
  "type": "Feature",
 
389
  },
390
  "geometry": geometry
391
  }
392
+
393
  geojson["features"].append(feature)
394
+
395
  return geojson
396
 
397
+ def process_image_to_geojson(image_path, feature_type="buildings", original_file_path=None):
398
  """
399
  Complete pipeline to convert an image to a simplified GeoJSON.
400
+
401
  Args:
402
  image_path (str): Path to the processed image
403
  feature_type (str): Type of features to extract ("buildings", "trees", "water", "roads")
404
+ original_file_path (str, optional): Path to the original uploaded file
405
+
406
  Returns:
407
  dict: GeoJSON object
408
  """
 
410
  # Open image to get dimensions
411
  img = Image.open(image_path)
412
  width, height = img.size
413
+
414
  # Import segmentation module here to avoid circular imports
415
  from utils.segmentation import segment_and_extract_features
416
+
417
  # Extract features using advanced segmentation
418
  _, polygons = segment_and_extract_features(
419
+ image_path,
420
  output_mask_path=None,
421
  feature_type=feature_type,
422
+ min_area=50,
423
  simplify_tolerance=2.0,
424
  merge_distance=5.0
425
  )
426
+
427
  if not polygons:
428
  logging.warning("No polygons found in the image after segmentation")
429
  return {"type": "FeatureCollection", "features": []}
430
+
431
+ # Use the provided original file path if available
432
+ original_image_path = original_file_path
433
+
434
+ # If no original file path was provided, try to find it
435
+ if not original_image_path and "_processed" in image_path:
436
  original_image_path = image_path.replace("_processed", "")
437
  # Try the original image path but replace the extension with common formats
438
  if not os.path.exists(original_image_path):
 
441
  if os.path.exists(base_path + ext):
442
  original_image_path = base_path + ext
443
  break
444
+
445
+ logging.info(f"Using original image path: {original_image_path}")
446
+
447
  # Extract bounds from image if possible
448
  coords = None
449
  if original_image_path and os.path.exists(original_image_path):
450
  logging.info(f"Checking original image for geospatial data: {original_image_path}")
451
  coords = extract_geo_coordinates_from_image(original_image_path)
452
+
453
  if not coords:
454
  logging.info("Checking processed image for geospatial data")
455
  coords = extract_geo_coordinates_from_image(image_path)
456
+
457
  # Use extracted coordinates or defaults
458
  if coords:
459
  min_lat, min_lon, max_lat, max_lon = coords
460
+ logging.info(f"Using extracted coordinates: {min_lon},{min_lat} to {max_lon},{max_lat}")
461
  else:
462
+ # Try one more time with rasterio directly on the original image if it exists
463
+ if original_image_path and os.path.exists(original_image_path) and original_image_path.lower().endswith(('.tif', '.tiff')):
464
+ try:
465
+ import rasterio
466
+ from rasterio.warp import transform_bounds
467
+
468
+ with rasterio.open(original_image_path) as src:
469
+ if src.crs is not None:
470
+ bounds = src.bounds
471
+ if src.crs.to_epsg() != 4326:
472
+ west, south, east, north = transform_bounds(
473
+ src.crs, 'EPSG:4326',
474
+ bounds.left, bounds.bottom, bounds.right, bounds.top
475
+ )
476
+ else:
477
+ west, south, east, north = bounds
478
+
479
+ min_lat, min_lon, max_lat, max_lon = south, west, north, east
480
+ logging.info(f"Using coordinates from rasterio: {min_lon},{min_lat} to {max_lon},{max_lat}")
481
+ except Exception as e:
482
+ logging.warning(f"Failed to extract coordinates with rasterio: {str(e)}")
483
+ logging.warning("No coordinates found in image, using default location in Central US")
484
+ min_lat, min_lon = 32.0, -98.0 # Central US
485
+ max_lat, max_lon = 34.0, -96.0
486
+ else:
487
+ logging.warning("No coordinates found in image, using default location in Central US")
488
+ min_lat, min_lon = 32.0, -98.0 # Central US
489
+ max_lat, max_lon = 34.0, -96.0
490
+
491
  # Convert to GeoJSON with proper transformation
492
  geojson = convert_to_geojson_with_transform(
493
  polygons, height, width,
494
  min_lat=min_lat, min_lon=min_lon,
495
  max_lat=max_lat, max_lon=max_lon
496
  )
497
+
498
  return geojson
499
+
500
  except Exception as e:
501
  logging.error(f"Error in GeoJSON processing: {str(e)}")
502
  return {"type": "FeatureCollection", "features": []}