coderuday21 commited on
Commit
7e52c2b
·
1 Parent(s): 466c163

Revert to satellite change detection only - remove landslide, pothole, detection type menu

Browse files
Dockerfile CHANGED
@@ -3,13 +3,6 @@ FROM python:3.11-slim
3
  # Ensure build logs flush immediately (helps when HF shows “BUILDING” with no output)
4
  ENV PYTHONUNBUFFERED=1
5
 
6
- # Hugging Face Hub cache:
7
- # Some Spaces build steps scan/download using the local Hugging Face cache.
8
- # In containers this cache can be missing/unwritable unless we force it.
9
- ENV HF_HOME=/tmp/hf
10
- ENV HF_HUB_CACHE=/tmp/hf/hub
11
- ENV TRANSFORMERS_CACHE=/tmp/hf/transformers
12
-
13
  # System dependencies for OpenCV and image processing
14
  RUN apt-get update && apt-get install -y --no-install-recommends \
15
  libgl1 \
@@ -26,7 +19,7 @@ WORKDIR /app
26
 
27
  # Build-time info + cache-bust:
28
  # Changing APP_BUILD forces Docker to re-run subsequent layers (including pip install).
29
- ARG APP_BUILD=11
30
  ENV APP_BUILD=${APP_BUILD}
31
  RUN echo "Docker build start: APP_BUILD=${APP_BUILD}" && python -V
32
 
 
3
  # Ensure build logs flush immediately (helps when HF shows “BUILDING” with no output)
4
  ENV PYTHONUNBUFFERED=1
5
 
 
 
 
 
 
 
 
6
  # System dependencies for OpenCV and image processing
7
  RUN apt-get update && apt-get install -y --no-install-recommends \
8
  libgl1 \
 
19
 
20
  # Build-time info + cache-bust:
21
  # Changing APP_BUILD forces Docker to re-run subsequent layers (including pip install).
22
+ ARG APP_BUILD=12
23
  ENV APP_BUILD=${APP_BUILD}
24
  RUN echo "Docker build start: APP_BUILD=${APP_BUILD}" && python -V
25
 
Landslide_Detection_Uttarakhand_Integration_Plan.md DELETED
@@ -1,118 +0,0 @@
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
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Pothole_Detection_Integration_Plan.md DELETED
@@ -1,92 +0,0 @@
1
- # Pothole Detection Integration (Research + Architecture)
2
-
3
- ## 1) Computer vision approaches for road damage detection
4
-
5
- Typical successful families:
6
-
7
- - **Object detection (bounding boxes)**: YOLOv5/YOLOv8, Faster R-CNN
8
- - Pros: simple outputs, fast, easy UI.
9
- - Cons: boxes are coarse; struggles with thin cracks and complex shapes.
10
-
11
- - **Instance segmentation**: Mask R-CNN, YOLACT
12
- - Pros: tighter region boundary and size estimation.
13
- - Cons: heavier models, more training complexity.
14
-
15
- - **Semantic segmentation**: U-Net / DeepLabv3+ / SegFormer
16
- - Pros: best for pixel-level damage maps, severity estimation.
17
- - Cons: needs mask labels; inference cost.
18
-
19
- - **Two-stage pipelines**:
20
- 1) road surface ROI extraction (segment road), then
21
- 2) damage detection inside road only
22
- - Pros: reduces false positives (buildings, shadows, non-road textures).
23
-
24
- ## 2) Datasets and pretrained models (starting points)
25
-
26
- Common public datasets (road damage + potholes):
27
-
28
- - **RDD (Road Damage Dataset / Road Damage Detection)**
29
- Includes potholes and other damage classes from multiple countries.
30
-
31
- - **Pothole-600 / Pothole datasets on Kaggle**
32
- Smaller but useful for prototyping.
33
-
34
- - **CrackForest / CFD / other crack datasets**
35
- More focused on cracks; can help pretraining for surface defects.
36
-
37
- Practical approach:
38
- - Use a model pretrained on COCO, then fine-tune on RDD/pothole datasets.
39
- - For best results, fine-tune on **your region-specific imagery** (road texture and lighting differs).
40
-
41
- ## 3) Feasibility: drone vs satellite vs vehicle camera
42
-
43
- - **Vehicle camera (recommended)**:
44
- - Highest feasibility for potholes.
45
- - Typical resolution and perspective supports pothole features.
46
-
47
- - **Drone (good)**:
48
- - Works well at low altitude with good GSD (cm/px).
49
- - Requires flight plan and stable capture.
50
-
51
- - **Satellite (usually not feasible)**:
52
- - Most satellite imagery is too low resolution for potholes.
53
- - Only very high-res (sub-10cm) commercial imagery could work, and still hard due to shadows and angle.
54
-
55
- ## 4) Detection pipeline (integrated with current system)
56
-
57
- Implemented integration strategy:
58
-
59
- - Add **Pothole Detection** as another detection type in the menu.
60
- - Route through the existing `POST /api/detect` using:
61
- - `detection_type=pothole_detection`
62
- - `pothole_model=<selected>`
63
- - Separate engine module:
64
- - `app/pothole_engine.py`
65
-
66
- ### Starter logic implemented (Rule-Based v1)
67
-
68
- For fast CPU MVP (vehicle/drone imagery):
69
- - shadow/dark region score (local brightness drop)
70
- - rough texture score (Laplacian roughness)
71
- - edge score (Canny)
72
- - fuse + sensitivity percentile threshold
73
- - region extraction + severity/confidence
74
-
75
- ## 5) Model architectures to implement next
76
-
77
- - **YOLOv8n (boxes)** for fast detection and scalable deployment.
78
- - **SegFormer-b0 / U-Net** for pixel-level damage mapping.
79
- - Optional road ROI segmentation first to reduce false positives.
80
-
81
- ## 6) Immediate next steps for dataset preprocessing / feature extraction
82
-
83
- 1. Define input standard: camera height, FOV, resolution, and capture protocol.
84
- 2. Build a labeled dataset:
85
- - bounding boxes or masks for potholes,
86
- - metadata: day/night, wet/dry, shadows.
87
- 3. Add preprocessing:
88
- - road ROI extraction,
89
- - illumination normalization,
90
- - motion blur handling.
91
- 4. Train baseline YOLO model and integrate as `pothole_model=YOLOv8`.
92
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/landslide_engine.py DELETED
@@ -1,223 +0,0 @@
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 DELETED
@@ -1,136 +0,0 @@
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
@@ -222,12 +222,9 @@ def me(user: Optional[User] = Depends(get_current_user)):
222
  @app.post("/api/detect")
223
  async def detect(
224
  request: Request,
225
- before: Optional[UploadFile] = File(None),
226
- after: Optional[UploadFile] = File(None),
227
  method: str = Form("AI-Based Deep Learning"),
228
- detection_type: str = Form("change_detection"),
229
- landslide_model: str = Form("Rule-Based v1"),
230
- pothole_model: str = Form("Rule-Based v1"),
231
  title: str = Form("Untitled run"),
232
  zone: str = Form(""),
233
  village: str = Form(""),
@@ -252,11 +249,8 @@ async def detect(
252
  if not user:
253
  raise HTTPException(status_code=401, detail="Login required")
254
  MAX_UPLOAD_BYTES = 20 * 1024 * 1024 # 20 MB
255
- detection_type = (detection_type or "change_detection").strip().lower()
256
 
257
- def _read_upload(upload: Optional[UploadFile], field_name: str):
258
- if upload is None:
259
- raise HTTPException(status_code=400, detail=f"{field_name} image is required")
260
  raw = None
261
  try:
262
  raw = upload.file.read()
@@ -276,50 +270,22 @@ async def detect(
276
  except Exception:
277
  pass
278
 
279
- if detection_type == "pothole_detection":
280
- # Single-image mode: use after if present, else before.
281
- primary = after if after is not None else before
282
- after_pil = _read_upload(primary, "road")
283
- before_pil = after_pil
284
- else:
285
- before_pil = _read_upload(before, "before")
286
- after_pil = _read_upload(after, "after")
287
  detection_sensitivity = max(0.0, min(1.0, float(detection_sensitivity)))
288
  if min_region_area is not None:
289
  min_region_area = int(max(50, min(10000, min_region_area)))
290
 
291
- if detection_type == "landslide_detection":
292
- from .landslide_engine import run_landslide_detection
293
- method = f"Landslide - {landslide_model}"
294
- change_mask, result_image, stats, change_regions = run_landslide_detection(
295
- before_pil,
296
- after_pil,
297
- model_name=landslide_model,
298
- detection_sensitivity=detection_sensitivity,
299
- min_region_area=min_region_area,
300
- )
301
- elif detection_type == "pothole_detection":
302
- from .pothole_engine import run_pothole_detection
303
- method = f"Pothole - {pothole_model}"
304
- change_mask, result_image, stats, change_regions = run_pothole_detection(
305
- before_pil,
306
- after_pil,
307
- model_name=pothole_model,
308
- detection_sensitivity=detection_sensitivity,
309
- min_region_area=min_region_area,
310
- )
311
- else:
312
- detection_type = "change_detection"
313
- from .detection_engine import run_detection
314
- change_mask, result_image, stats, change_regions = run_detection(
315
- before_pil,
316
- after_pil,
317
- method=method,
318
- enable_registration=enable_registration,
319
- enable_normalization=enable_normalization,
320
- detection_sensitivity=detection_sensitivity,
321
- min_region_area=min_region_area,
322
- )
323
  # Save overlay and thumbnails for history table view
324
  base_name = f"{user.id}_{uuid.uuid4().hex}"
325
  overlay_filename = base_name + ".png"
@@ -420,7 +386,6 @@ async def detect(
420
  "id": run.id,
421
  "title": run.title,
422
  "method": run.method,
423
- "detectionType": detection_type,
424
  "zone": run.zone or "",
425
  "village": run.village or "",
426
  "statistics": {
@@ -594,20 +559,3 @@ def index():
594
  if not index_file.exists():
595
  return HTMLResponse("<h1>Satellite Change Detection</h1><p>Create <code>templates/index.html</code> and <code>static/</code>.</p>")
596
  return FileResponse(index_file)
597
-
598
-
599
- # --- Detection type landing pages ---
600
- # These serve the same SPA, but the frontend selects the correct mode based on URL.
601
- @app.get("/detect/change", response_class=HTMLResponse)
602
- def detect_change_page():
603
- return index()
604
-
605
-
606
- @app.get("/detect/landslide", response_class=HTMLResponse)
607
- def detect_landslide_page():
608
- return index()
609
-
610
-
611
- @app.get("/detect/pothole", response_class=HTMLResponse)
612
- def detect_pothole_page():
613
- return index()
 
222
  @app.post("/api/detect")
223
  async def detect(
224
  request: Request,
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(""),
 
249
  if not user:
250
  raise HTTPException(status_code=401, detail="Login required")
251
  MAX_UPLOAD_BYTES = 20 * 1024 * 1024 # 20 MB
 
252
 
253
+ def _read_upload(upload: UploadFile, field_name: str):
 
 
254
  raw = None
255
  try:
256
  raw = upload.file.read()
 
270
  except Exception:
271
  pass
272
 
273
+ before_pil = _read_upload(before, "before")
274
+ after_pil = _read_upload(after, "after")
 
 
 
 
 
 
275
  detection_sensitivity = max(0.0, min(1.0, float(detection_sensitivity)))
276
  if min_region_area is not None:
277
  min_region_area = int(max(50, min(10000, min_region_area)))
278
 
279
+ from .detection_engine import run_detection
280
+ change_mask, result_image, stats, change_regions = run_detection(
281
+ before_pil,
282
+ after_pil,
283
+ method=method,
284
+ enable_registration=enable_registration,
285
+ enable_normalization=enable_normalization,
286
+ detection_sensitivity=detection_sensitivity,
287
+ min_region_area=min_region_area,
288
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  # Save overlay and thumbnails for history table view
290
  base_name = f"{user.id}_{uuid.uuid4().hex}"
291
  overlay_filename = base_name + ".png"
 
386
  "id": run.id,
387
  "title": run.title,
388
  "method": run.method,
 
389
  "zone": run.zone or "",
390
  "village": run.village or "",
391
  "statistics": {
 
559
  if not index_file.exists():
560
  return HTMLResponse("<h1>Satellite Change Detection</h1><p>Create <code>templates/index.html</code> and <code>static/</code>.</p>")
561
  return FileResponse(index_file)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/pothole_detection/__init__.py DELETED
@@ -1,2 +0,0 @@
1
- from .pothole_detector import PotholeDetector
2
-
 
 
 
app/pothole_detection/inference.py DELETED
@@ -1,52 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import List, Dict
4
-
5
- import numpy as np
6
-
7
-
8
- def run_pothole_inference(
9
- model,
10
- image_bgr: np.ndarray,
11
- conf_threshold: float = 0.25,
12
- iou_threshold: float = 0.45,
13
- ) -> List[Dict]:
14
- """
15
- Run YOLO inference and normalize predictions to a simple list format.
16
- """
17
- results = model.predict(
18
- source=image_bgr,
19
- conf=conf_threshold,
20
- iou=iou_threshold,
21
- verbose=False,
22
- )
23
- preds: List[Dict] = []
24
- if not results:
25
- return preds
26
-
27
- r = results[0]
28
- names = getattr(r, "names", {}) or {}
29
- boxes = getattr(r, "boxes", None)
30
- if boxes is None:
31
- return preds
32
-
33
- xyxy = boxes.xyxy.cpu().numpy() if hasattr(boxes.xyxy, "cpu") else boxes.xyxy
34
- confs = boxes.conf.cpu().numpy() if hasattr(boxes.conf, "cpu") else boxes.conf
35
- clss = boxes.cls.cpu().numpy() if hasattr(boxes.cls, "cpu") else boxes.cls
36
-
37
- for i in range(len(xyxy)):
38
- x1, y1, x2, y2 = [int(v) for v in xyxy[i]]
39
- confidence = float(confs[i])
40
- cls_id = int(clss[i]) if clss is not None else 0
41
- cls_name = names.get(cls_id, "pothole")
42
- preds.append(
43
- {
44
- "bbox": [x1, y1, x2, y2],
45
- "confidence": confidence,
46
- "class_id": cls_id,
47
- "class_name": str(cls_name),
48
- }
49
- )
50
-
51
- return preds
52
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/pothole_detection/model_loader.py DELETED
@@ -1,18 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import os
4
- from functools import lru_cache
5
-
6
-
7
- @lru_cache(maxsize=1)
8
- def get_yolo_model():
9
- """
10
- Lazy-load Ultralytics YOLO model once per process.
11
-
12
- Env:
13
- - POTHOLE_MODEL_PATH: local path or model name (default: yolov8n.pt)
14
- """
15
- model_path = os.environ.get("POTHOLE_MODEL_PATH", "yolov8n.pt").strip() or "yolov8n.pt"
16
- from ultralytics import YOLO
17
- return YOLO(model_path)
18
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/pothole_detection/pothole_detector.py DELETED
@@ -1,52 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Dict, Any, List
4
-
5
- import cv2
6
- import numpy as np
7
-
8
- from .model_loader import get_yolo_model
9
- from .inference import run_pothole_inference
10
- from .visualization import draw_pothole_boxes
11
-
12
-
13
- class PotholeDetector:
14
- """
15
- Modular pothole detector:
16
- - preprocessing
17
- - model inference
18
- - post-processing
19
- - visualization
20
- """
21
-
22
- def __init__(self, conf_threshold: float = 0.25, iou_threshold: float = 0.45):
23
- self.conf_threshold = float(conf_threshold)
24
- self.iou_threshold = float(iou_threshold)
25
- self.model = get_yolo_model()
26
-
27
- def preprocess(self, image_bgr: np.ndarray) -> np.ndarray:
28
- # Lightweight denoise for road textures
29
- return cv2.bilateralFilter(image_bgr, 5, 35, 35)
30
-
31
- def infer(self, image_bgr: np.ndarray) -> List[Dict[str, Any]]:
32
- return run_pothole_inference(
33
- self.model,
34
- image_bgr,
35
- conf_threshold=self.conf_threshold,
36
- iou_threshold=self.iou_threshold,
37
- )
38
-
39
- def postprocess(self, detections: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
40
- # Keep all detections; custom filtering can be added here.
41
- return detections
42
-
43
- def visualize(self, image_bgr: np.ndarray, detections: List[Dict[str, Any]]) -> np.ndarray:
44
- return draw_pothole_boxes(image_bgr, detections)
45
-
46
- def run(self, image_bgr: np.ndarray):
47
- prep = self.preprocess(image_bgr)
48
- detections = self.infer(prep)
49
- detections = self.postprocess(detections)
50
- vis = self.visualize(image_bgr, detections)
51
- return detections, vis
52
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/pothole_detection/visualization.py DELETED
@@ -1,28 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import List, Dict
4
-
5
- import cv2
6
- import numpy as np
7
-
8
-
9
- def draw_pothole_boxes(image_bgr: np.ndarray, detections: List[Dict]) -> np.ndarray:
10
- """
11
- Draw red bounding boxes with confidence labels.
12
- """
13
- out = image_bgr.copy()
14
- for det in detections:
15
- x1, y1, x2, y2 = det["bbox"]
16
- conf = float(det.get("confidence", 0.0))
17
- label = f"pothole {conf:.2f}"
18
-
19
- # Red box (BGR)
20
- cv2.rectangle(out, (x1, y1), (x2, y2), (0, 0, 255), 2)
21
-
22
- # Label background
23
- (tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.55, 1)
24
- y_text = max(16, y1 - 6)
25
- cv2.rectangle(out, (x1, y_text - th - 6), (x1 + tw + 8, y_text + 2), (0, 0, 255), -1)
26
- cv2.putText(out, label, (x1 + 4, y_text - 2), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (255, 255, 255), 1, cv2.LINE_AA)
27
- return out
28
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/pothole_engine.py DELETED
@@ -1,119 +0,0 @@
1
- """
2
- Pothole / road damage detection engine (YOLO-ready).
3
-
4
- Uses modular pipeline under app/pothole_detection:
5
- - model_loader.py
6
- - inference.py
7
- - visualization.py
8
- - pothole_detector.py
9
- """
10
- from __future__ import annotations
11
-
12
- import numpy as np
13
- from PIL import Image
14
- import cv2
15
-
16
- from .pothole_detection import PotholeDetector
17
-
18
-
19
- def _preprocess(image: Image.Image, max_size: int = 1600) -> np.ndarray:
20
- arr = np.array(image.convert("RGB"))
21
- h, w = arr.shape[:2]
22
- if max(h, w) > max_size:
23
- s = max_size / max(h, w)
24
- arr = cv2.resize(arr, (max(1, int(w * s)), max(1, int(h * s))), interpolation=cv2.INTER_AREA)
25
- return arr
26
-
27
-
28
- def _norm01(x: np.ndarray) -> np.ndarray:
29
- x = x.astype(np.float32)
30
- lo = float(np.min(x))
31
- hi = float(np.max(x))
32
- if hi - lo < 1e-8:
33
- return np.zeros_like(x, dtype=np.float32)
34
- return (x - lo) / (hi - lo)
35
-
36
-
37
- def run_pothole_detection(
38
- before_pil: Image.Image,
39
- after_pil: Image.Image,
40
- model_name: str = "Rule-Based v1",
41
- detection_sensitivity: float = 0.6,
42
- min_region_area: int | None = None,
43
- ):
44
- """
45
- Current UI uses (before, after) upload. For potholes, we treat the provided road
46
- image as the target and run YOLO-style detection.
47
- """
48
- img = _preprocess(after_pil)
49
- # Ultralytics model expects BGR ndarray from OpenCV style pipeline.
50
- bgr = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
51
-
52
- # Sensitivity maps to confidence threshold inversely.
53
- sens = float(np.clip(detection_sensitivity, 0.0, 1.0))
54
- conf_thr = float(np.clip(0.45 - (sens - 0.5) * 0.35, 0.10, 0.70))
55
- iou_thr = 0.45
56
- detector = PotholeDetector(conf_threshold=conf_thr, iou_threshold=iou_thr)
57
- detections, vis_bgr = detector.run(bgr)
58
- result = cv2.cvtColor(vis_bgr, cv2.COLOR_BGR2RGB)
59
-
60
- regions = []
61
- rid = 0
62
- for d in detections:
63
- x1, y1, x2, y2 = d["bbox"]
64
- w = max(1, x2 - x1)
65
- h = max(1, y2 - y1)
66
- area = int(w * h)
67
- if min_region_area is not None and area < int(min_region_area):
68
- continue
69
- rid += 1
70
- conf = float(d.get("confidence", 0.0))
71
- severity = "minor"
72
- area_ratio = area / max(1, img.shape[0] * img.shape[1])
73
- if area_ratio > 0.01:
74
- severity = "major"
75
- elif area_ratio > 0.003:
76
- severity = "moderate"
77
- regions.append(
78
- {
79
- "id": rid,
80
- "area": area,
81
- "bbox": (int(x1), int(y1), int(w), int(h)),
82
- "center": (int(x1 + w // 2), int(y1 + h // 2)),
83
- "object_type": "Pothole / Road Damage",
84
- "confidence": conf,
85
- "severity": severity,
86
- "sub_type": str(d.get("class_name", "pothole")),
87
- "sub_type_confidence": conf,
88
- "estimated_stories": None,
89
- "estimated_height_m": None,
90
- "construction_stage": None,
91
- }
92
- )
93
-
94
- total = int(img.shape[0] * img.shape[1])
95
- changed = int(sum(r["area"] for r in regions))
96
- stats = {
97
- "total_pixels": total,
98
- "changed_pixels": changed,
99
- "unchanged_pixels": total - changed,
100
- "change_percentage": (changed / total * 100.0) if total else 0.0,
101
- "image_width": img.shape[1],
102
- "image_height": img.shape[0],
103
- "threshold_debug": {
104
- "method": f"Pothole Detection ({model_name})",
105
- "threshold_used": None,
106
- "confidence_threshold": conf_thr,
107
- "iou_threshold": iou_thr,
108
- "sensitivity": sens,
109
- "detected_boxes": len(regions),
110
- },
111
- "params": {
112
- "detection_sensitivity": sens,
113
- "min_region_area": int(min_region_area) if min_region_area is not None else None,
114
- "model_name": model_name,
115
- "input": "after_only",
116
- },
117
- }
118
- return mask, result, stats, regions
119
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -10,4 +10,3 @@ numpy>=1.24.0
10
  opencv-python-headless>=4.8.0
11
  scikit-learn>=1.3.0
12
  requests>=2.28.0
13
- ultralytics>=8.2.0
 
10
  opencv-python-headless>=4.8.0
11
  scikit-learn>=1.3.0
12
  requests>=2.28.0
 
static/js/app.js CHANGED
@@ -12,49 +12,6 @@ function showView(id) {
12
  if (el) el.classList.add('active');
13
  }
14
 
15
- function getDetectionTypeFromPath() {
16
- const p = (window.location.pathname || '').toLowerCase();
17
- if (p.includes('/detect/landslide')) return 'landslide_detection';
18
- if (p.includes('/detect/pothole')) return 'pothole_detection';
19
- if (p.includes('/detect/change')) return 'change_detection';
20
- return null;
21
- }
22
-
23
- function applyDetectionTypeToUI(type) {
24
- const typeSel = document.getElementById('detect-type');
25
- if (!typeSel || !type) return;
26
- typeSel.value = type;
27
- typeSel.dispatchEvent(new Event('change'));
28
- }
29
-
30
- function pathForDetectionType(type) {
31
- if (type === 'landslide_detection') return '/detect/landslide';
32
- if (type === 'pothole_detection') return '/detect/pothole';
33
- return '/detect/change';
34
- }
35
-
36
- function navigateToDetectionType(type, replace = false) {
37
- applyDetectionTypeToUI(type);
38
- const targetPath = pathForDetectionType(type);
39
- if ((window.location.pathname || '') !== targetPath) {
40
- const fn = replace ? 'replaceState' : 'pushState';
41
- window.history[fn]({}, '', targetPath);
42
- }
43
- showView('dashboard');
44
- loadHistory();
45
- }
46
-
47
- // ---- Detection type selection buttons ----
48
- document.getElementById('btn-type-change')?.addEventListener('click', () => {
49
- navigateToDetectionType('change_detection');
50
- });
51
- document.getElementById('btn-type-landslide')?.addEventListener('click', () => {
52
- navigateToDetectionType('landslide_detection');
53
- });
54
- document.getElementById('btn-type-pothole')?.addEventListener('click', () => {
55
- navigateToDetectionType('pothole_detection');
56
- });
57
-
58
  function showError(id, msg) {
59
  const el = document.getElementById(id);
60
  if (!el) return;
@@ -121,9 +78,8 @@ document.getElementById('form-register')?.addEventListener('submit', async (e) =
121
  });
122
 
123
  function handlePostAuthNavigation() {
124
- const preferred = getDetectionTypeFromPath();
125
- if (preferred) applyDetectionTypeToUI(preferred);
126
- showView('detection-type');
127
  }
128
 
129
  // ---- Forgot password ----
@@ -187,13 +143,8 @@ async function init() {
187
  try {
188
  const user = await api('GET', '/api/me');
189
  document.getElementById('user-email').textContent = user.email;
190
- // Always show the selection menu first.
191
- // If the user already landed on /detect/change or /detect/landslide, we
192
- // pre-select the corresponding detection type in the dropdown for convenience,
193
- // but we still show the menu page before redirecting.
194
- const preferred = getDetectionTypeFromPath();
195
- if (preferred) applyDetectionTypeToUI(preferred);
196
- showView('detection-type');
197
  } catch (_) { setToken(null); showView('login'); }
198
  }
199
 
@@ -238,42 +189,6 @@ function setupUploadZone(inputId, nameId, zoneId, previewId) {
238
  setupUploadZone('file-before', 'name-before', 'zone-before', 'preview-before');
239
  setupUploadZone('file-after', 'name-after', 'zone-after', 'preview-after');
240
 
241
- // ---- Detection menu (General vs Landslide vs Pothole) ----
242
- (function initDetectionMenu() {
243
- const typeSel = document.getElementById('detect-type');
244
- const landslideGroup = document.getElementById('landslide-model-group');
245
- const potholeGroup = document.getElementById('pothole-model-group');
246
- const methodGroup = document.getElementById('detect-method')?.closest('.form-group');
247
- const regGroup = document.getElementById('detect-registration')?.closest('.form-group');
248
- const normGroup = document.getElementById('detect-normalization')?.closest('.form-group');
249
- if (!typeSel) return;
250
-
251
- function refresh() {
252
- const isLandslide = typeSel.value === 'landslide_detection';
253
- const isPothole = typeSel.value === 'pothole_detection';
254
- const beforeZone = document.getElementById('zone-before');
255
- const beforeInput = document.getElementById('file-before');
256
- const beforeName = document.getElementById('name-before');
257
- if (landslideGroup) landslideGroup.classList.toggle('hidden', !isLandslide);
258
- if (potholeGroup) potholeGroup.classList.toggle('hidden', !isPothole);
259
- const hideCore = isLandslide || isPothole;
260
- if (methodGroup) methodGroup.classList.toggle('hidden', hideCore);
261
- if (regGroup) regGroup.classList.toggle('hidden', hideCore);
262
- if (normGroup) normGroup.classList.toggle('hidden', hideCore);
263
- // Pothole mode uses a single image upload (after image).
264
- if (beforeZone) beforeZone.classList.toggle('hidden', isPothole);
265
- if (isPothole && beforeInput) {
266
- beforeInput.value = '';
267
- if (beforeName) beforeName.textContent = 'No file chosen';
268
- const prev = document.getElementById('preview-before');
269
- if (prev) prev.classList.add('hidden');
270
- }
271
- }
272
-
273
- typeSel.addEventListener('change', refresh);
274
- refresh();
275
- })();
276
-
277
  // ---- Delhi Zone → Village cascading dropdowns ----
278
  const DELHI_ZONES = {
279
  "Central Delhi": [
@@ -481,15 +396,9 @@ function stopDetectionProgress(success) {
481
  document.getElementById('form-detect')?.addEventListener('submit', async (e) => {
482
  e.preventDefault();
483
  hideError('dashboard-error');
484
- const detectionType = document.getElementById('detect-type')?.value || 'change_detection';
485
  const before = document.getElementById('file-before').files?.[0];
486
  const after = document.getElementById('file-after').files?.[0];
487
- if (detectionType === 'pothole_detection') {
488
- if (!after && !before) {
489
- showError('dashboard-error', 'Please upload one road image for pothole detection.');
490
- return;
491
- }
492
- } else if (!before || !after) {
493
  showError('dashboard-error', 'Please select both before and after images.');
494
  return;
495
  }
@@ -498,20 +407,13 @@ document.getElementById('form-detect')?.addEventListener('submit', async (e) =>
498
  const loading = document.getElementById('run-loading');
499
  btn.disabled = true;
500
  loading.classList.remove('hidden');
501
- startDetectionProgress();
502
 
503
  const token = getToken();
504
  const form = new FormData();
505
- if (before) form.append('before', before);
506
- if (after) form.append('after', after);
507
- form.append('detection_type', detectionType);
508
  form.append('method', document.getElementById('detect-method').value);
509
- if (detectionType === 'landslide_detection') {
510
- form.append('landslide_model', document.getElementById('landslide-model')?.value || 'Rule-Based v1');
511
- }
512
- if (detectionType === 'pothole_detection') {
513
- form.append('pothole_model', document.getElementById('pothole-model')?.value || 'Rule-Based v1');
514
- }
515
  form.append('title', document.getElementById('detect-title').value || 'Untitled run');
516
  form.append('zone', document.getElementById('detect-zone').value || '');
517
  form.append('village', document.getElementById('detect-village').value || '');
 
12
  if (el) el.classList.add('active');
13
  }
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  function showError(id, msg) {
16
  const el = document.getElementById(id);
17
  if (!el) return;
 
78
  });
79
 
80
  function handlePostAuthNavigation() {
81
+ showView('dashboard');
82
+ loadHistory();
 
83
  }
84
 
85
  // ---- Forgot password ----
 
143
  try {
144
  const user = await api('GET', '/api/me');
145
  document.getElementById('user-email').textContent = user.email;
146
+ showView('dashboard');
147
+ loadHistory();
 
 
 
 
 
148
  } catch (_) { setToken(null); showView('login'); }
149
  }
150
 
 
189
  setupUploadZone('file-before', 'name-before', 'zone-before', 'preview-before');
190
  setupUploadZone('file-after', 'name-after', 'zone-after', 'preview-after');
191
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  // ---- Delhi Zone → Village cascading dropdowns ----
193
  const DELHI_ZONES = {
194
  "Central Delhi": [
 
396
  document.getElementById('form-detect')?.addEventListener('submit', async (e) => {
397
  e.preventDefault();
398
  hideError('dashboard-error');
 
399
  const before = document.getElementById('file-before').files?.[0];
400
  const after = document.getElementById('file-after').files?.[0];
401
+ if (!before || !after) {
 
 
 
 
 
402
  showError('dashboard-error', 'Please select both before and after images.');
403
  return;
404
  }
 
407
  const loading = document.getElementById('run-loading');
408
  btn.disabled = true;
409
  loading.classList.remove('hidden');
410
+ startDetectionProgress();
411
 
412
  const token = getToken();
413
  const form = new FormData();
414
+ form.append('before', before);
415
+ form.append('after', after);
 
416
  form.append('method', document.getElementById('detect-method').value);
 
 
 
 
 
 
417
  form.append('title', document.getElementById('detect-title').value || 'Untitled run');
418
  form.append('zone', document.getElementById('detect-zone').value || '');
419
  form.append('village', document.getElementById('detect-village').value || '');
templates/index.html CHANGED
@@ -131,25 +131,6 @@
131
  </section>
132
 
133
  <!-- Dashboard view -->
134
- <section id="view-detection-type" class="view">
135
- <div class="auth-container">
136
- <div class="auth-logo">
137
- <div class="auth-logo-icon">
138
- <svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l9 4.5-9 4.5-9-4.5L12 2z"/><path d="M3 6.5V17.5L12 22l9-4.5V6.5"/><path d="M12 11v11"/></svg>
139
- </div>
140
- <span>Choose Detection Type</span>
141
- </div>
142
- <div class="card">
143
- <h2 style="margin-bottom:0.75rem;">Select what you want to detect</h2>
144
- <button type="button" class="btn btn-primary btn-block" id="btn-type-change">General Change Detection</button>
145
- <div style="height:0.75rem;"></div>
146
- <button type="button" class="btn btn-secondary btn-block" id="btn-type-landslide">Landslide Detection (Uttarakhand)</button>
147
- <div style="height:0.75rem;"></div>
148
- <button type="button" class="btn btn-secondary btn-block" id="btn-type-pothole">Pothole Detection</button>
149
- </div>
150
- </div>
151
- </section>
152
-
153
  <section id="view-dashboard" class="view">
154
  <div class="topbar">
155
  <div class="nav-user">
@@ -177,28 +158,6 @@
177
  <h3>Upload / Detection</h3>
178
  </div>
179
  <form id="form-detect">
180
- <div class="options-row">
181
- <div class="form-group">
182
- <label for="detect-type">Detection Menu</label>
183
- <select id="detect-type">
184
- <option value="change_detection" selected>General Change Detection</option>
185
- <option value="landslide_detection">Landslide Detection (Uttarakhand)</option>
186
- <option value="pothole_detection">Pothole Detection</option>
187
- </select>
188
- </div>
189
- <div class="form-group hidden" id="landslide-model-group">
190
- <label for="landslide-model">Landslide Model</label>
191
- <select id="landslide-model">
192
- <option value="Rule-Based v1" selected>Rule-Based v1 (Vegetation Loss + Soil Gain)</option>
193
- </select>
194
- </div>
195
- <div class="form-group hidden" id="pothole-model-group">
196
- <label for="pothole-model">Pothole Model</label>
197
- <select id="pothole-model">
198
- <option value="Rule-Based v1" selected>Rule-Based v1 (Edges + Shadows + Texture)</option>
199
- </select>
200
- </div>
201
- </div>
202
  <div class="location-row">
203
  <div class="form-group">
204
  <label for="detect-zone">
@@ -401,6 +360,6 @@
401
  </div>
402
  </div>
403
 
404
- <script src="/static/js/app.js?v=32"></script>
405
  </body>
406
  </html>
 
131
  </section>
132
 
133
  <!-- Dashboard view -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  <section id="view-dashboard" class="view">
135
  <div class="topbar">
136
  <div class="nav-user">
 
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
  </div>
361
  </div>
362
 
363
+ <script src="/static/js/app.js?v=33"></script>
364
  </body>
365
  </html>