Spaces:
Sleeping
Sleeping
Commit ·
c2dff01
1
Parent(s): 2b175bf
Detection tuning controls + threshold diagnostics in API response
Browse files- app/detection_engine.py +92 -22
- app/main.py +15 -1
- static/js/app.js +26 -1
- templates/index.html +10 -0
app/detection_engine.py
CHANGED
|
@@ -201,7 +201,27 @@ def compute_edge_change(img1, img2):
|
|
| 201 |
# 7. Improved detection methods
|
| 202 |
# ---------------------------------------------------------------------------
|
| 203 |
|
| 204 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
"""Improved image difference with multi-channel analysis and adaptive threshold."""
|
| 206 |
if img1.shape != img2.shape:
|
| 207 |
img2 = cv2.resize(img2, (img1.shape[1], img1.shape[0]))
|
|
@@ -223,13 +243,19 @@ def image_difference_method(img1, img2, threshold=0.25, blur_size=5):
|
|
| 223 |
delta_e = delta_e / delta_e.max() if delta_e.max() > 0 else delta_e
|
| 224 |
|
| 225 |
delta_uint8 = (delta_e * 255).astype(np.uint8)
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
_, change_mask = cv2.threshold(delta_uint8, 30, 255, cv2.THRESH_BINARY)
|
| 230 |
|
| 231 |
change_mask = _clean_mask(change_mask)
|
| 232 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
|
| 234 |
|
| 235 |
def feature_based_method(img1, img2, num_clusters=4, sensitivity=0.5):
|
|
@@ -288,7 +314,7 @@ def feature_based_method(img1, img2, num_clusters=4, sensitivity=0.5):
|
|
| 288 |
return change_mask
|
| 289 |
|
| 290 |
|
| 291 |
-
def ai_deep_learning_method(img1, img2):
|
| 292 |
"""
|
| 293 |
Advanced multi-signal fusion:
|
| 294 |
- Multi-scale color difference (LAB)
|
|
@@ -358,10 +384,10 @@ def ai_deep_learning_method(img1, img2):
|
|
| 358 |
fused = fused / (fused.max() + 1e-8)
|
| 359 |
fused_uint8 = (fused * 255).astype(np.uint8)
|
| 360 |
|
| 361 |
-
#
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
|
| 366 |
change_mask = _clean_mask(change_mask)
|
| 367 |
|
|
@@ -369,17 +395,24 @@ def ai_deep_learning_method(img1, img2):
|
|
| 369 |
change_mask = cv2.bilateralFilter(change_mask, 9, 75, 75)
|
| 370 |
_, change_mask = cv2.threshold(change_mask, 127, 255, cv2.THRESH_BINARY)
|
| 371 |
|
| 372 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 373 |
|
| 374 |
|
| 375 |
-
def hybrid_method(img1, img2):
|
| 376 |
"""Hybrid: weighted fusion of all methods with confidence-based merging."""
|
| 377 |
if img1.shape != img2.shape:
|
| 378 |
img2 = cv2.resize(img2, (img1.shape[1], img1.shape[0]))
|
| 379 |
|
| 380 |
-
diff_mask = image_difference_method(img1, img2)
|
| 381 |
feature_mask = feature_based_method(img1, img2)
|
| 382 |
-
ai_mask = ai_deep_learning_method(img1, img2)
|
| 383 |
|
| 384 |
# Weighted combination: AI method gets most weight
|
| 385 |
combined = (
|
|
@@ -389,9 +422,21 @@ def hybrid_method(img1, img2):
|
|
| 389 |
)
|
| 390 |
|
| 391 |
# Use a higher threshold: a pixel must be flagged by multiple methods
|
| 392 |
-
|
|
|
|
|
|
|
|
|
|
| 393 |
final_mask = _clean_mask(final_mask)
|
| 394 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
|
| 396 |
|
| 397 |
# ---------------------------------------------------------------------------
|
|
@@ -1470,6 +1515,11 @@ def analyze_change_regions(change_mask, image, min_area=400, use_ensemble=True,
|
|
| 1470 |
|
| 1471 |
img_h, img_w = change_mask.shape[:2]
|
| 1472 |
img_area = img_h * img_w
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1473 |
|
| 1474 |
for i in range(1, num_labels):
|
| 1475 |
raw_area = stats[i, cv2.CC_STAT_AREA]
|
|
@@ -1559,7 +1609,8 @@ def analyze_change_regions(change_mask, image, min_area=400, use_ensemble=True,
|
|
| 1559 |
# ---------------------------------------------------------------------------
|
| 1560 |
|
| 1561 |
def run_detection(before_pil, after_pil, method="AI-Based Deep Learning",
|
| 1562 |
-
enable_registration=True, enable_normalization=True
|
|
|
|
| 1563 |
"""Run full detection pipeline; returns change_mask, result_image, stats, regions."""
|
| 1564 |
before_array = preprocess_image(before_pil)
|
| 1565 |
after_array = preprocess_image(after_pil)
|
|
@@ -1570,16 +1621,28 @@ def run_detection(before_pil, after_pil, method="AI-Based Deep Learning",
|
|
| 1570 |
before_array, after_array = normalize_radiometry(before_array, after_array)
|
| 1571 |
|
| 1572 |
if method == "AI-Based Deep Learning":
|
| 1573 |
-
change_mask = ai_deep_learning_method(
|
|
|
|
|
|
|
| 1574 |
elif method == "Image Difference":
|
| 1575 |
-
change_mask = image_difference_method(
|
|
|
|
|
|
|
| 1576 |
elif method == "Feature-Based":
|
| 1577 |
change_mask = feature_based_method(before_array, after_array)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1578 |
else:
|
| 1579 |
-
change_mask = hybrid_method(
|
|
|
|
|
|
|
| 1580 |
|
| 1581 |
change_regions = analyze_change_regions(
|
| 1582 |
-
change_mask, after_array, min_area=
|
| 1583 |
)
|
| 1584 |
|
| 1585 |
total_pixels = int(change_mask.shape[0] * change_mask.shape[1])
|
|
@@ -1597,6 +1660,13 @@ def run_detection(before_pil, after_pil, method="AI-Based Deep Learning",
|
|
| 1597 |
"change_percentage": change_pct,
|
| 1598 |
"image_width": change_mask.shape[1],
|
| 1599 |
"image_height": change_mask.shape[0],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1600 |
}
|
| 1601 |
|
| 1602 |
return change_mask, result_image, stats, change_regions
|
|
|
|
| 201 |
# 7. Improved detection methods
|
| 202 |
# ---------------------------------------------------------------------------
|
| 203 |
|
| 204 |
+
def _adaptive_binary_threshold(score_uint8, min_floor=25, sensitivity=0.5):
|
| 205 |
+
"""
|
| 206 |
+
Robust thresholding for noisy scenes.
|
| 207 |
+
Uses max(Otsu, noise-floor, fixed floor) where noise-floor is median + 3*MAD.
|
| 208 |
+
"""
|
| 209 |
+
otsu_val, _ = cv2.threshold(
|
| 210 |
+
score_uint8, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU
|
| 211 |
+
)
|
| 212 |
+
median = float(np.median(score_uint8))
|
| 213 |
+
mad = float(np.median(np.abs(score_uint8.astype(np.float32) - median)))
|
| 214 |
+
noise_floor = median + 3.0 * mad
|
| 215 |
+
# Higher sensitivity => lower threshold (detect more), lower sensitivity => stricter
|
| 216 |
+
sens = float(np.clip(sensitivity, 0.0, 1.0))
|
| 217 |
+
sens_shift = int((0.5 - sens) * 24) # approx -12..+12 around baseline
|
| 218 |
+
thr = int(max(min_floor, otsu_val, noise_floor) + sens_shift)
|
| 219 |
+
thr = max(0, min(255, thr))
|
| 220 |
+
_, mask = cv2.threshold(score_uint8, thr, 255, cv2.THRESH_BINARY)
|
| 221 |
+
return mask, thr, float(otsu_val), float(noise_floor)
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def image_difference_method(img1, img2, threshold=0.25, blur_size=5, sensitivity=0.5):
|
| 225 |
"""Improved image difference with multi-channel analysis and adaptive threshold."""
|
| 226 |
if img1.shape != img2.shape:
|
| 227 |
img2 = cv2.resize(img2, (img1.shape[1], img1.shape[0]))
|
|
|
|
| 243 |
delta_e = delta_e / delta_e.max() if delta_e.max() > 0 else delta_e
|
| 244 |
|
| 245 |
delta_uint8 = (delta_e * 255).astype(np.uint8)
|
| 246 |
+
change_mask, used_thr, otsu_val, noise_floor = _adaptive_binary_threshold(
|
| 247 |
+
delta_uint8, min_floor=30, sensitivity=sensitivity
|
| 248 |
+
)
|
|
|
|
| 249 |
|
| 250 |
change_mask = _clean_mask(change_mask)
|
| 251 |
+
debug = {
|
| 252 |
+
"method": "Image Difference",
|
| 253 |
+
"threshold_used": int(used_thr),
|
| 254 |
+
"otsu": float(otsu_val),
|
| 255 |
+
"noise_floor": float(noise_floor),
|
| 256 |
+
"sensitivity": float(sensitivity),
|
| 257 |
+
}
|
| 258 |
+
return change_mask, debug
|
| 259 |
|
| 260 |
|
| 261 |
def feature_based_method(img1, img2, num_clusters=4, sensitivity=0.5):
|
|
|
|
| 314 |
return change_mask
|
| 315 |
|
| 316 |
|
| 317 |
+
def ai_deep_learning_method(img1, img2, sensitivity=0.5):
|
| 318 |
"""
|
| 319 |
Advanced multi-signal fusion:
|
| 320 |
- Multi-scale color difference (LAB)
|
|
|
|
| 384 |
fused = fused / (fused.max() + 1e-8)
|
| 385 |
fused_uint8 = (fused * 255).astype(np.uint8)
|
| 386 |
|
| 387 |
+
# Robust thresholding: handles low-contrast and noisy scenes better than Otsu-only.
|
| 388 |
+
change_mask, used_thr, otsu_val, noise_floor = _adaptive_binary_threshold(
|
| 389 |
+
fused_uint8, min_floor=25, sensitivity=sensitivity
|
| 390 |
+
)
|
| 391 |
|
| 392 |
change_mask = _clean_mask(change_mask)
|
| 393 |
|
|
|
|
| 395 |
change_mask = cv2.bilateralFilter(change_mask, 9, 75, 75)
|
| 396 |
_, change_mask = cv2.threshold(change_mask, 127, 255, cv2.THRESH_BINARY)
|
| 397 |
|
| 398 |
+
debug = {
|
| 399 |
+
"method": "AI-Based Deep Learning",
|
| 400 |
+
"threshold_used": int(used_thr),
|
| 401 |
+
"otsu": float(otsu_val),
|
| 402 |
+
"noise_floor": float(noise_floor),
|
| 403 |
+
"sensitivity": float(sensitivity),
|
| 404 |
+
}
|
| 405 |
+
return change_mask, debug
|
| 406 |
|
| 407 |
|
| 408 |
+
def hybrid_method(img1, img2, sensitivity=0.5):
|
| 409 |
"""Hybrid: weighted fusion of all methods with confidence-based merging."""
|
| 410 |
if img1.shape != img2.shape:
|
| 411 |
img2 = cv2.resize(img2, (img1.shape[1], img1.shape[0]))
|
| 412 |
|
| 413 |
+
diff_mask, diff_debug = image_difference_method(img1, img2, sensitivity=sensitivity)
|
| 414 |
feature_mask = feature_based_method(img1, img2)
|
| 415 |
+
ai_mask, ai_debug = ai_deep_learning_method(img1, img2, sensitivity=sensitivity)
|
| 416 |
|
| 417 |
# Weighted combination: AI method gets most weight
|
| 418 |
combined = (
|
|
|
|
| 422 |
)
|
| 423 |
|
| 424 |
# Use a higher threshold: a pixel must be flagged by multiple methods
|
| 425 |
+
base_thr = 140
|
| 426 |
+
sens = float(np.clip(sensitivity, 0.0, 1.0))
|
| 427 |
+
hybrid_thr = int(np.clip(base_thr + int((0.5 - sens) * 24), 90, 180))
|
| 428 |
+
_, final_mask = cv2.threshold(combined.astype(np.uint8), hybrid_thr, 255, cv2.THRESH_BINARY)
|
| 429 |
final_mask = _clean_mask(final_mask)
|
| 430 |
+
debug = {
|
| 431 |
+
"method": "Hybrid Approach",
|
| 432 |
+
"threshold_used": int(hybrid_thr),
|
| 433 |
+
"sensitivity": float(sensitivity),
|
| 434 |
+
"sub_methods": {
|
| 435 |
+
"image_difference": diff_debug,
|
| 436 |
+
"ai_deep_learning": ai_debug,
|
| 437 |
+
},
|
| 438 |
+
}
|
| 439 |
+
return final_mask, debug
|
| 440 |
|
| 441 |
|
| 442 |
# ---------------------------------------------------------------------------
|
|
|
|
| 1515 |
|
| 1516 |
img_h, img_w = change_mask.shape[:2]
|
| 1517 |
img_area = img_h * img_w
|
| 1518 |
+
# Adaptive minimum region size:
|
| 1519 |
+
# - keeps sensitivity on smaller images
|
| 1520 |
+
# - suppresses speckle noise on larger images
|
| 1521 |
+
if min_area is None:
|
| 1522 |
+
min_area = int(max(250, min(1200, img_area * 0.00008)))
|
| 1523 |
|
| 1524 |
for i in range(1, num_labels):
|
| 1525 |
raw_area = stats[i, cv2.CC_STAT_AREA]
|
|
|
|
| 1609 |
# ---------------------------------------------------------------------------
|
| 1610 |
|
| 1611 |
def run_detection(before_pil, after_pil, method="AI-Based Deep Learning",
|
| 1612 |
+
enable_registration=True, enable_normalization=True,
|
| 1613 |
+
detection_sensitivity=0.5, min_region_area=None):
|
| 1614 |
"""Run full detection pipeline; returns change_mask, result_image, stats, regions."""
|
| 1615 |
before_array = preprocess_image(before_pil)
|
| 1616 |
after_array = preprocess_image(after_pil)
|
|
|
|
| 1621 |
before_array, after_array = normalize_radiometry(before_array, after_array)
|
| 1622 |
|
| 1623 |
if method == "AI-Based Deep Learning":
|
| 1624 |
+
change_mask, threshold_debug = ai_deep_learning_method(
|
| 1625 |
+
before_array, after_array, sensitivity=detection_sensitivity
|
| 1626 |
+
)
|
| 1627 |
elif method == "Image Difference":
|
| 1628 |
+
change_mask, threshold_debug = image_difference_method(
|
| 1629 |
+
before_array, after_array, sensitivity=detection_sensitivity
|
| 1630 |
+
)
|
| 1631 |
elif method == "Feature-Based":
|
| 1632 |
change_mask = feature_based_method(before_array, after_array)
|
| 1633 |
+
threshold_debug = {
|
| 1634 |
+
"method": "Feature-Based",
|
| 1635 |
+
"threshold_used": None,
|
| 1636 |
+
"note": "KMeans clustering path does not use binary threshold.",
|
| 1637 |
+
"sensitivity": float(detection_sensitivity),
|
| 1638 |
+
}
|
| 1639 |
else:
|
| 1640 |
+
change_mask, threshold_debug = hybrid_method(
|
| 1641 |
+
before_array, after_array, sensitivity=detection_sensitivity
|
| 1642 |
+
)
|
| 1643 |
|
| 1644 |
change_regions = analyze_change_regions(
|
| 1645 |
+
change_mask, after_array, min_area=min_region_area, before_img=before_array
|
| 1646 |
)
|
| 1647 |
|
| 1648 |
total_pixels = int(change_mask.shape[0] * change_mask.shape[1])
|
|
|
|
| 1660 |
"change_percentage": change_pct,
|
| 1661 |
"image_width": change_mask.shape[1],
|
| 1662 |
"image_height": change_mask.shape[0],
|
| 1663 |
+
"threshold_debug": threshold_debug,
|
| 1664 |
+
"params": {
|
| 1665 |
+
"detection_sensitivity": float(detection_sensitivity),
|
| 1666 |
+
"min_region_area": min_region_area,
|
| 1667 |
+
"enable_registration": bool(enable_registration),
|
| 1668 |
+
"enable_normalization": bool(enable_normalization),
|
| 1669 |
+
},
|
| 1670 |
}
|
| 1671 |
|
| 1672 |
return change_mask, result_image, stats, change_regions
|
app/main.py
CHANGED
|
@@ -230,6 +230,8 @@ async def detect(
|
|
| 230 |
village: str = Form(""),
|
| 231 |
enable_registration: bool = Form(True),
|
| 232 |
enable_normalization: bool = Form(True),
|
|
|
|
|
|
|
| 233 |
notify_email: Optional[str] = Form(None),
|
| 234 |
access_token: Optional[str] = Form(None),
|
| 235 |
db: Session = Depends(get_db),
|
|
@@ -259,9 +261,19 @@ async def detect(
|
|
| 259 |
raise
|
| 260 |
except Exception as e:
|
| 261 |
raise HTTPException(status_code=400, detail=f"Invalid image: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
from .detection_engine import run_detection
|
| 263 |
change_mask, result_image, stats, change_regions = run_detection(
|
| 264 |
-
before_pil,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
)
|
| 266 |
# Save overlay and thumbnails for history table view
|
| 267 |
base_name = f"{user.id}_{uuid.uuid4().hex}"
|
|
@@ -370,6 +382,8 @@ async def detect(
|
|
| 370 |
"changedPixels": changed_px,
|
| 371 |
"unchangedPixels": unchanged_px,
|
| 372 |
"changePercentage": change_pct,
|
|
|
|
|
|
|
| 373 |
},
|
| 374 |
"regions": regions_serializable,
|
| 375 |
"overlayBase64Png": overlay_b64,
|
|
|
|
| 230 |
village: str = Form(""),
|
| 231 |
enable_registration: bool = Form(True),
|
| 232 |
enable_normalization: bool = Form(True),
|
| 233 |
+
detection_sensitivity: float = Form(0.5),
|
| 234 |
+
min_region_area: Optional[int] = Form(None),
|
| 235 |
notify_email: Optional[str] = Form(None),
|
| 236 |
access_token: Optional[str] = Form(None),
|
| 237 |
db: Session = Depends(get_db),
|
|
|
|
| 261 |
raise
|
| 262 |
except Exception as e:
|
| 263 |
raise HTTPException(status_code=400, detail=f"Invalid image: {e}")
|
| 264 |
+
detection_sensitivity = max(0.0, min(1.0, float(detection_sensitivity)))
|
| 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}"
|
|
|
|
| 382 |
"changedPixels": changed_px,
|
| 383 |
"unchangedPixels": unchanged_px,
|
| 384 |
"changePercentage": change_pct,
|
| 385 |
+
"thresholdDebug": stats.get("threshold_debug", {}),
|
| 386 |
+
"params": stats.get("params", {}),
|
| 387 |
},
|
| 388 |
"regions": regions_serializable,
|
| 389 |
"overlayBase64Png": overlay_b64,
|
static/js/app.js
CHANGED
|
@@ -416,6 +416,29 @@ document.getElementById('form-detect')?.addEventListener('submit', async (e) =>
|
|
| 416 |
form.append('village', document.getElementById('detect-village').value || '');
|
| 417 |
form.append('enable_registration', document.getElementById('detect-registration').checked);
|
| 418 |
form.append('enable_normalization', document.getElementById('detect-normalization').checked);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 419 |
|
| 420 |
// Notify: validate and attach email if checkbox is checked
|
| 421 |
const notifyCb = document.getElementById('detect-notify');
|
|
@@ -449,7 +472,9 @@ document.getElementById('form-detect')?.addEventListener('submit', async (e) =>
|
|
| 449 |
? ' Notification email sent.'
|
| 450 |
: ` Email notification failed${data.notificationError ? `: ${data.notificationError}` : '.'}`;
|
| 451 |
}
|
| 452 |
-
|
|
|
|
|
|
|
| 453 |
loadHistory();
|
| 454 |
} catch (err) {
|
| 455 |
showError('dashboard-error', err.message);
|
|
|
|
| 416 |
form.append('village', document.getElementById('detect-village').value || '');
|
| 417 |
form.append('enable_registration', document.getElementById('detect-registration').checked);
|
| 418 |
form.append('enable_normalization', document.getElementById('detect-normalization').checked);
|
| 419 |
+
const sensitivityInput = document.getElementById('detect-sensitivity');
|
| 420 |
+
const minAreaInput = document.getElementById('detect-min-area');
|
| 421 |
+
const sensitivity = Number(sensitivityInput?.value ?? 0.5);
|
| 422 |
+
if (Number.isNaN(sensitivity) || sensitivity < 0 || sensitivity > 1) {
|
| 423 |
+
showError('dashboard-error', 'Detection sensitivity must be between 0 and 1.');
|
| 424 |
+
btn.disabled = false;
|
| 425 |
+
loading.classList.add('hidden');
|
| 426 |
+
stopDetectionProgress(false);
|
| 427 |
+
return;
|
| 428 |
+
}
|
| 429 |
+
form.append('detection_sensitivity', String(sensitivity));
|
| 430 |
+
const minAreaRaw = (minAreaInput?.value || '').trim();
|
| 431 |
+
if (minAreaRaw) {
|
| 432 |
+
const minArea = Number(minAreaRaw);
|
| 433 |
+
if (Number.isNaN(minArea) || minArea < 50) {
|
| 434 |
+
showError('dashboard-error', 'Min region area must be at least 50.');
|
| 435 |
+
btn.disabled = false;
|
| 436 |
+
loading.classList.add('hidden');
|
| 437 |
+
stopDetectionProgress(false);
|
| 438 |
+
return;
|
| 439 |
+
}
|
| 440 |
+
form.append('min_region_area', String(Math.round(minArea)));
|
| 441 |
+
}
|
| 442 |
|
| 443 |
// Notify: validate and attach email if checkbox is checked
|
| 444 |
const notifyCb = document.getElementById('detect-notify');
|
|
|
|
| 472 |
? ' Notification email sent.'
|
| 473 |
: ` Email notification failed${data.notificationError ? `: ${data.notificationError}` : '.'}`;
|
| 474 |
}
|
| 475 |
+
const thrInfo = data?.statistics?.thresholdDebug?.threshold_used;
|
| 476 |
+
const thrMsg = typeof thrInfo === 'number' ? ` Threshold: ${thrInfo}.` : '';
|
| 477 |
+
showSuccess('dashboard-success', 'Detection complete!' + thrMsg + notifyMsg);
|
| 478 |
loadHistory();
|
| 479 |
} catch (err) {
|
| 480 |
showError('dashboard-error', err.message);
|
templates/index.html
CHANGED
|
@@ -219,6 +219,16 @@
|
|
| 219 |
<label><input type="checkbox" id="detect-normalization" checked /> Radiometric Normalization</label>
|
| 220 |
</div>
|
| 221 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
<div class="notify-row">
|
| 223 |
<div class="form-group checkbox-group">
|
| 224 |
<label><input type="checkbox" id="detect-notify" /> Notify via Email</label>
|
|
|
|
| 219 |
<label><input type="checkbox" id="detect-normalization" checked /> Radiometric Normalization</label>
|
| 220 |
</div>
|
| 221 |
</div>
|
| 222 |
+
<div class="options-row">
|
| 223 |
+
<div class="form-group">
|
| 224 |
+
<label for="detect-sensitivity">Detection Sensitivity (0-1)</label>
|
| 225 |
+
<input type="number" id="detect-sensitivity" min="0" max="1" step="0.05" value="0.5" />
|
| 226 |
+
</div>
|
| 227 |
+
<div class="form-group">
|
| 228 |
+
<label for="detect-min-area">Min Region Area (px, optional)</label>
|
| 229 |
+
<input type="number" id="detect-min-area" min="50" max="10000" step="50" placeholder="Auto" />
|
| 230 |
+
</div>
|
| 231 |
+
</div>
|
| 232 |
<div class="notify-row">
|
| 233 |
<div class="form-group checkbox-group">
|
| 234 |
<label><input type="checkbox" id="detect-notify" /> Notify via Email</label>
|