Geoeasy commited on
Commit
a071f48
·
verified ·
1 Parent(s): 8e67ac5

Upload 2 files

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