coderuday21 commited on
Commit
466c163
·
1 Parent(s): 4e9c6ba

Pothole YOLO pipeline: single-image mode, modular detector, red box visualization

Browse files
app/main.py CHANGED
@@ -222,8 +222,8 @@ def me(user: Optional[User] = Depends(get_current_user)):
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
  detection_type: str = Form("change_detection"),
229
  landslide_model: str = Form("Rule-Based v1"),
@@ -252,23 +252,42 @@ 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
- try:
256
- before_bytes = await before.read()
257
- after_bytes = await after.read()
258
- if len(before_bytes) > MAX_UPLOAD_BYTES or len(after_bytes) > MAX_UPLOAD_BYTES:
259
- raise HTTPException(status_code=400, detail="Image too large (max 20 MB)")
260
- before_pil = Image.open(io.BytesIO(before_bytes)).convert("RGB")
261
- after_pil = Image.open(io.BytesIO(after_bytes)).convert("RGB")
262
- del before_bytes, after_bytes
263
- except HTTPException:
264
- raise
265
- except Exception as e:
266
- raise HTTPException(status_code=400, detail=f"Invalid image: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  detection_sensitivity = max(0.0, min(1.0, float(detection_sensitivity)))
268
  if min_region_area is not None:
269
  min_region_area = int(max(50, min(10000, min_region_area)))
270
 
271
- detection_type = (detection_type or "change_detection").strip().lower()
272
  if detection_type == "landslide_detection":
273
  from .landslide_engine import run_landslide_detection
274
  method = f"Landslide - {landslide_model}"
 
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"),
 
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()
263
+ if raw is None or len(raw) == 0:
264
+ raise HTTPException(status_code=400, detail=f"{field_name} image is empty")
265
+ if len(raw) > MAX_UPLOAD_BYTES:
266
+ raise HTTPException(status_code=400, detail="Image too large (max 20 MB)")
267
+ return Image.open(io.BytesIO(raw)).convert("RGB")
268
+ except HTTPException:
269
+ raise
270
+ except Exception as e:
271
+ raise HTTPException(status_code=400, detail=f"Invalid {field_name} image: {e}")
272
+ finally:
273
+ try:
274
+ if raw is not None:
275
+ del raw
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}"
app/pothole_detection/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ from .pothole_detector import PotholeDetector
2
+
app/pothole_detection/inference.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 CHANGED
@@ -1,18 +1,19 @@
1
  """
2
- Pothole / road damage detection starter engine.
3
 
4
- Goal: separate pipeline that can evolve to a real model (YOLO/Mask R-CNN/SegFormer).
5
- This initial version is a CPU-friendly heuristic detector designed for vehicle/drone imagery.
6
-
7
- Notes:
8
- - Satellite imagery is generally too coarse for potholes unless very high resolution (<10 cm/px).
9
- - Vehicle camera or low-altitude drone is the realistic input for pothole detection.
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 = 1600) -> np.ndarray:
@@ -33,156 +34,83 @@ def _norm01(x: np.ndarray) -> np.ndarray:
33
  return (x - lo) / (hi - lo)
34
 
35
 
36
- def _road_texture_response(gray: np.ndarray) -> np.ndarray:
37
- # Potholes often appear as dark regions with sharp boundaries + rough texture.
38
- blur = cv2.GaussianBlur(gray, (5, 5), 0)
39
- lap = cv2.Laplacian(blur, cv2.CV_32F, ksize=3)
40
- rough = cv2.GaussianBlur(np.abs(lap), (7, 7), 0)
41
- return _norm01(rough)
42
-
43
-
44
- def _shadow_score(gray: np.ndarray) -> np.ndarray:
45
- # Darker-than-local background regions.
46
- local = cv2.GaussianBlur(gray, (31, 31), 0)
47
- diff = np.clip((local - gray).astype(np.float32), 0, None)
48
- return _norm01(diff)
49
-
50
-
51
- def _edge_score(gray: np.ndarray) -> np.ndarray:
52
- med = float(np.median(gray))
53
- t1 = int(max(0, 0.66 * med))
54
- t2 = int(min(255, 1.33 * med))
55
- edges = cv2.Canny(gray, t1, t2)
56
- edges = cv2.dilate(edges, np.ones((3, 3), np.uint8), iterations=1)
57
- return edges.astype(np.float32) / 255.0
58
-
59
-
60
- def _clean(mask: np.ndarray) -> np.ndarray:
61
- m = mask.copy()
62
- m = cv2.medianBlur(m, 5)
63
- k_open = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
64
- k_close = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (9, 9))
65
- m = cv2.morphologyEx(m, cv2.MORPH_OPEN, k_open)
66
- m = cv2.morphologyEx(m, cv2.MORPH_CLOSE, k_close)
67
- return m
68
 
 
 
 
 
 
 
 
69
 
70
- def _extract_regions(mask: np.ndarray, min_area: int = 220):
71
- n, labels, stats, cents = cv2.connectedComponentsWithStats(mask, connectivity=8)
72
- h, w = mask.shape[:2]
73
- img_area = h * w
74
- regs = []
75
  rid = 0
76
- for i in range(1, n):
77
- area = int(stats[i, cv2.CC_STAT_AREA])
78
- if area < min_area:
 
 
 
79
  continue
80
- x = int(stats[i, cv2.CC_STAT_LEFT])
81
- y = int(stats[i, cv2.CC_STAT_TOP])
82
- bw = int(stats[i, cv2.CC_STAT_WIDTH])
83
- bh = int(stats[i, cv2.CC_STAT_HEIGHT])
84
- if bw * bh > img_area * 0.25:
85
- continue
86
- ar = max(bw, bh) / max(1, min(bw, bh))
87
- if ar > 6.0:
88
- continue
89
- cx, cy = cents[i]
90
- fill = area / max(1, bw * bh)
91
- conf = float(np.clip(0.25 + fill * 0.7, 0.25, 0.95))
92
- sev = "minor"
93
- if area / img_area > 0.01:
94
- sev = "major"
95
- elif area / img_area > 0.003:
96
- sev = "moderate"
97
  rid += 1
98
- regs.append(
 
 
 
 
 
 
 
99
  {
100
  "id": rid,
101
  "area": area,
102
- "bbox": (x, y, bw, bh),
103
- "center": (int(cx), int(cy)),
104
  "object_type": "Pothole / Road Damage",
105
  "confidence": conf,
106
- "severity": sev,
107
- "sub_type": "Pothole",
108
  "sub_type_confidence": conf,
109
  "estimated_stories": None,
110
  "estimated_height_m": None,
111
  "construction_stage": None,
112
  }
113
  )
114
- regs.sort(key=lambda r: r["area"], reverse=True)
115
- return regs[:80]
116
-
117
-
118
- def _visualize(img: np.ndarray, mask: np.ndarray, regions: list[dict]) -> np.ndarray:
119
- out = img.copy().astype(np.float32)
120
- m = (mask > 127).astype(np.float32)
121
- # Orange overlay for road damage
122
- layer = np.zeros_like(out)
123
- layer[:, :, 0] = 255
124
- layer[:, :, 1] = 165
125
- alpha = 0.35
126
- for c in range(3):
127
- out[:, :, c] = out[:, :, c] * (1 - m * alpha) + layer[:, :, c] * (m * alpha)
128
- vis = np.clip(out, 0, 255).astype(np.uint8)
129
- for r in regions:
130
- x, y, w, h = r["bbox"]
131
- cv2.rectangle(vis, (x, y), (x + w, y + h), (0, 140, 255), 2)
132
- cv2.putText(vis, str(r["id"]), (x + 4, max(14, y - 4)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1, cv2.LINE_AA)
133
- return vis
134
-
135
-
136
- def run_pothole_detection(
137
- before_pil: Image.Image,
138
- after_pil: Image.Image,
139
- model_name: str = "Rule-Based v1",
140
- detection_sensitivity: float = 0.6,
141
- min_region_area: int | None = None,
142
- ):
143
- """
144
- Current UI uses (before, after) upload. For potholes, we treat the *after* image as
145
- the road image and ignore the before image.
146
- """
147
- img = _preprocess(after_pil)
148
- gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
149
-
150
- rough = _road_texture_response(gray)
151
- shadow = _shadow_score(gray)
152
- edges = _edge_score(gray)
153
-
154
- fused = 0.45 * shadow + 0.35 * rough + 0.20 * edges
155
- fused = cv2.GaussianBlur(fused.astype(np.float32), (7, 7), 0)
156
-
157
- sens = float(np.clip(detection_sensitivity, 0.0, 1.0))
158
- q = float(np.clip(0.975 - (sens - 0.5) * 0.10, 0.85, 0.985))
159
- thr = float(np.quantile(fused, q))
160
- mask = (fused >= thr).astype(np.uint8) * 255
161
- mask = _clean(mask)
162
-
163
- if min_region_area is None:
164
- min_region_area = int(max(150, min(1200, mask.shape[0] * mask.shape[1] * 0.00005)))
165
- regions = _extract_regions(mask, min_area=int(min_region_area))
166
- result = _visualize(img, mask, regions)
167
 
168
- total = int(mask.shape[0] * mask.shape[1])
169
- changed = int(np.sum(mask > 127))
170
  stats = {
171
  "total_pixels": total,
172
  "changed_pixels": changed,
173
  "unchanged_pixels": total - changed,
174
  "change_percentage": (changed / total * 100.0) if total else 0.0,
175
- "image_width": mask.shape[1],
176
- "image_height": mask.shape[0],
177
  "threshold_debug": {
178
  "method": f"Pothole Detection ({model_name})",
179
- "threshold_used": int(np.clip(thr * 255.0, 0, 255)),
180
- "threshold_percentile_q": q,
 
181
  "sensitivity": sens,
 
182
  },
183
  "params": {
184
  "detection_sensitivity": sens,
185
- "min_region_area": int(min_region_area),
186
  "model_name": model_name,
187
  "input": "after_only",
188
  },
 
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:
 
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
  },
requirements.txt CHANGED
@@ -10,3 +10,4 @@ numpy>=1.24.0
10
  opencv-python-headless>=4.8.0
11
  scikit-learn>=1.3.0
12
  requests>=2.28.0
 
 
10
  opencv-python-headless>=4.8.0
11
  scikit-learn>=1.3.0
12
  requests>=2.28.0
13
+ ultralytics>=8.2.0
static/js/app.js CHANGED
@@ -251,12 +251,23 @@ setupUploadZone('file-after', 'name-after', 'zone-after', 'preview-after');
251
  function refresh() {
252
  const isLandslide = typeSel.value === 'landslide_detection';
253
  const isPothole = typeSel.value === 'pothole_detection';
 
 
 
254
  if (landslideGroup) landslideGroup.classList.toggle('hidden', !isLandslide);
255
  if (potholeGroup) potholeGroup.classList.toggle('hidden', !isPothole);
256
  const hideCore = isLandslide || isPothole;
257
  if (methodGroup) methodGroup.classList.toggle('hidden', hideCore);
258
  if (regGroup) regGroup.classList.toggle('hidden', hideCore);
259
  if (normGroup) normGroup.classList.toggle('hidden', hideCore);
 
 
 
 
 
 
 
 
260
  }
261
 
262
  typeSel.addEventListener('change', refresh);
@@ -470,9 +481,15 @@ function stopDetectionProgress(success) {
470
  document.getElementById('form-detect')?.addEventListener('submit', async (e) => {
471
  e.preventDefault();
472
  hideError('dashboard-error');
 
473
  const before = document.getElementById('file-before').files?.[0];
474
  const after = document.getElementById('file-after').files?.[0];
475
- if (!before || !after) {
 
 
 
 
 
476
  showError('dashboard-error', 'Please select both before and after images.');
477
  return;
478
  }
@@ -485,9 +502,8 @@ document.getElementById('form-detect')?.addEventListener('submit', async (e) =>
485
 
486
  const token = getToken();
487
  const form = new FormData();
488
- form.append('before', before);
489
- form.append('after', after);
490
- const detectionType = document.getElementById('detect-type')?.value || 'change_detection';
491
  form.append('detection_type', detectionType);
492
  form.append('method', document.getElementById('detect-method').value);
493
  if (detectionType === 'landslide_detection') {
 
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);
 
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
  }
 
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') {
templates/index.html CHANGED
@@ -401,6 +401,6 @@
401
  </div>
402
  </div>
403
 
404
- <script src="/static/js/app.js?v=31"></script>
405
  </body>
406
  </html>
 
401
  </div>
402
  </div>
403
 
404
+ <script src="/static/js/app.js?v=32"></script>
405
  </body>
406
  </html>