coderuday21 commited on
Commit
18a63c5
·
1 Parent(s): c2dff01

Detection fix: ECC registration fallback + less aggressive large-region rejection

Browse files
Files changed (1) hide show
  1. 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, False
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, False
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, False
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, False
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, False
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
- # Reject regions that cover more than 40% of the image (likely a global
1536
- # illumination shift, not a real change)
1537
- if (w * h) > img_area * 0.40:
 
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, _ = register_images(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, 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])
@@ -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