manuelhorveydaniel commited on
Commit
9412d2c
·
verified ·
1 Parent(s): 29e87c3

Initial push to HuggingFace with model files

Browse files
Files changed (4) hide show
  1. README.md +56 -3
  2. app.py +214 -0
  3. models/best_model.pth +3 -0
  4. requirements.txt +8 -0
README.md CHANGED
@@ -1,3 +1,56 @@
1
- ---
2
- license: mit
3
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Deforestation Detection App
2
+
3
+ This application uses a transformer-based ChangeFormer model to detect deforestation in the Brazilian Amazon using Sentinel-2 satellite imagery. Developed as a final year project, it processes 4-band (RGB + NIR) .tif images from 2020 and 2021 to generate binary change masks and overlay predictions, achieving an F1-score of 0.9986 and IoU of 0.9972 on validation data.
4
+
5
+ ## Overview
6
+ - **Model**: Custom ChangeFormer with a VisionTransformer encoder, FeatureDifferenceModule, and DeconvDecoder.
7
+ - **Data**: Sentinel-2 Level-2A imagery (10m resolution) and PRODES ground-truth data.
8
+ - **Interface**: Built with Gradio for interactive uploads and visualizations.
9
+ - **Purpose**: Supports land governance, policy-making, and ecological conservation through scalable deforestation monitoring.
10
+
11
+ ## Features
12
+ - Upload two .tif images (2020 and 2021) with 4 bands (B2, B3, B4, B8).
13
+ - Outputs: Raw 2021 RGB, overlay with predicted deforestation, binary change mask, and a comment on change percentage.
14
+ - Patch-wise processing for large images, with percentile-based normalization and stitching.
15
+
16
+ ## Setup
17
+ 1. **Prerequisites**:
18
+ - Python 3.8+
19
+ - Required libraries: `torch`, `torchvision`, `timm`, `rasterio`, `numpy`, `pillow`, `gradio`.
20
+
21
+ 2. **Installation**:
22
+ - Clone or download this repository.
23
+ - Install dependencies:
24
+ ```bash
25
+ pip install -r requirements.txt
26
+ ```
27
+ - Place your pretrained model (`best_model.pth`) in the `models/` folder.
28
+
29
+ 3. **Run Locally**:
30
+ - Launch the app:
31
+ ```bash
32
+ python app.py
33
+ ```
34
+ - Access the interface at `http://localhost:7860`.
35
+
36
+ 4. **Deployed Version**:
37
+ - Check the live app at [Insert Hugging Face Space URL] (once deployed).
38
+
39
+ ## Usage
40
+ - **Input**: Upload two .tif files (e.g., 256x256 patches) containing RGB and NIR bands.
41
+ - **Output**:
42
+ - **Raw 2021 RGB**: Normalized base image.
43
+ - **Overlay with Prediction**: Red overlay highlighting deforested areas.
44
+ - **Binary Change Mask**: Black-and-white change map.
45
+ - **Comment**: Auto-generated note on change extent (e.g., "Significant change detected: 5.83%").
46
+ - **Notes**: Ensure images are preprocessed (e.g., <20% cloud cover) for best results.
47
+
48
+ ## Project Details
49
+ - **Region of Interest**: Top 5 deforested conservation units (e.g., Área de Proteção Ambiental Triunfo do Xingu).
50
+ - **Dataset**: 19,560 bitemporal patches (2020–2021), augmented with rotations.
51
+ - **Performance**: Validation F1-score: 0.9986, IoU: 0.9972.
52
+ - **Future Work**: Multi-year forecasting, web-based alerts, SAR integration.
53
+
54
+ ## Credits
55
+ - **Author**: Emmanuel Amey, Sammuel Young Appiah, Asare Prince Owusu, Yaaya Pearl Apenu.
56
+ - **References**: Inspired by Alshehri et al. (2024), IEEE Geoscience and Remote Sensing Letters.
app.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import gradio as gr
3
+ import torch
4
+ import numpy as np
5
+ from torchvision import transforms
6
+ from PIL import Image
7
+ import rasterio
8
+ import torch.nn as nn
9
+ import torch.nn.functional as F
10
+ from timm.models.vision_transformer import VisionTransformer
11
+
12
+ # Model Components
13
+ class FeatureDifferenceModule(nn.Module):
14
+ def __init__(self, in_channels):
15
+ super(FeatureDifferenceModule, self).__init__()
16
+ self.conv = nn.Conv2d(in_channels, in_channels // 2, kernel_size=3, padding=1)
17
+ self.bn = nn.BatchNorm2d(in_channels // 2)
18
+ self.relu = nn.ReLU()
19
+
20
+ def forward(self, feat1, feat2):
21
+ x = torch.abs(feat1 - feat2)
22
+ x = self.conv(x)
23
+ x = self.bn(x)
24
+ x = self.relu(x)
25
+ return x
26
+
27
+ class DeconvDecoder(nn.Module):
28
+ def __init__(self, in_channels, num_classes):
29
+ super(DeconvDecoder, self).__init__()
30
+ self.deconv1 = nn.ConvTranspose2d(in_channels // 2, 128, kernel_size=3, stride=2, padding=1, output_padding=1)
31
+ self.deconv2 = nn.ConvTranspose2d(128, 32, kernel_size=3, stride=2, padding=1, output_padding=1)
32
+ self.deconv3 = nn.ConvTranspose2d(32, num_classes, kernel_size=3, stride=2, padding=1, output_padding=1)
33
+
34
+ def forward(self, x):
35
+ x = F.relu(self.deconv1(x))
36
+ x = F.relu(self.deconv2(x))
37
+ x = self.deconv3(x)
38
+ return x
39
+
40
+ class ChangeFormer(nn.Module):
41
+ def __init__(self, img_size=256, num_classes=1):
42
+ super(ChangeFormer, self).__init__()
43
+ self.encoder = VisionTransformer(
44
+ img_size=img_size,
45
+ patch_size=16,
46
+ embed_dim=384,
47
+ depth=4,
48
+ num_heads=6,
49
+ in_chans=4,
50
+ )
51
+ self.feature_diff = FeatureDifferenceModule(in_channels=384)
52
+ self.decoder = DeconvDecoder(in_channels=384, num_classes=num_classes)
53
+ self.img_size = img_size
54
+ self.patch_size = 16
55
+
56
+ def forward(self, img1, img2):
57
+ feat1 = self.encoder.forward_features(img1)
58
+ feat2 = self.encoder.forward_features(img2)
59
+ feat1 = feat1[:, 1:, :]
60
+ feat2 = feat2[:, 1:, :]
61
+ B, N, C = feat1.shape
62
+ h = w = self.img_size // self.patch_size
63
+ feat1 = feat1.transpose(1, 2).view(B, C, h, w)
64
+ feat2 = feat2.transpose(1, 2).view(B, C, h, w)
65
+ diff = self.feature_diff(feat1, feat2)
66
+ out = self.decoder(diff)
67
+ out = F.interpolate(out, size=(self.img_size, self.img_size), mode='bilinear', align_corners=False)
68
+ return out
69
+
70
+ # Model Initialization
71
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
72
+ model = ChangeFormer(num_classes=1).to(device)
73
+ print("ChangeFormer Model Initialized!")
74
+
75
+ # Load model weights
76
+ model_path = "/content/drive/MyDrive/DeforestationApp/models/best_model.pth"
77
+ if not os.path.exists(model_path):
78
+ raise FileNotFoundError(f"Model file not found at {model_path}.")
79
+ model.load_state_dict(torch.load(model_path, map_location=device))
80
+ model.eval()
81
+
82
+ PATCH_SIZE = 256
83
+ transform = transforms.ToTensor()
84
+
85
+ def read_patch_4band(path, x, y, size=PATCH_SIZE):
86
+ with rasterio.open(path) as src:
87
+ band_indices = [i for i in range(1, min(src.count, 4) + 1)] # Bands 1–4
88
+ patch = src.read(band_indices, window=rasterio.windows.Window(x, y, size, size))
89
+
90
+ # Optional: cloud masking if band 8 (SCL) is present
91
+ if src.count >= 8:
92
+ scl = src.read(8, window=rasterio.windows.Window(x, y, size, size))
93
+ cloud_mask = (scl == 3) | (scl == 8) | (scl == 9)
94
+ patch[:, cloud_mask] = 0
95
+
96
+ patch = np.transpose(patch, (1, 2, 0))
97
+ return patch
98
+
99
+ def get_patch_coords(path, patch_size=PATCH_SIZE):
100
+ with rasterio.open(path) as src:
101
+ w, h = src.width, src.height
102
+ coords = [(x, y) for y in range(0, h, patch_size)
103
+ for x in range(0, w, patch_size)
104
+ if x + patch_size <= w and y + patch_size <= h]
105
+ return coords, (w, h)
106
+
107
+ def predict_on_large_4band_tifs(path1, path2):
108
+ coords, full_size = get_patch_coords(path1)
109
+ preds = []
110
+ for i in range(0, len(coords), 4): # Batch size of 4
111
+ batch_coords = coords[i:i+4]
112
+ batch_t1, batch_t2 = [], []
113
+ for x, y in batch_coords:
114
+ patch1 = read_patch_4band(path1, x, y)
115
+ patch2 = read_patch_4band(path2, x, y)
116
+ batch_t1.append(transform(patch1))
117
+ batch_t2.append(transform(patch2))
118
+ t1 = torch.stack(batch_t1).to(device)
119
+ t2 = torch.stack(batch_t2).to(device)
120
+ with torch.no_grad():
121
+ pred = model(t1, t2)
122
+ pred = torch.sigmoid(pred).squeeze().cpu().numpy()
123
+ for p, (x, y) in zip(pred, batch_coords):
124
+ pred_binary = (p > 0.5).astype(np.uint8)
125
+ preds.append((pred_binary, (x, y)))
126
+ return preds, full_size
127
+
128
+ def stitch_patches(preds, full_size, patch_size=PATCH_SIZE):
129
+ stitched = np.zeros((full_size[1], full_size[0]), dtype=np.uint8)
130
+ for patch, (x, y) in preds:
131
+ stitched[y:y+patch_size, x:x+patch_size] = patch
132
+ return stitched
133
+
134
+ def normalize_rgb(path):
135
+ with rasterio.open(path) as src:
136
+ rgb = src.read([1, 2, 3]).astype(np.float32)
137
+ rgb = np.transpose(rgb, (1, 2, 0))
138
+ mask = np.any(np.isnan(rgb), axis=-1) | np.all(rgb == 0, axis=-1)
139
+ rgb[mask] = np.nan
140
+ p2 = np.nanpercentile(rgb, 2)
141
+ p98 = np.nanpercentile(rgb, 98)
142
+ if p98 - p2 < 1e-5:
143
+ rgb = np.clip(rgb / 255.0, 0, 1)
144
+ else:
145
+ rgb = np.clip((rgb - p2) / (p98 - p2), 0, 1)
146
+ rgb = np.nan_to_num(rgb)
147
+ return rgb
148
+
149
+ def overlay_mask(rgb_img, mask, alpha=0.4):
150
+ mask = mask.astype(np.float32)
151
+ color_mask = np.zeros_like(rgb_img)
152
+ color_mask[..., 0] = mask
153
+ blended = (1 - alpha) * rgb_img + alpha * color_mask
154
+ blended = np.clip(blended, 0, 1)
155
+ return (blended * 255).astype(np.uint8)
156
+
157
+ def generate_comment(mask):
158
+ changed_pixels = np.count_nonzero(mask)
159
+ total_pixels = mask.size
160
+ percent = (changed_pixels / total_pixels) * 100
161
+ if percent > 5:
162
+ return f"Significant change detected: {percent:.2f}%"
163
+ elif percent > 1:
164
+ return f"Minor change detected: {percent:.2f}%"
165
+ elif percent > 0:
166
+ return f"Minimal change: {percent:.2f}%"
167
+ else:
168
+ return "No change detected."
169
+
170
+ def clear_outputs():
171
+ return None, None, None, "Please upload new images to generate results."
172
+
173
+ def predict_change(file1, file2):
174
+ try:
175
+ path1, path2 = file1.name, file2.name
176
+ with rasterio.open(path1) as src:
177
+ if src.count < 4:
178
+ raise ValueError("Input image must have at least 4 bands (RGB+NIR).")
179
+
180
+ preds, full_size = predict_on_large_4band_tifs(path1, path2)
181
+ mask = stitch_patches(preds, full_size)
182
+ rgb = normalize_rgb(path2)
183
+ overlay = overlay_mask(rgb, mask)
184
+ return (
185
+ Image.fromarray((rgb * 255).astype(np.uint8)),
186
+ Image.fromarray(overlay),
187
+ Image.fromarray((mask * 255).astype(np.uint8)),
188
+ generate_comment(mask)
189
+ )
190
+ except Exception as e:
191
+ return None, None, None, f"Error: {str(e)}"
192
+
193
+ # ==========================
194
+ # Gradio UI
195
+ # ==========================
196
+ with gr.Blocks() as demo:
197
+ gr.Markdown("### UPLOAD INSTRUCTIONS:\n- **First Image** → OLDER image (earlier date)\n- **Second Image** → NEWER image (later date)\n\n> Both images must have **at least 4 bands (RGB + NIR)**.")
198
+
199
+ with gr.Row():
200
+ file1 = gr.File(label=" First Image (OLDER)", file_types=[".tif"])
201
+ file2 = gr.File(label=" Second Image (NEWER)", file_types=[".tif"])
202
+ with gr.Row():
203
+ output1 = gr.Image(label="Raw Second Image RGB")
204
+ output2 = gr.Image(label="Overlay with Prediction")
205
+ output3 = gr.Image(label="Binary Change Mask")
206
+ output4 = gr.Textbox(label="Auto-generated Comment")
207
+
208
+ file1.upload(clear_outputs, None, [output1, output2, output3, output4])
209
+ file2.upload(clear_outputs, None, [output1, output2, output3, output4])
210
+
211
+ btn = gr.Button("Submit")
212
+ btn.click(predict_change, inputs=[file1, file2], outputs=[output1, output2, output3, output4])
213
+
214
+ demo.launch()
models/best_model.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:caa69fd5f563fd7a92da4011bbe0a36025130c09618a1da583b377ddef38dee0
3
+ size 35624354
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # %%writefile /content/drive/MyDrive/DeforestationApp/requirements.txt
2
+ torch
3
+ torchvision
4
+ timm
5
+ rasterio
6
+ numpy
7
+ pillow
8
+ gradio