Spaces:
Building
Building
Commit ·
18a63c5
1
Parent(s): c2dff01
Detection fix: ECC registration fallback + less aggressive large-region rejection
Browse files- app/detection_engine.py +52 -11
app/detection_engine.py
CHANGED
|
@@ -47,7 +47,7 @@ def register_images(img1, img2, max_features=2000):
|
|
| 47 |
kp2, des2 = orb.detectAndCompute(gray2, None)
|
| 48 |
|
| 49 |
if des1 is None or des2 is None or len(des1) < 10 or len(des2) < 10:
|
| 50 |
-
return img1, img2
|
| 51 |
|
| 52 |
# Use kNN matching with Lowe's ratio test for better matches
|
| 53 |
bf = cv2.BFMatcher(cv2.NORM_HAMMING)
|
|
@@ -61,29 +61,63 @@ def register_images(img1, img2, max_features=2000):
|
|
| 61 |
good_matches.append(m)
|
| 62 |
|
| 63 |
if len(good_matches) < 10:
|
| 64 |
-
return img1, img2
|
| 65 |
|
| 66 |
src_pts = np.float32([kp1[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2)
|
| 67 |
dst_pts = np.float32([kp2[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2)
|
| 68 |
|
| 69 |
homography, mask = cv2.findHomography(dst_pts, src_pts, cv2.RANSAC, 3.0)
|
| 70 |
if homography is None:
|
| 71 |
-
return img1, img2
|
| 72 |
|
| 73 |
inlier_ratio = np.sum(mask) / len(mask) if mask is not None else 0
|
| 74 |
if inlier_ratio < 0.3:
|
| 75 |
-
return img1, img2
|
| 76 |
|
| 77 |
# Reject degenerate homographies (near-singular or extreme distortion)
|
| 78 |
det = np.linalg.det(homography)
|
| 79 |
if abs(det) < 0.1 or abs(det) > 10.0:
|
| 80 |
-
return img1, img2
|
| 81 |
|
| 82 |
h, w = img1.shape[:2]
|
| 83 |
img2_aligned = cv2.warpPerspective(img2, homography, (w, h), borderMode=cv2.BORDER_REFLECT)
|
| 84 |
return img1, img2_aligned, True
|
| 85 |
|
| 86 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
# ---------------------------------------------------------------------------
|
| 88 |
# 3. Improved radiometric normalization
|
| 89 |
# ---------------------------------------------------------------------------
|
|
@@ -1499,7 +1533,7 @@ def _nms_regions(regions, iou_thresh=0.45):
|
|
| 1499 |
|
| 1500 |
|
| 1501 |
def analyze_change_regions(change_mask, image, min_area=400, use_ensemble=True,
|
| 1502 |
-
before_img=None):
|
| 1503 |
"""
|
| 1504 |
Find connected change regions with strict quality filters:
|
| 1505 |
- Higher min_area (400) to reject noise
|
|
@@ -1532,9 +1566,10 @@ def analyze_change_regions(change_mask, image, min_area=400, use_ensemble=True,
|
|
| 1532 |
if fill_ratio < 0.10:
|
| 1533 |
continue
|
| 1534 |
|
| 1535 |
-
#
|
| 1536 |
-
#
|
| 1537 |
-
|
|
|
|
| 1538 |
continue
|
| 1539 |
|
| 1540 |
cx, cy = centroids[i]
|
|
@@ -1615,8 +1650,9 @@ def run_detection(before_pil, after_pil, method="AI-Based Deep Learning",
|
|
| 1615 |
before_array = preprocess_image(before_pil)
|
| 1616 |
after_array = preprocess_image(after_pil)
|
| 1617 |
|
|
|
|
| 1618 |
if enable_registration:
|
| 1619 |
-
before_array, after_array,
|
| 1620 |
if enable_normalization:
|
| 1621 |
before_array, after_array = normalize_radiometry(before_array, after_array)
|
| 1622 |
|
|
@@ -1642,7 +1678,11 @@ def run_detection(before_pil, after_pil, method="AI-Based Deep Learning",
|
|
| 1642 |
)
|
| 1643 |
|
| 1644 |
change_regions = analyze_change_regions(
|
| 1645 |
-
change_mask,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1646 |
)
|
| 1647 |
|
| 1648 |
total_pixels = int(change_mask.shape[0] * change_mask.shape[1])
|
|
@@ -1666,6 +1706,7 @@ def run_detection(before_pil, after_pil, method="AI-Based Deep Learning",
|
|
| 1666 |
"min_region_area": min_region_area,
|
| 1667 |
"enable_registration": bool(enable_registration),
|
| 1668 |
"enable_normalization": bool(enable_normalization),
|
|
|
|
| 1669 |
},
|
| 1670 |
}
|
| 1671 |
|
|
|
|
| 47 |
kp2, des2 = orb.detectAndCompute(gray2, None)
|
| 48 |
|
| 49 |
if des1 is None or des2 is None or len(des1) < 10 or len(des2) < 10:
|
| 50 |
+
return _register_images_ecc_fallback(img1, img2)
|
| 51 |
|
| 52 |
# Use kNN matching with Lowe's ratio test for better matches
|
| 53 |
bf = cv2.BFMatcher(cv2.NORM_HAMMING)
|
|
|
|
| 61 |
good_matches.append(m)
|
| 62 |
|
| 63 |
if len(good_matches) < 10:
|
| 64 |
+
return _register_images_ecc_fallback(img1, img2)
|
| 65 |
|
| 66 |
src_pts = np.float32([kp1[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2)
|
| 67 |
dst_pts = np.float32([kp2[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2)
|
| 68 |
|
| 69 |
homography, mask = cv2.findHomography(dst_pts, src_pts, cv2.RANSAC, 3.0)
|
| 70 |
if homography is None:
|
| 71 |
+
return _register_images_ecc_fallback(img1, img2)
|
| 72 |
|
| 73 |
inlier_ratio = np.sum(mask) / len(mask) if mask is not None else 0
|
| 74 |
if inlier_ratio < 0.3:
|
| 75 |
+
return _register_images_ecc_fallback(img1, img2)
|
| 76 |
|
| 77 |
# Reject degenerate homographies (near-singular or extreme distortion)
|
| 78 |
det = np.linalg.det(homography)
|
| 79 |
if abs(det) < 0.1 or abs(det) > 10.0:
|
| 80 |
+
return _register_images_ecc_fallback(img1, img2)
|
| 81 |
|
| 82 |
h, w = img1.shape[:2]
|
| 83 |
img2_aligned = cv2.warpPerspective(img2, homography, (w, h), borderMode=cv2.BORDER_REFLECT)
|
| 84 |
return img1, img2_aligned, True
|
| 85 |
|
| 86 |
|
| 87 |
+
def _register_images_ecc_fallback(img1, img2):
|
| 88 |
+
"""
|
| 89 |
+
Fallback alignment with ECC affine registration.
|
| 90 |
+
More stable than ORB on low-texture agricultural areas.
|
| 91 |
+
"""
|
| 92 |
+
try:
|
| 93 |
+
gray1 = cv2.cvtColor(img1, cv2.COLOR_RGB2GRAY)
|
| 94 |
+
gray2 = cv2.cvtColor(img2, cv2.COLOR_RGB2GRAY)
|
| 95 |
+
gray1_f = gray1.astype(np.float32) / 255.0
|
| 96 |
+
gray2_f = gray2.astype(np.float32) / 255.0
|
| 97 |
+
|
| 98 |
+
warp = np.eye(2, 3, dtype=np.float32)
|
| 99 |
+
criteria = (
|
| 100 |
+
cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT,
|
| 101 |
+
200,
|
| 102 |
+
1e-6,
|
| 103 |
+
)
|
| 104 |
+
cc, warp = cv2.findTransformECC(
|
| 105 |
+
gray1_f, gray2_f, warp, cv2.MOTION_AFFINE, criteria
|
| 106 |
+
)
|
| 107 |
+
h, w = img1.shape[:2]
|
| 108 |
+
aligned = cv2.warpAffine(
|
| 109 |
+
img2,
|
| 110 |
+
warp,
|
| 111 |
+
(w, h),
|
| 112 |
+
flags=cv2.INTER_LINEAR + cv2.WARP_INVERSE_MAP,
|
| 113 |
+
borderMode=cv2.BORDER_REFLECT,
|
| 114 |
+
)
|
| 115 |
+
# Treat as successful only if ECC correlation is reasonable.
|
| 116 |
+
return img1, aligned, bool(cc >= 0.45)
|
| 117 |
+
except Exception:
|
| 118 |
+
return img1, img2, False
|
| 119 |
+
|
| 120 |
+
|
| 121 |
# ---------------------------------------------------------------------------
|
| 122 |
# 3. Improved radiometric normalization
|
| 123 |
# ---------------------------------------------------------------------------
|
|
|
|
| 1533 |
|
| 1534 |
|
| 1535 |
def analyze_change_regions(change_mask, image, min_area=400, use_ensemble=True,
|
| 1536 |
+
before_img=None, registration_ok=True):
|
| 1537 |
"""
|
| 1538 |
Find connected change regions with strict quality filters:
|
| 1539 |
- Higher min_area (400) to reject noise
|
|
|
|
| 1566 |
if fill_ratio < 0.10:
|
| 1567 |
continue
|
| 1568 |
|
| 1569 |
+
# Keep large real changes; only suppress near-full-frame artifacts.
|
| 1570 |
+
# When registration failed, allow larger regions to avoid missing true changes.
|
| 1571 |
+
max_region_cover = 0.92 if not registration_ok else 0.75
|
| 1572 |
+
if (w * h) > img_area * max_region_cover and fill_ratio < 0.35:
|
| 1573 |
continue
|
| 1574 |
|
| 1575 |
cx, cy = centroids[i]
|
|
|
|
| 1650 |
before_array = preprocess_image(before_pil)
|
| 1651 |
after_array = preprocess_image(after_pil)
|
| 1652 |
|
| 1653 |
+
registration_ok = False
|
| 1654 |
if enable_registration:
|
| 1655 |
+
before_array, after_array, registration_ok = register_images(before_array, after_array)
|
| 1656 |
if enable_normalization:
|
| 1657 |
before_array, after_array = normalize_radiometry(before_array, after_array)
|
| 1658 |
|
|
|
|
| 1678 |
)
|
| 1679 |
|
| 1680 |
change_regions = analyze_change_regions(
|
| 1681 |
+
change_mask,
|
| 1682 |
+
after_array,
|
| 1683 |
+
min_area=min_region_area,
|
| 1684 |
+
before_img=before_array,
|
| 1685 |
+
registration_ok=registration_ok,
|
| 1686 |
)
|
| 1687 |
|
| 1688 |
total_pixels = int(change_mask.shape[0] * change_mask.shape[1])
|
|
|
|
| 1706 |
"min_region_area": min_region_area,
|
| 1707 |
"enable_registration": bool(enable_registration),
|
| 1708 |
"enable_normalization": bool(enable_normalization),
|
| 1709 |
+
"registration_ok": bool(registration_ok),
|
| 1710 |
},
|
| 1711 |
}
|
| 1712 |
|