Geoeasy commited on
Commit
21dff12
·
verified ·
1 Parent(s): 10d326d

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +230 -0
app.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import os
3
+ import tempfile
4
+ import zipfile
5
+ import uuid
6
+ import rasterio
7
+ import numpy as np
8
+ from rasterio.mask import mask
9
+ import geopandas as gpd
10
+ import matplotlib.pyplot as plt
11
+ from io import BytesIO
12
+ from PIL import Image
13
+ import shutil
14
+
15
+
16
+ def clip_geotiff(tif_file, shapefile_zip):
17
+ """
18
+ Clips a GeoTIFF file using a shapefile
19
+ """
20
+ temp_dir = None
21
+ try:
22
+ # Check if files were provided
23
+ if tif_file is None or shapefile_zip is None:
24
+ return None, None
25
+
26
+ # Create unique temporary directory
27
+ temp_dir = tempfile.mkdtemp(prefix="geotiff_clip_")
28
+ img_dir = os.path.join(temp_dir, "image")
29
+ shp_dir = os.path.join(temp_dir, "shapefile")
30
+ out_dir = os.path.join(temp_dir, "output")
31
+
32
+ # Create subdirectories
33
+ for directory in [img_dir, shp_dir, out_dir]:
34
+ os.makedirs(directory, exist_ok=True)
35
+
36
+ # Handle TIFF file - tif_file is now a file path (string)
37
+ tif_path = os.path.join(img_dir, f"input_{uuid.uuid4().hex}.tif")
38
+ shutil.copy2(tif_file, tif_path)
39
+
40
+ # Handle shapefile ZIP - shapefile_zip is now a file path (string)
41
+ zip_path = os.path.join(shp_dir, f"shapefile_{uuid.uuid4().hex}.zip")
42
+ shutil.copy2(shapefile_zip, zip_path)
43
+
44
+ # Extract ZIP
45
+ with zipfile.ZipFile(zip_path, 'r') as zip_ref:
46
+ zip_ref.extractall(shp_dir)
47
+
48
+ # Find .shp file
49
+ shp_files = [f for f in os.listdir(shp_dir) if f.endswith(".shp")]
50
+ if not shp_files:
51
+ raise FileNotFoundError(".shp file not found in the provided ZIP.")
52
+
53
+ shp_file = os.path.join(shp_dir, shp_files[0])
54
+
55
+ # Read shapefile using geopandas
56
+ gdf = gpd.read_file(shp_file)
57
+
58
+ # Check if shapefile has valid geometries
59
+ if gdf.empty or gdf.geometry.isna().all():
60
+ raise ValueError("Shapefile does not contain valid geometries.")
61
+
62
+ # Open and process GeoTIFF file
63
+ with rasterio.open(tif_path) as src:
64
+ # Check for overlap between shapefile and raster
65
+ gdf_proj = gdf.to_crs(src.crs)
66
+
67
+ # Perform clipping
68
+ out_image, out_transform = mask(src, gdf_proj.geometry, crop=True, nodata=src.nodata)
69
+
70
+ # Update metadata
71
+ out_meta = src.meta.copy()
72
+ out_meta.update({
73
+ "height": out_image.shape[1],
74
+ "width": out_image.shape[2],
75
+ "transform": out_transform,
76
+ "nodata": src.nodata
77
+ })
78
+
79
+ # Create output file in a persistent temporary location
80
+ output_filename = f"clipped_{uuid.uuid4().hex}.tif"
81
+ # Use tempfile.NamedTemporaryFile to create a file that Gradio can manage
82
+ output_temp_file = tempfile.NamedTemporaryFile(
83
+ suffix=".tif",
84
+ prefix="clipped_",
85
+ delete=False # Don't auto-delete, let Gradio handle it
86
+ )
87
+ output_tif_path = output_temp_file.name
88
+ output_temp_file.close() # Close the file handle so rasterio can write to it
89
+
90
+ with rasterio.open(output_tif_path, "w", **out_meta) as dest:
91
+ dest.write(out_image)
92
+
93
+ # Create PNG visualization in memory
94
+ # Prepare data for visualization
95
+ if out_image.shape[0] >= 3:
96
+ # If has 3 or more bands, use first 3 (RGB)
97
+ preview_array = out_image[:3]
98
+ else:
99
+ # If has less than 3 bands, repeat first band
100
+ preview_array = np.repeat(out_image[0:1], 3, axis=0)
101
+
102
+ # Rearrange dimensions (bands, height, width) -> (height, width, bands)
103
+ preview_array = np.moveaxis(preview_array, 0, -1)
104
+
105
+ # Normalize values to 0-255 if necessary
106
+ if preview_array.dtype != np.uint8:
107
+ # Normalize to 0-255
108
+ preview_min = np.nanmin(preview_array)
109
+ preview_max = np.nanmax(preview_array)
110
+ if preview_max > preview_min:
111
+ preview_array = ((preview_array - preview_min) / (preview_max - preview_min) * 255).astype(np.uint8)
112
+ else:
113
+ preview_array = np.zeros_like(preview_array, dtype=np.uint8)
114
+
115
+ # Handle nodata values
116
+ if out_meta.get('nodata') is not None:
117
+ nodata_mask = np.any(out_image == out_meta['nodata'], axis=0)
118
+ preview_array[nodata_mask] = [0, 0, 0] # Black for nodata
119
+
120
+ # Create matplotlib figure
121
+ plt.style.use('default') # Ensure default style
122
+ fig, ax = plt.subplots(figsize=(10, 8), dpi=100)
123
+ ax.imshow(preview_array)
124
+ ax.set_title(f"Clipped GeoTIFF - {output_filename}", fontsize=12, pad=20)
125
+ ax.axis('off')
126
+
127
+ # Save to memory buffer
128
+ buf = BytesIO()
129
+ plt.savefig(buf, format='png', bbox_inches='tight', pad_inches=0.1, dpi=150)
130
+ plt.close(fig) # Important: close figure to free memory
131
+ buf.seek(0)
132
+
133
+ # Convert to PIL image
134
+ pil_image = Image.open(buf).convert("RGB")
135
+ buf.close()
136
+
137
+ # Return the PIL image and the path to the persistent temporary file
138
+ return pil_image, output_tif_path
139
+
140
+ except Exception as e:
141
+ error_msg = f"Error during processing: {str(e)}"
142
+ print(f"[ERROR] {error_msg}")
143
+
144
+ # Create error image
145
+ error_image = Image.new('RGB', (400, 200), color='white')
146
+ from PIL import ImageDraw, ImageFont
147
+ draw = ImageDraw.Draw(error_image)
148
+ try:
149
+ font = ImageFont.truetype("arial.ttf", 16)
150
+ except:
151
+ font = ImageFont.load_default()
152
+
153
+ draw.text((10, 10), "Processing Error:", fill='red', font=font)
154
+ draw.text((10, 40), str(e)[:50] + "..." if len(str(e)) > 50 else str(e), fill='black', font=font)
155
+
156
+ return error_image, None
157
+
158
+ finally:
159
+ # Clean up temporary directory (but not the output file)
160
+ if temp_dir and os.path.exists(temp_dir):
161
+ try:
162
+ shutil.rmtree(temp_dir)
163
+ except:
164
+ pass # Ignore cleanup errors
165
+
166
+
167
+ # Gradio Interface
168
+ with gr.Blocks(title="GeoTIFF Clipper", theme=gr.themes.Soft()) as app:
169
+ gr.Markdown("""
170
+ # 🛰️ GeoTIFF Clipping with Shapefile
171
+
172
+ This tool allows you to clip GeoTIFF images using shapefiles as clipping masks.
173
+
174
+ **Instructions:**
175
+ 1. Upload a GeoTIFF file (.tif)
176
+ 2. Upload a ZIP file containing the shapefile (.shp, .dbf, .shx, .prj)
177
+ 3. Click "Execute Clipping"
178
+ 4. View the result and download the clipped file
179
+ """)
180
+
181
+ with gr.Row():
182
+ with gr.Column():
183
+ geotiff_input = gr.File(
184
+ label="📁 GeoTIFF File (.tif)",
185
+ file_types=[".tif", ".tiff"],
186
+ type="filepath"
187
+ )
188
+ shapefile_input = gr.File(
189
+ label="📁 Shapefile ZIP (.zip)",
190
+ file_types=[".zip"],
191
+ type="filepath"
192
+ )
193
+ run_button = gr.Button("🚀 Execute Clipping", variant="primary", size="lg")
194
+
195
+ with gr.Row():
196
+ with gr.Column():
197
+ preview_output = gr.Image(
198
+ label="🖼️ Result Preview",
199
+ type="pil",
200
+ height=400
201
+ )
202
+ with gr.Column():
203
+ tif_output = gr.File(
204
+ label="💾 Download Clipped GeoTIFF",
205
+ type="filepath"
206
+ )
207
+
208
+ # Connect function to button
209
+ run_button.click(
210
+ fn=clip_geotiff,
211
+ inputs=[geotiff_input, shapefile_input],
212
+ outputs=[preview_output, tif_output]
213
+ )
214
+
215
+ gr.Markdown("""
216
+ ---
217
+ **Notes:**
218
+ - The shapefile ZIP must contain at least .shp, .dbf and .shx files
219
+ - Coordinate systems will be automatically adjusted if necessary
220
+ - Preview is automatically generated for visualization
221
+ """)
222
+
223
+ # Configure for Hugging Face Spaces
224
+ if __name__ == "__main__":
225
+ app.launch(
226
+ server_name="0.0.0.0", # Required for Hugging Face Spaces
227
+ server_port=7860, # Default Hugging Face Spaces port
228
+ share=False # Don't create additional public link
229
+ )
230
+