File size: 8,264 Bytes
21dff12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
import gradio as gr
import os
import tempfile
import zipfile
import uuid
import rasterio
import numpy as np
from rasterio.mask import mask
import geopandas as gpd
import matplotlib.pyplot as plt
from io import BytesIO
from PIL import Image
import shutil


def clip_geotiff(tif_file, shapefile_zip):
    """
    Clips a GeoTIFF file using a shapefile
    """
    temp_dir = None
    try:
        # Check if files were provided
        if tif_file is None or shapefile_zip is None:
            return None, None
        
        # Create unique temporary directory
        temp_dir = tempfile.mkdtemp(prefix="geotiff_clip_")
        img_dir = os.path.join(temp_dir, "image")
        shp_dir = os.path.join(temp_dir, "shapefile")
        out_dir = os.path.join(temp_dir, "output")
        
        # Create subdirectories
        for directory in [img_dir, shp_dir, out_dir]:
            os.makedirs(directory, exist_ok=True)

        # Handle TIFF file - tif_file is now a file path (string)
        tif_path = os.path.join(img_dir, f"input_{uuid.uuid4().hex}.tif")
        shutil.copy2(tif_file, tif_path)

        # Handle shapefile ZIP - shapefile_zip is now a file path (string)
        zip_path = os.path.join(shp_dir, f"shapefile_{uuid.uuid4().hex}.zip")
        shutil.copy2(shapefile_zip, zip_path)
        
        # Extract ZIP
        with zipfile.ZipFile(zip_path, 'r') as zip_ref:
            zip_ref.extractall(shp_dir)

        # Find .shp file
        shp_files = [f for f in os.listdir(shp_dir) if f.endswith(".shp")]
        if not shp_files:
            raise FileNotFoundError(".shp file not found in the provided ZIP.")
        
        shp_file = os.path.join(shp_dir, shp_files[0])

        # Read shapefile using geopandas
        gdf = gpd.read_file(shp_file)
        
        # Check if shapefile has valid geometries
        if gdf.empty or gdf.geometry.isna().all():
            raise ValueError("Shapefile does not contain valid geometries.")

        # Open and process GeoTIFF file
        with rasterio.open(tif_path) as src:
            # Check for overlap between shapefile and raster
            gdf_proj = gdf.to_crs(src.crs)
            
            # Perform clipping
            out_image, out_transform = mask(src, gdf_proj.geometry, crop=True, nodata=src.nodata)
            
            # Update metadata
            out_meta = src.meta.copy()
            out_meta.update({
                "height": out_image.shape[1],
                "width": out_image.shape[2],
                "transform": out_transform,
                "nodata": src.nodata
            })

            # Create output file in a persistent temporary location
            output_filename = f"clipped_{uuid.uuid4().hex}.tif"
            # Use tempfile.NamedTemporaryFile to create a file that Gradio can manage
            output_temp_file = tempfile.NamedTemporaryFile(
                suffix=".tif", 
                prefix="clipped_", 
                delete=False  # Don't auto-delete, let Gradio handle it
            )
            output_tif_path = output_temp_file.name
            output_temp_file.close()  # Close the file handle so rasterio can write to it
            
            with rasterio.open(output_tif_path, "w", **out_meta) as dest:
                dest.write(out_image)

        # Create PNG visualization in memory
        # Prepare data for visualization
        if out_image.shape[0] >= 3:
            # If has 3 or more bands, use first 3 (RGB)
            preview_array = out_image[:3]
        else:
            # If has less than 3 bands, repeat first band
            preview_array = np.repeat(out_image[0:1], 3, axis=0)
        
        # Rearrange dimensions (bands, height, width) -> (height, width, bands)
        preview_array = np.moveaxis(preview_array, 0, -1)
        
        # Normalize values to 0-255 if necessary
        if preview_array.dtype != np.uint8:
            # Normalize to 0-255
            preview_min = np.nanmin(preview_array)
            preview_max = np.nanmax(preview_array)
            if preview_max > preview_min:
                preview_array = ((preview_array - preview_min) / (preview_max - preview_min) * 255).astype(np.uint8)
            else:
                preview_array = np.zeros_like(preview_array, dtype=np.uint8)
        
        # Handle nodata values
        if out_meta.get('nodata') is not None:
            nodata_mask = np.any(out_image == out_meta['nodata'], axis=0)
            preview_array[nodata_mask] = [0, 0, 0]  # Black for nodata

        # Create matplotlib figure
        plt.style.use('default')  # Ensure default style
        fig, ax = plt.subplots(figsize=(10, 8), dpi=100)
        ax.imshow(preview_array)
        ax.set_title(f"Clipped GeoTIFF - {output_filename}", fontsize=12, pad=20)
        ax.axis('off')
        
        # Save to memory buffer
        buf = BytesIO()
        plt.savefig(buf, format='png', bbox_inches='tight', pad_inches=0.1, dpi=150)
        plt.close(fig)  # Important: close figure to free memory
        buf.seek(0)

        # Convert to PIL image
        pil_image = Image.open(buf).convert("RGB")
        buf.close()

        # Return the PIL image and the path to the persistent temporary file
        return pil_image, output_tif_path

    except Exception as e:
        error_msg = f"Error during processing: {str(e)}"
        print(f"[ERROR] {error_msg}")
        
        # Create error image
        error_image = Image.new('RGB', (400, 200), color='white')
        from PIL import ImageDraw, ImageFont
        draw = ImageDraw.Draw(error_image)
        try:
            font = ImageFont.truetype("arial.ttf", 16)
        except:
            font = ImageFont.load_default()
        
        draw.text((10, 10), "Processing Error:", fill='red', font=font)
        draw.text((10, 40), str(e)[:50] + "..." if len(str(e)) > 50 else str(e), fill='black', font=font)
        
        return error_image, None
    
    finally:
        # Clean up temporary directory (but not the output file)
        if temp_dir and os.path.exists(temp_dir):
            try:
                shutil.rmtree(temp_dir)
            except:
                pass  # Ignore cleanup errors


# Gradio Interface
with gr.Blocks(title="GeoTIFF Clipper", theme=gr.themes.Soft()) as app:
    gr.Markdown("""
    # ๐Ÿ›ฐ๏ธ GeoTIFF Clipping with Shapefile
    
    This tool allows you to clip GeoTIFF images using shapefiles as clipping masks.
    
    **Instructions:**
    1. Upload a GeoTIFF file (.tif)
    2. Upload a ZIP file containing the shapefile (.shp, .dbf, .shx, .prj)
    3. Click "Execute Clipping"
    4. View the result and download the clipped file
    """)

    with gr.Row():
        with gr.Column():
            geotiff_input = gr.File(
                label="๐Ÿ“ GeoTIFF File (.tif)", 
                file_types=[".tif", ".tiff"],
                type="filepath"
            )
            shapefile_input = gr.File(
                label="๐Ÿ“ Shapefile ZIP (.zip)", 
                file_types=[".zip"],
                type="filepath"
            )
            run_button = gr.Button("๐Ÿš€ Execute Clipping", variant="primary", size="lg")

    with gr.Row():
        with gr.Column():
            preview_output = gr.Image(
                label="๐Ÿ–ผ๏ธ Result Preview", 
                type="pil",
                height=400
            )
        with gr.Column():
            tif_output = gr.File(
                label="๐Ÿ’พ Download Clipped GeoTIFF",
                type="filepath"
            )

    # Connect function to button
    run_button.click(
        fn=clip_geotiff, 
        inputs=[geotiff_input, shapefile_input], 
        outputs=[preview_output, tif_output]
    )

    gr.Markdown("""
    ---
    **Notes:**
    - The shapefile ZIP must contain at least .shp, .dbf and .shx files
    - Coordinate systems will be automatically adjusted if necessary
    - Preview is automatically generated for visualization
    """)

# Configure for Hugging Face Spaces
if __name__ == "__main__":
    app.launch(
        server_name="0.0.0.0",  # Required for Hugging Face Spaces
        server_port=7860,       # Default Hugging Face Spaces port
        share=False             # Don't create additional public link
    )