coderuday21 commited on
Commit
5cee5a6
·
1 Parent(s): 105a05d

Add landslide detection menu, separate engine, and Uttarakhand integration plan

Browse files
Landslide_Detection_Uttarakhand_Integration_Plan.md ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Landslide Detection Integration Plan (Uttarakhand)
2
+
3
+ This note covers:
4
+ - candidate datasets for Uttarakhand landslide monitoring,
5
+ - model/research direction,
6
+ - system architecture for integration,
7
+ - preprocessing and feature extraction starter workflow.
8
+
9
+ ## 1) Candidate Datasets (Uttarakhand + nearby Himalayan context)
10
+
11
+ Use a layered strategy (event inventory + optical + terrain + rainfall):
12
+
13
+ 1. **Landslide Inventory / Event Data**
14
+ - Geological Survey of India (GSI) landslide inventory products.
15
+ - NRSC/Bhuvan and disaster mapping layers (where available for state districts).
16
+ - State disaster management/public reports for dated event polygons/points.
17
+
18
+ 2. **Optical Satellite Time Series**
19
+ - **Sentinel-2 (10m/20m)** for frequent revisit and vegetation/soil change.
20
+ - **Landsat-8/9 (30m)** for long historical baseline.
21
+ - Optional high-resolution commercial tiles for selected validation zones.
22
+
23
+ 3. **Terrain Data (critical for landslide susceptibility)**
24
+ - **SRTM/ALOS/CartoDEM** DEM.
25
+ - Derived slope, aspect, curvature, roughness, topographic wetness proxies.
26
+
27
+ 4. **Rainfall / Trigger Data**
28
+ - IMD gridded rainfall, GPM/IMERG rainfall products.
29
+ - Cumulative rainfall windows (1-day, 3-day, 7-day, 15-day anomalies).
30
+
31
+ 5. **Ancillary Layers**
32
+ - Landcover/forest loss,
33
+ - road and river proximity,
34
+ - settlements/infrastructure overlays for risk prioritization.
35
+
36
+ ## 2) Model/Research Direction
37
+
38
+ Recommended progression:
39
+
40
+ ### Phase A (already started in-app)
41
+ - Rule-based bi-temporal landslide candidate detection:
42
+ - vegetation loss proxy,
43
+ - bare-soil increase,
44
+ - texture and edge disruption,
45
+ - connected-component region extraction.
46
+
47
+ ### Phase B (ML baseline)
48
+ - Pixel/patch classifier (Random Forest / XGBoost) using:
49
+ - optical change features,
50
+ - terrain derivatives,
51
+ - rainfall context,
52
+ - neighborhood statistics.
53
+
54
+ ### Phase C (Deep Learning)
55
+ - U-Net/DeepLab/SegFormer style landslide segmentation with multi-channel input:
56
+ - pre-event image,
57
+ - post-event image,
58
+ - DEM-derived bands (slope/aspect),
59
+ - rainfall summary channels.
60
+
61
+ ### Research papers to review first
62
+ - Remote sensing landslide mapping with deep learning in Himalayan terrain.
63
+ - Bi-temporal change detection for landslide scars (optical and SAR fusion).
64
+ - DEM + rainfall + optical hybrid susceptibility modeling.
65
+
66
+ ## 3) Architecture for Integration (Current App)
67
+
68
+ Integrated design implemented in the app:
69
+
70
+ - New detection menu in UI:
71
+ - `General Change Detection` (existing pipeline),
72
+ - `Landslide Detection (Uttarakhand)` (separate pipeline).
73
+
74
+ - Shared API entrypoint:
75
+ - `POST /api/detect`
76
+ - new form field `detection_type`.
77
+
78
+ - Routing:
79
+ - `detection_type=change_detection` -> `app/detection_engine.py`
80
+ - `detection_type=landslide_detection` -> `app/landslide_engine.py`
81
+
82
+ - Shared output contract:
83
+ - overlay image,
84
+ - stats,
85
+ - regions list,
86
+ - history storage compatible with existing UI and DB.
87
+
88
+ This keeps current production behavior intact while enabling model-specific evolution for landslide.
89
+
90
+ ## 4) Preprocessing and Feature Extraction (Starter)
91
+
92
+ Current landslide starter logic (`app/landslide_engine.py`) includes:
93
+
94
+ 1. **Preprocessing**
95
+ - RGB conversion, controlled resizing.
96
+
97
+ 2. **Feature channels**
98
+ - Green-index drop (vegetation loss proxy),
99
+ - Soil score increase (HSV warm/dry proxy),
100
+ - Texture roughness change (Laplacian-based),
101
+ - Edge disruption map (Canny difference).
102
+
103
+ 3. **Fusion + threshold**
104
+ - weighted fusion of channels,
105
+ - sensitivity-driven percentile threshold.
106
+
107
+ 4. **Post-processing**
108
+ - morphology cleanup,
109
+ - region extraction with confidence/severity assignment.
110
+
111
+ ## 5) Immediate next execution tasks
112
+
113
+ 1. Build a curated Uttarakhand event list (district/date) and collect before/after pairs.
114
+ 2. Generate DEM derivatives for those AOIs (slope/aspect/curvature).
115
+ 3. Create a labeling protocol (landslide polygon + confidence tier).
116
+ 4. Add benchmark script (precision/recall/F1/IoU per district/event).
117
+ 5. Move from Rule-Based v1 to ML baseline (RF/XGBoost) with reproducible feature table.
118
+
README.md CHANGED
@@ -16,6 +16,7 @@ Standalone web application for satellite image change detection with **user acco
16
  - **Login / Register** — JWT-based auth, passwords hashed with bcrypt
17
  - **Database** — SQLite (or set `DATABASE_URL` for PostgreSQL); stores users and detection runs
18
  - **Change detection** — Same model as the original app: AI-based, image difference, feature-based, hybrid
 
19
  - **Object classification** — Changed regions labeled as Water, Vegetation/Tree, Building, Road, Bare Ground/Soil
20
  - **History** — List of past runs with overlay images and stats
21
  - **UI** — Single-page app with a dark, “control room” style and teal accents
@@ -57,6 +58,12 @@ Standalone web application for satellite image change detection with **user acco
57
  - **JWT**: set `SECRET_KEY` in `app/auth.py` (or via env) in production.
58
  - **Email**: By default, notifications are sent via the manager's email API (`https://emailservice.managemybusinessess.com/api/email/send`). Override with `EMAIL_API_URL` if needed. To use SMTP (e.g. Gmail) instead, set `EMAIL_API_URL` to empty and set `SMTP_USER` and `SMTP_PASS`.
59
 
 
 
 
 
 
 
60
  ## Project layout
61
 
62
  ```
 
16
  - **Login / Register** — JWT-based auth, passwords hashed with bcrypt
17
  - **Database** — SQLite (or set `DATABASE_URL` for PostgreSQL); stores users and detection runs
18
  - **Change detection** — Same model as the original app: AI-based, image difference, feature-based, hybrid
19
+ - **Detection menu** — Choose between General Change Detection and Landslide Detection (Uttarakhand starter)
20
  - **Object classification** — Changed regions labeled as Water, Vegetation/Tree, Building, Road, Bare Ground/Soil
21
  - **History** — List of past runs with overlay images and stats
22
  - **UI** — Single-page app with a dark, “control room” style and teal accents
 
58
  - **JWT**: set `SECRET_KEY` in `app/auth.py` (or via env) in production.
59
  - **Email**: By default, notifications are sent via the manager's email API (`https://emailservice.managemybusinessess.com/api/email/send`). Override with `EMAIL_API_URL` if needed. To use SMTP (e.g. Gmail) instead, set `EMAIL_API_URL` to empty and set `SMTP_USER` and `SMTP_PASS`.
60
 
61
+ - **Landslide module**:
62
+ - Integrated at runtime through the same `/api/detect` endpoint using `detection_type=landslide_detection`.
63
+ - Engine code: `app/landslide_engine.py`
64
+ - Dataset preprocessing starter: `app/landslide_preprocessing.py`
65
+ - Planning/research brief: `Landslide_Detection_Uttarakhand_Integration_Plan.md`
66
+
67
  ## Project layout
68
 
69
  ```
app/landslide_engine.py ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Landslide Detection Engine (Uttarakhand-focused starter).
3
+
4
+ This module is intentionally separate from the generic change detection engine.
5
+ It uses landslide-oriented cues from before/after optical imagery:
6
+ - vegetation loss
7
+ - bare-soil increase
8
+ - texture roughness change
9
+ - edge disruption
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import cv2
14
+ import numpy as np
15
+ from PIL import Image
16
+
17
+
18
+ def _preprocess(image: Image.Image, max_size: int = 2200) -> np.ndarray:
19
+ arr = np.array(image.convert("RGB"))
20
+ h, w = arr.shape[:2]
21
+ if max(h, w) > max_size:
22
+ s = max_size / max(h, w)
23
+ arr = cv2.resize(arr, (max(1, int(w * s)), max(1, int(h * s))), interpolation=cv2.INTER_AREA)
24
+ return arr
25
+
26
+
27
+ def _norm01(x: np.ndarray) -> np.ndarray:
28
+ x = x.astype(np.float32)
29
+ lo = float(np.min(x))
30
+ hi = float(np.max(x))
31
+ if hi - lo < 1e-8:
32
+ return np.zeros_like(x, dtype=np.float32)
33
+ return (x - lo) / (hi - lo)
34
+
35
+
36
+ def _green_index(rgb: np.ndarray) -> np.ndarray:
37
+ # RGB proxy for vegetation index when NIR is unavailable.
38
+ r = rgb[:, :, 0].astype(np.float32)
39
+ g = rgb[:, :, 1].astype(np.float32)
40
+ return (g - r) / (g + r + 1e-6)
41
+
42
+
43
+ def _soil_score(rgb: np.ndarray) -> np.ndarray:
44
+ hsv = cv2.cvtColor(rgb, cv2.COLOR_RGB2HSV).astype(np.float32)
45
+ h = hsv[:, :, 0]
46
+ s = hsv[:, :, 1] / 255.0
47
+ v = hsv[:, :, 2] / 255.0
48
+ # Dry/bare soil often: warm hue, medium saturation, medium/high brightness.
49
+ warm = ((h >= 8) & (h <= 38)).astype(np.float32)
50
+ sat = np.clip(1.0 - np.abs(s - 0.45) / 0.45, 0, 1)
51
+ bri = np.clip((v - 0.25) / 0.75, 0, 1)
52
+ return _norm01(0.5 * warm + 0.25 * sat + 0.25 * bri)
53
+
54
+
55
+ def _texture_roughness(gray: np.ndarray) -> np.ndarray:
56
+ lap = cv2.Laplacian(gray, cv2.CV_32F, ksize=3)
57
+ rough = cv2.GaussianBlur(np.abs(lap), (5, 5), 0)
58
+ return _norm01(rough)
59
+
60
+
61
+ def _edge_change(before: np.ndarray, after: np.ndarray) -> np.ndarray:
62
+ g1 = cv2.cvtColor(before, cv2.COLOR_RGB2GRAY)
63
+ g2 = cv2.cvtColor(after, cv2.COLOR_RGB2GRAY)
64
+ e1 = cv2.Canny(g1, 60, 140)
65
+ e2 = cv2.Canny(g2, 60, 140)
66
+ diff = cv2.absdiff(e1, e2).astype(np.float32) / 255.0
67
+ return cv2.GaussianBlur(diff, (5, 5), 0)
68
+
69
+
70
+ def _clean(mask: np.ndarray) -> np.ndarray:
71
+ m = mask.copy()
72
+ h, w = m.shape[:2]
73
+ b = max(8, int(min(h, w) * 0.01))
74
+ m[:b, :] = 0
75
+ m[-b:, :] = 0
76
+ m[:, :b] = 0
77
+ m[:, -b:] = 0
78
+ m = cv2.medianBlur(m, 5)
79
+ k_open = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
80
+ k_close = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (9, 9))
81
+ m = cv2.morphologyEx(m, cv2.MORPH_OPEN, k_open)
82
+ m = cv2.morphologyEx(m, cv2.MORPH_CLOSE, k_close)
83
+ return m
84
+
85
+
86
+ def _extract_regions(mask: np.ndarray, after: np.ndarray, min_area: int = 350):
87
+ n, labels, stats, cents = cv2.connectedComponentsWithStats(mask, connectivity=8)
88
+ h, w = mask.shape[:2]
89
+ img_area = h * w
90
+ regions = []
91
+ rid = 0
92
+ for i in range(1, n):
93
+ area = int(stats[i, cv2.CC_STAT_AREA])
94
+ if area < min_area:
95
+ continue
96
+ x = int(stats[i, cv2.CC_STAT_LEFT])
97
+ y = int(stats[i, cv2.CC_STAT_TOP])
98
+ bw = int(stats[i, cv2.CC_STAT_WIDTH])
99
+ bh = int(stats[i, cv2.CC_STAT_HEIGHT])
100
+ if bw * bh > img_area * 0.9:
101
+ continue
102
+ cx, cy = cents[i]
103
+ ratio = area / max(1, bw * bh)
104
+ conf = float(np.clip(0.25 + ratio * 0.65, 0.25, 0.95))
105
+ sev = "minor"
106
+ if area / img_area > 0.02:
107
+ sev = "major"
108
+ elif area / img_area > 0.006:
109
+ sev = "moderate"
110
+ rid += 1
111
+ regions.append(
112
+ {
113
+ "id": rid,
114
+ "area": area,
115
+ "bbox": (x, y, bw, bh),
116
+ "center": (int(cx), int(cy)),
117
+ "object_type": "Landslide Suspected Zone",
118
+ "confidence": conf,
119
+ "severity": sev,
120
+ "sub_type": "Debris / Slope Failure",
121
+ "sub_type_confidence": conf,
122
+ "estimated_stories": None,
123
+ "estimated_height_m": None,
124
+ "construction_stage": None,
125
+ }
126
+ )
127
+ return regions[:80]
128
+
129
+
130
+ def _visualize(after: np.ndarray, mask: np.ndarray, regions: list[dict]) -> np.ndarray:
131
+ out = after.copy().astype(np.float32)
132
+ m = (mask > 127).astype(np.float32)
133
+ amber = np.zeros_like(out)
134
+ amber[:, :, 0] = 255 # R
135
+ amber[:, :, 1] = 165 # G
136
+ alpha = 0.35
137
+ for c in range(3):
138
+ out[:, :, c] = out[:, :, c] * (1 - m * alpha) + amber[:, :, c] * (m * alpha)
139
+ vis = np.clip(out, 0, 255).astype(np.uint8)
140
+ for r in regions:
141
+ x, y, w, h = r["bbox"]
142
+ color = (0, 140, 255) # BGR-like style for warning tone in RGB draw context
143
+ cv2.rectangle(vis, (x, y), (x + w, y + h), color, 2)
144
+ label = f'{r["id"]}'
145
+ cv2.putText(vis, label, (x + 4, max(14, y - 4)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1, cv2.LINE_AA)
146
+ return vis
147
+
148
+
149
+ def run_landslide_detection(
150
+ before_pil: Image.Image,
151
+ after_pil: Image.Image,
152
+ model_name: str = "Rule-Based v1",
153
+ detection_sensitivity: float = 0.6,
154
+ min_region_area: int | None = None,
155
+ ):
156
+ """
157
+ Returns: change_mask, result_image, stats, regions.
158
+ """
159
+ before = _preprocess(before_pil)
160
+ after = _preprocess(after_pil)
161
+ if before.shape != after.shape:
162
+ after = cv2.resize(after, (before.shape[1], before.shape[0]), interpolation=cv2.INTER_LINEAR)
163
+
164
+ g_before = _green_index(before)
165
+ g_after = _green_index(after)
166
+ veg_loss = _norm01(np.clip(g_before - g_after, 0, None))
167
+
168
+ soil_before = _soil_score(before)
169
+ soil_after = _soil_score(after)
170
+ soil_gain = _norm01(np.clip(soil_after - soil_before, 0, None))
171
+
172
+ gray_before = cv2.cvtColor(before, cv2.COLOR_RGB2GRAY).astype(np.float32)
173
+ gray_after = cv2.cvtColor(after, cv2.COLOR_RGB2GRAY).astype(np.float32)
174
+ rough_before = _texture_roughness(gray_before)
175
+ rough_after = _texture_roughness(gray_after)
176
+ rough_change = _norm01(np.abs(rough_after - rough_before))
177
+
178
+ edge_change = _edge_change(before, after)
179
+
180
+ sens = float(np.clip(detection_sensitivity, 0.0, 1.0))
181
+ # Landslide-oriented fusion
182
+ fused = (
183
+ 0.38 * veg_loss
184
+ + 0.30 * soil_gain
185
+ + 0.20 * rough_change
186
+ + 0.12 * edge_change
187
+ )
188
+ fused = cv2.GaussianBlur(fused.astype(np.float32), (7, 7), 0)
189
+
190
+ # Higher sensitivity => lower quantile threshold.
191
+ q = float(np.clip(0.965 - (sens - 0.5) * 0.08, 0.88, 0.98))
192
+ thr = float(np.quantile(fused, q))
193
+ mask = (fused >= thr).astype(np.uint8) * 255
194
+ mask = _clean(mask)
195
+
196
+ if min_region_area is None:
197
+ min_region_area = int(max(250, min(1400, mask.shape[0] * mask.shape[1] * 0.00010)))
198
+ regions = _extract_regions(mask, after, min_area=int(min_region_area))
199
+ result = _visualize(after, mask, regions)
200
+
201
+ total = int(mask.shape[0] * mask.shape[1])
202
+ changed = int(np.sum(mask > 127))
203
+ stats = {
204
+ "total_pixels": total,
205
+ "changed_pixels": changed,
206
+ "unchanged_pixels": total - changed,
207
+ "change_percentage": (changed / total * 100.0) if total else 0.0,
208
+ "image_width": mask.shape[1],
209
+ "image_height": mask.shape[0],
210
+ "threshold_debug": {
211
+ "method": f"Landslide Detection ({model_name})",
212
+ "threshold_used": int(np.clip(thr * 255.0, 0, 255)),
213
+ "threshold_percentile_q": q,
214
+ "sensitivity": sens,
215
+ },
216
+ "params": {
217
+ "detection_sensitivity": sens,
218
+ "min_region_area": int(min_region_area),
219
+ "model_name": model_name,
220
+ },
221
+ }
222
+ return mask, result, stats, regions
223
+
app/landslide_preprocessing.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Dataset preprocessing and feature extraction starter for landslide modeling.
3
+
4
+ Usage example:
5
+ python -m app.landslide_preprocessing --pairs_dir data/landslide_pairs --out_csv data/landslide_features.csv
6
+
7
+ Expected pairs_dir structure:
8
+ pairs_dir/
9
+ event_001/
10
+ before.png
11
+ after.png
12
+ label.png # optional (binary mask)
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import csv
18
+ from pathlib import Path
19
+
20
+ import cv2
21
+ import numpy as np
22
+ from PIL import Image
23
+
24
+
25
+ def _norm01(x: np.ndarray) -> np.ndarray:
26
+ x = x.astype(np.float32)
27
+ lo = float(np.min(x))
28
+ hi = float(np.max(x))
29
+ if hi - lo < 1e-8:
30
+ return np.zeros_like(x, dtype=np.float32)
31
+ return (x - lo) / (hi - lo)
32
+
33
+
34
+ def _green_index(rgb: np.ndarray) -> np.ndarray:
35
+ r = rgb[:, :, 0].astype(np.float32)
36
+ g = rgb[:, :, 1].astype(np.float32)
37
+ return (g - r) / (g + r + 1e-6)
38
+
39
+
40
+ def _soil_score(rgb: np.ndarray) -> np.ndarray:
41
+ hsv = cv2.cvtColor(rgb, cv2.COLOR_RGB2HSV).astype(np.float32)
42
+ h = hsv[:, :, 0]
43
+ s = hsv[:, :, 1] / 255.0
44
+ v = hsv[:, :, 2] / 255.0
45
+ warm = ((h >= 8) & (h <= 38)).astype(np.float32)
46
+ sat = np.clip(1.0 - np.abs(s - 0.45) / 0.45, 0, 1)
47
+ bri = np.clip((v - 0.25) / 0.75, 0, 1)
48
+ return _norm01(0.5 * warm + 0.25 * sat + 0.25 * bri)
49
+
50
+
51
+ def _texture(gray: np.ndarray) -> np.ndarray:
52
+ lap = cv2.Laplacian(gray.astype(np.float32), cv2.CV_32F, ksize=3)
53
+ return _norm01(cv2.GaussianBlur(np.abs(lap), (5, 5), 0))
54
+
55
+
56
+ def _chip_stats(chip: np.ndarray) -> tuple[float, float, float]:
57
+ return float(np.mean(chip)), float(np.std(chip)), float(np.quantile(chip, 0.9))
58
+
59
+
60
+ def extract_pair_features(before_rgb: np.ndarray, after_rgb: np.ndarray, chip: int = 64):
61
+ if before_rgb.shape != after_rgb.shape:
62
+ after_rgb = cv2.resize(after_rgb, (before_rgb.shape[1], before_rgb.shape[0]))
63
+
64
+ g_before = _green_index(before_rgb)
65
+ g_after = _green_index(after_rgb)
66
+ veg_loss = _norm01(np.clip(g_before - g_after, 0, None))
67
+
68
+ soil_before = _soil_score(before_rgb)
69
+ soil_after = _soil_score(after_rgb)
70
+ soil_gain = _norm01(np.clip(soil_after - soil_before, 0, None))
71
+
72
+ gray_before = cv2.cvtColor(before_rgb, cv2.COLOR_RGB2GRAY)
73
+ gray_after = cv2.cvtColor(after_rgb, cv2.COLOR_RGB2GRAY)
74
+ tex_before = _texture(gray_before)
75
+ tex_after = _texture(gray_after)
76
+ tex_delta = _norm01(np.abs(tex_after - tex_before))
77
+
78
+ h, w = veg_loss.shape
79
+ rows = []
80
+ for y in range(0, h - chip + 1, chip):
81
+ for x in range(0, w - chip + 1, chip):
82
+ v = veg_loss[y:y + chip, x:x + chip]
83
+ s = soil_gain[y:y + chip, x:x + chip]
84
+ t = tex_delta[y:y + chip, x:x + chip]
85
+ v_m, v_sd, v_q = _chip_stats(v)
86
+ s_m, s_sd, s_q = _chip_stats(s)
87
+ t_m, t_sd, t_q = _chip_stats(t)
88
+ rows.append({
89
+ "x": x, "y": y,
90
+ "veg_loss_mean": v_m, "veg_loss_std": v_sd, "veg_loss_q90": v_q,
91
+ "soil_gain_mean": s_m, "soil_gain_std": s_sd, "soil_gain_q90": s_q,
92
+ "tex_delta_mean": t_m, "tex_delta_std": t_sd, "tex_delta_q90": t_q,
93
+ })
94
+ return rows
95
+
96
+
97
+ def main():
98
+ parser = argparse.ArgumentParser()
99
+ parser.add_argument("--pairs_dir", required=True, help="Directory containing event folders with before/after images.")
100
+ parser.add_argument("--out_csv", required=True, help="Output CSV path.")
101
+ parser.add_argument("--chip", type=int, default=64, help="Chip size for feature aggregation.")
102
+ args = parser.parse_args()
103
+
104
+ pairs_dir = Path(args.pairs_dir)
105
+ out_csv = Path(args.out_csv)
106
+ out_csv.parent.mkdir(parents=True, exist_ok=True)
107
+
108
+ all_rows = []
109
+ for event_dir in sorted([p for p in pairs_dir.iterdir() if p.is_dir()]):
110
+ before_path = event_dir / "before.png"
111
+ after_path = event_dir / "after.png"
112
+ if not before_path.exists() or not after_path.exists():
113
+ continue
114
+ before = np.array(Image.open(before_path).convert("RGB"))
115
+ after = np.array(Image.open(after_path).convert("RGB"))
116
+ rows = extract_pair_features(before, after, chip=args.chip)
117
+ for r in rows:
118
+ r["event_id"] = event_dir.name
119
+ all_rows.extend(rows)
120
+
121
+ if not all_rows:
122
+ print("No valid before/after pairs found.")
123
+ return
124
+
125
+ fieldnames = list(all_rows[0].keys())
126
+ with out_csv.open("w", newline="", encoding="utf-8") as f:
127
+ writer = csv.DictWriter(f, fieldnames=fieldnames)
128
+ writer.writeheader()
129
+ writer.writerows(all_rows)
130
+
131
+ print(f"Wrote {len(all_rows)} rows to {out_csv}")
132
+
133
+
134
+ if __name__ == "__main__":
135
+ main()
136
+
app/main.py CHANGED
@@ -225,6 +225,8 @@ async def detect(
225
  before: UploadFile = File(...),
226
  after: UploadFile = File(...),
227
  method: str = Form("AI-Based Deep Learning"),
 
 
228
  title: str = Form("Untitled run"),
229
  zone: str = Form(""),
230
  village: str = Form(""),
@@ -265,16 +267,29 @@ async def detect(
265
  if min_region_area is not None:
266
  min_region_area = int(max(50, min(10000, min_region_area)))
267
 
268
- from .detection_engine import run_detection
269
- change_mask, result_image, stats, change_regions = run_detection(
270
- before_pil,
271
- after_pil,
272
- method=method,
273
- enable_registration=enable_registration,
274
- enable_normalization=enable_normalization,
275
- detection_sensitivity=detection_sensitivity,
276
- min_region_area=min_region_area,
277
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  # Save overlay and thumbnails for history table view
279
  base_name = f"{user.id}_{uuid.uuid4().hex}"
280
  overlay_filename = base_name + ".png"
@@ -375,6 +390,7 @@ async def detect(
375
  "id": run.id,
376
  "title": run.title,
377
  "method": run.method,
 
378
  "zone": run.zone or "",
379
  "village": run.village or "",
380
  "statistics": {
 
225
  before: UploadFile = File(...),
226
  after: UploadFile = File(...),
227
  method: str = Form("AI-Based Deep Learning"),
228
+ detection_type: str = Form("change_detection"),
229
+ landslide_model: str = Form("Rule-Based v1"),
230
  title: str = Form("Untitled run"),
231
  zone: str = Form(""),
232
  village: str = Form(""),
 
267
  if min_region_area is not None:
268
  min_region_area = int(max(50, min(10000, min_region_area)))
269
 
270
+ detection_type = (detection_type or "change_detection").strip().lower()
271
+ if detection_type == "landslide_detection":
272
+ from .landslide_engine import run_landslide_detection
273
+ method = f"Landslide - {landslide_model}"
274
+ change_mask, result_image, stats, change_regions = run_landslide_detection(
275
+ before_pil,
276
+ after_pil,
277
+ model_name=landslide_model,
278
+ detection_sensitivity=detection_sensitivity,
279
+ min_region_area=min_region_area,
280
+ )
281
+ else:
282
+ detection_type = "change_detection"
283
+ from .detection_engine import run_detection
284
+ change_mask, result_image, stats, change_regions = run_detection(
285
+ before_pil,
286
+ after_pil,
287
+ method=method,
288
+ enable_registration=enable_registration,
289
+ enable_normalization=enable_normalization,
290
+ detection_sensitivity=detection_sensitivity,
291
+ min_region_area=min_region_area,
292
+ )
293
  # Save overlay and thumbnails for history table view
294
  base_name = f"{user.id}_{uuid.uuid4().hex}"
295
  overlay_filename = base_name + ".png"
 
390
  "id": run.id,
391
  "title": run.title,
392
  "method": run.method,
393
+ "detectionType": detection_type,
394
  "zone": run.zone or "",
395
  "village": run.village or "",
396
  "statistics": {
static/js/app.js CHANGED
@@ -186,6 +186,27 @@ function setupUploadZone(inputId, nameId, zoneId, previewId) {
186
  setupUploadZone('file-before', 'name-before', 'zone-before', 'preview-before');
187
  setupUploadZone('file-after', 'name-after', 'zone-after', 'preview-after');
188
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  // ---- Delhi Zone → Village cascading dropdowns ----
190
  const DELHI_ZONES = {
191
  "Central Delhi": [
@@ -410,7 +431,12 @@ document.getElementById('form-detect')?.addEventListener('submit', async (e) =>
410
  const form = new FormData();
411
  form.append('before', before);
412
  form.append('after', after);
 
 
413
  form.append('method', document.getElementById('detect-method').value);
 
 
 
414
  form.append('title', document.getElementById('detect-title').value || 'Untitled run');
415
  form.append('zone', document.getElementById('detect-zone').value || '');
416
  form.append('village', document.getElementById('detect-village').value || '');
 
186
  setupUploadZone('file-before', 'name-before', 'zone-before', 'preview-before');
187
  setupUploadZone('file-after', 'name-after', 'zone-after', 'preview-after');
188
 
189
+ // ---- Detection menu (General vs Landslide) ----
190
+ (function initDetectionMenu() {
191
+ const typeSel = document.getElementById('detect-type');
192
+ const landslideGroup = document.getElementById('landslide-model-group');
193
+ const methodGroup = document.getElementById('detect-method')?.closest('.form-group');
194
+ const regGroup = document.getElementById('detect-registration')?.closest('.form-group');
195
+ const normGroup = document.getElementById('detect-normalization')?.closest('.form-group');
196
+ if (!typeSel) return;
197
+
198
+ function refresh() {
199
+ const isLandslide = typeSel.value === 'landslide_detection';
200
+ if (landslideGroup) landslideGroup.classList.toggle('hidden', !isLandslide);
201
+ if (methodGroup) methodGroup.classList.toggle('hidden', isLandslide);
202
+ if (regGroup) regGroup.classList.toggle('hidden', isLandslide);
203
+ if (normGroup) normGroup.classList.toggle('hidden', isLandslide);
204
+ }
205
+
206
+ typeSel.addEventListener('change', refresh);
207
+ refresh();
208
+ })();
209
+
210
  // ---- Delhi Zone → Village cascading dropdowns ----
211
  const DELHI_ZONES = {
212
  "Central Delhi": [
 
431
  const form = new FormData();
432
  form.append('before', before);
433
  form.append('after', after);
434
+ const detectionType = document.getElementById('detect-type')?.value || 'change_detection';
435
+ form.append('detection_type', detectionType);
436
  form.append('method', document.getElementById('detect-method').value);
437
+ if (detectionType === 'landslide_detection') {
438
+ form.append('landslide_model', document.getElementById('landslide-model')?.value || 'Rule-Based v1');
439
+ }
440
  form.append('title', document.getElementById('detect-title').value || 'Untitled run');
441
  form.append('zone', document.getElementById('detect-zone').value || '');
442
  form.append('village', document.getElementById('detect-village').value || '');
templates/index.html CHANGED
@@ -158,6 +158,21 @@
158
  <h3>Upload / Detection</h3>
159
  </div>
160
  <form id="form-detect">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  <div class="location-row">
162
  <div class="form-group">
163
  <label for="detect-zone">
@@ -360,6 +375,6 @@
360
  </div>
361
  </div>
362
 
363
- <script src="/static/js/app.js?v=25"></script>
364
  </body>
365
  </html>
 
158
  <h3>Upload / Detection</h3>
159
  </div>
160
  <form id="form-detect">
161
+ <div class="options-row">
162
+ <div class="form-group">
163
+ <label for="detect-type">Detection Menu</label>
164
+ <select id="detect-type">
165
+ <option value="change_detection" selected>General Change Detection</option>
166
+ <option value="landslide_detection">Landslide Detection (Uttarakhand)</option>
167
+ </select>
168
+ </div>
169
+ <div class="form-group hidden" id="landslide-model-group">
170
+ <label for="landslide-model">Landslide Model</label>
171
+ <select id="landslide-model">
172
+ <option value="Rule-Based v1" selected>Rule-Based v1 (Vegetation Loss + Soil Gain)</option>
173
+ </select>
174
+ </div>
175
+ </div>
176
  <div class="location-row">
177
  <div class="form-group">
178
  <label for="detect-zone">
 
375
  </div>
376
  </div>
377
 
378
+ <script src="/static/js/app.js?v=26"></script>
379
  </body>
380
  </html>