coderuday21 commited on
Commit
ce994d8
·
1 Parent(s): b7a71f3

Improve building detection: fix tile size to 256, lower thresholds, add SSIM, smarter scoring

Browse files
Dockerfile CHANGED
@@ -19,7 +19,7 @@ WORKDIR /app
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=16
23
  ENV APP_BUILD=${APP_BUILD}
24
  RUN echo "Docker build start: APP_BUILD=${APP_BUILD}" && python -V
25
 
 
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=17
23
  ENV APP_BUILD=${APP_BUILD}
24
  RUN echo "Docker build start: APP_BUILD=${APP_BUILD}" && python -V
25
 
app/detection_engine.py CHANGED
@@ -593,7 +593,7 @@ def ai_deep_learning_method(img1, img2, sensitivity=0.5):
593
  from .model_inference import is_model_available, predict_change_mask
594
 
595
  if is_model_available():
596
- threshold = 0.35 + (1.0 - sensitivity) * 0.3
597
  try:
598
  change_mask, score_map = predict_change_mask(
599
  img1, img2, threshold=threshold)
@@ -705,7 +705,7 @@ def _clean_mask(mask, sensitivity=0.5, border_margin=12):
705
  filled = cv2.dilate(filled, k_break, iterations=1)
706
 
707
  # 7. Component-level filtering: remove tiny survivors and elongated noise
708
- min_component_px = max(80, int(h * w * 0.00004))
709
  num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(filled, connectivity=8)
710
  clean = np.zeros_like(filled)
711
  for i in range(1, num_labels):
@@ -980,12 +980,33 @@ def _extract_differential_features(before_crop, after_crop):
980
  lines_a, linelen_a = _count_line_segments(gray_a)
981
  corners_b = _count_corners(gray_b)
982
  corners_a = _count_corners(gray_a)
 
983
  hull_a = _rectangular_hull_ratio(gray_a)
984
 
985
  lab_b = cv2.cvtColor(before_crop, cv2.COLOR_RGB2LAB).astype(np.float32)
986
  lab_a = cv2.cvtColor(after_crop, cv2.COLOR_RGB2LAB).astype(np.float32)
987
  lab_dist = float(np.mean(np.sqrt(np.sum((lab_a - lab_b) ** 2, axis=2))))
988
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
989
  return {
990
  "before": feat_b, "after": feat_a,
991
  "delta_ndvi": feat_a["ndvi"] - feat_b["ndvi"],
@@ -1000,8 +1021,10 @@ def _extract_differential_features(before_crop, after_crop):
1000
  "delta_corners": corners_a - corners_b,
1001
  "lines_after": lines_a, "corners_after": corners_a,
1002
  "lines_before": lines_b, "corners_before": corners_b,
 
1003
  "hull_ratio_after": hull_a,
1004
  "lab_color_distance": lab_dist,
 
1005
  }
1006
 
1007
 
@@ -1102,60 +1125,145 @@ def classify_object_type(image_region, bbox, before_region=None):
1102
  # ---- New Construction/Building ----
1103
  bld = 0.0
1104
  if diff:
1105
- if diff["delta_edge_density"] > 15:
1106
- bld += 0.20
1107
- if diff["delta_orientation_entropy"] < -0.4:
1108
- bld += 0.15
1109
- if diff["delta_lines"] > 5:
 
 
 
 
 
 
 
1110
  bld += 0.15
1111
- if diff["delta_corners"] > 8:
1112
- bld += 0.12
1113
- if diff["after"]["ndvi"] < 0.05 and diff["before"]["ndvi"] > 0.03:
1114
- bld += 0.12
1115
- if diff["hull_ratio_after"] > 0.55:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1116
  bld += 0.10
1117
- if 1.0 <= aspect_ratio <= 4.0:
 
 
 
 
 
 
1118
  bld += 0.08
1119
- if area > 1000:
 
 
 
 
 
 
 
 
1120
  bld += 0.05
 
 
 
 
 
 
 
 
 
 
 
1121
  else:
1122
  if feat_a["orientation_entropy"] < 2.5:
1123
  bld += 0.18
1124
  if feat_a["color_homogeneity"] < 28:
1125
  bld += 0.15
1126
- if 1.0 <= aspect_ratio <= 4.0:
1127
- bld += 0.12
1128
- if 0.3 <= compactness <= 0.9:
1129
  bld += 0.10
1130
- if feat_a["edge_density"] > 30:
1131
- bld += 0.12
1132
- if feat_a["glcm_contrast"] > 400:
1133
  bld += 0.10
1134
- if feat_a["saturation"] < 90:
 
 
1135
  bld += 0.10
1136
- if 40 <= feat_a["brightness"] <= 90:
 
 
1137
  bld += 0.08
1138
- if area > 1000:
1139
  bld += 0.05
1140
  scores["New Construction/Building"] = bld
1141
 
1142
  # ---- Demolition/Clearing ----
1143
  demo = 0.0
1144
  if diff:
1145
- if diff["delta_edge_density"] < -15:
 
1146
  demo += 0.22
1147
- if diff["delta_lines"] < -5:
 
 
 
 
 
 
1148
  demo += 0.18
1149
- if diff["delta_corners"] < -8:
 
 
 
 
 
 
1150
  demo += 0.15
 
 
 
 
 
1151
  if diff["delta_texture_std"] > 8:
1152
- demo += 0.12
1153
  if diff["delta_brightness"] > 10:
1154
- demo += 0.12
1155
- if diff["after"]["ndvi"] > 0.03 and diff["before"]["ndvi"] < 0.02:
 
 
 
 
 
1156
  demo += 0.08
1157
- if area > 800:
1158
- demo += 0.05
 
 
 
 
 
 
1159
  else:
1160
  if feat_a["texture_std"] > 30:
1161
  demo += 0.18
@@ -1904,7 +2012,7 @@ def analyze_change_regions(change_mask, image, min_area=400, use_ensemble=True,
1904
  # - keeps sensitivity on smaller images
1905
  # - suppresses speckle noise on larger images
1906
  if min_area is None:
1907
- min_area = int(max(350, min(1400, img_area * 0.00012)))
1908
 
1909
  for i in range(1, num_labels):
1910
  raw_area = stats[i, cv2.CC_STAT_AREA]
 
593
  from .model_inference import is_model_available, predict_change_mask
594
 
595
  if is_model_available():
596
+ threshold = 0.25 + (1.0 - sensitivity) * 0.25
597
  try:
598
  change_mask, score_map = predict_change_mask(
599
  img1, img2, threshold=threshold)
 
705
  filled = cv2.dilate(filled, k_break, iterations=1)
706
 
707
  # 7. Component-level filtering: remove tiny survivors and elongated noise
708
+ min_component_px = max(50, int(h * w * 0.00003))
709
  num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(filled, connectivity=8)
710
  clean = np.zeros_like(filled)
711
  for i in range(1, num_labels):
 
980
  lines_a, linelen_a = _count_line_segments(gray_a)
981
  corners_b = _count_corners(gray_b)
982
  corners_a = _count_corners(gray_a)
983
+ hull_b = _rectangular_hull_ratio(gray_b)
984
  hull_a = _rectangular_hull_ratio(gray_a)
985
 
986
  lab_b = cv2.cvtColor(before_crop, cv2.COLOR_RGB2LAB).astype(np.float32)
987
  lab_a = cv2.cvtColor(after_crop, cv2.COLOR_RGB2LAB).astype(np.float32)
988
  lab_dist = float(np.mean(np.sqrt(np.sum((lab_a - lab_b) ** 2, axis=2))))
989
 
990
+ # Fast SSIM approximation using cv2 (avoids scikit-image dependency)
991
+ ssim_val = 1.0
992
+ try:
993
+ if gray_b.shape == gray_a.shape and gray_b.shape[0] >= 7 and gray_b.shape[1] >= 7:
994
+ C1 = (0.01 * 255) ** 2
995
+ C2 = (0.03 * 255) ** 2
996
+ fb = gray_b.astype(np.float64)
997
+ fa = gray_a.astype(np.float64)
998
+ mu_b = cv2.GaussianBlur(fb, (11, 11), 1.5)
999
+ mu_a = cv2.GaussianBlur(fa, (11, 11), 1.5)
1000
+ sig_b2 = cv2.GaussianBlur(fb * fb, (11, 11), 1.5) - mu_b * mu_b
1001
+ sig_a2 = cv2.GaussianBlur(fa * fa, (11, 11), 1.5) - mu_a * mu_a
1002
+ sig_ba = cv2.GaussianBlur(fb * fa, (11, 11), 1.5) - mu_b * mu_a
1003
+ numer = (2 * mu_b * mu_a + C1) * (2 * sig_ba + C2)
1004
+ denom = (mu_b ** 2 + mu_a ** 2 + C1) * (sig_b2 + sig_a2 + C2)
1005
+ ssim_map = numer / (denom + 1e-12)
1006
+ ssim_val = float(np.mean(ssim_map))
1007
+ except Exception:
1008
+ pass
1009
+
1010
  return {
1011
  "before": feat_b, "after": feat_a,
1012
  "delta_ndvi": feat_a["ndvi"] - feat_b["ndvi"],
 
1021
  "delta_corners": corners_a - corners_b,
1022
  "lines_after": lines_a, "corners_after": corners_a,
1023
  "lines_before": lines_b, "corners_before": corners_b,
1024
+ "hull_ratio_before": hull_b,
1025
  "hull_ratio_after": hull_a,
1026
  "lab_color_distance": lab_dist,
1027
+ "ssim": ssim_val,
1028
  }
1029
 
1030
 
 
1125
  # ---- New Construction/Building ----
1126
  bld = 0.0
1127
  if diff:
1128
+ # Edge density increase: strong for buildings, lower threshold to catch smaller ones
1129
+ ded = diff["delta_edge_density"]
1130
+ if ded > 20:
1131
+ bld += 0.22
1132
+ elif ded > 10:
1133
+ bld += 0.16
1134
+ elif ded > 5:
1135
+ bld += 0.08
1136
+
1137
+ # More ordered structure (lower entropy = more regular geometry)
1138
+ doe = diff["delta_orientation_entropy"]
1139
+ if doe < -0.6:
1140
  bld += 0.15
1141
+ elif doe < -0.2:
1142
+ bld += 0.10
1143
+
1144
+ # New straight lines appearing
1145
+ dl = diff["delta_lines"]
1146
+ if dl > 8:
1147
+ bld += 0.16
1148
+ elif dl > 3:
1149
+ bld += 0.10
1150
+ elif dl > 1:
1151
+ bld += 0.05
1152
+
1153
+ # New corners appearing
1154
+ dc = diff["delta_corners"]
1155
+ if dc > 10:
1156
+ bld += 0.14
1157
+ elif dc > 4:
1158
+ bld += 0.10
1159
+ elif dc > 1:
1160
+ bld += 0.05
1161
+
1162
+ # Vegetation replaced by non-vegetation
1163
+ if diff["after"]["ndvi"] < 0.08 and diff["before"]["ndvi"] > 0.02:
1164
+ bld += 0.10
1165
+ # Brightness increase (concrete/roofing vs bare ground)
1166
+ if diff["delta_brightness"] > 8:
1167
+ bld += 0.06
1168
+ # Rectangular shape in after image
1169
+ if diff["hull_ratio_after"] > 0.50:
1170
  bld += 0.10
1171
+ elif diff["hull_ratio_after"] > 0.35:
1172
+ bld += 0.05
1173
+ # After image has structural features even if delta is modest
1174
+ if diff["lines_after"] > 4 and diff["corners_after"] > 6:
1175
+ bld += 0.08
1176
+ # LAB color distance (significant visual change)
1177
+ if diff["lab_color_distance"] > 25:
1178
  bld += 0.08
1179
+ elif diff["lab_color_distance"] > 15:
1180
+ bld += 0.04
1181
+ # SSIM: low = big structural change; very important for building detection
1182
+ ssim = diff.get("ssim", 1.0)
1183
+ if ssim < 0.4:
1184
+ bld += 0.14
1185
+ elif ssim < 0.6:
1186
+ bld += 0.10
1187
+ elif ssim < 0.75:
1188
  bld += 0.05
1189
+ # Low NDVI + high edge density in after = likely built structure
1190
+ if diff["after"]["ndvi"] < 0.05 and diff["after"]["edge_density"] > 25:
1191
+ bld += 0.08
1192
+ # New rectangular shape appearing (hull increased)
1193
+ hull_delta = diff["hull_ratio_after"] - diff.get("hull_ratio_before", 0)
1194
+ if hull_delta > 0.2:
1195
+ bld += 0.06
1196
+ if 1.0 <= aspect_ratio <= 5.0:
1197
+ bld += 0.06
1198
+ if area > 600:
1199
+ bld += 0.04
1200
  else:
1201
  if feat_a["orientation_entropy"] < 2.5:
1202
  bld += 0.18
1203
  if feat_a["color_homogeneity"] < 28:
1204
  bld += 0.15
1205
+ if 1.0 <= aspect_ratio <= 5.0:
 
 
1206
  bld += 0.10
1207
+ if 0.2 <= compactness <= 0.95:
 
 
1208
  bld += 0.10
1209
+ if feat_a["edge_density"] > 25:
1210
+ bld += 0.14
1211
+ if feat_a["glcm_contrast"] > 300:
1212
  bld += 0.10
1213
+ if feat_a["saturation"] < 100:
1214
+ bld += 0.08
1215
+ if 30 <= feat_a["brightness"] <= 95:
1216
  bld += 0.08
1217
+ if area > 600:
1218
  bld += 0.05
1219
  scores["New Construction/Building"] = bld
1220
 
1221
  # ---- Demolition/Clearing ----
1222
  demo = 0.0
1223
  if diff:
1224
+ ded_neg = diff["delta_edge_density"]
1225
+ if ded_neg < -20:
1226
  demo += 0.22
1227
+ elif ded_neg < -10:
1228
+ demo += 0.16
1229
+ elif ded_neg < -5:
1230
+ demo += 0.08
1231
+
1232
+ dl_neg = diff["delta_lines"]
1233
+ if dl_neg < -8:
1234
  demo += 0.18
1235
+ elif dl_neg < -3:
1236
+ demo += 0.12
1237
+ elif dl_neg < -1:
1238
+ demo += 0.05
1239
+
1240
+ dc_neg = diff["delta_corners"]
1241
+ if dc_neg < -10:
1242
  demo += 0.15
1243
+ elif dc_neg < -4:
1244
+ demo += 0.10
1245
+ elif dc_neg < -1:
1246
+ demo += 0.05
1247
+
1248
  if diff["delta_texture_std"] > 8:
1249
+ demo += 0.10
1250
  if diff["delta_brightness"] > 10:
1251
+ demo += 0.10
1252
+ # Structural features disappeared
1253
+ if diff["lines_before"] > 4 and diff["lines_after"] <= 1:
1254
+ demo += 0.10
1255
+ # Hull ratio dropped (rectangular structure removed)
1256
+ hull_drop = diff.get("hull_ratio_before", 0) - diff["hull_ratio_after"]
1257
+ if hull_drop > 0.2:
1258
  demo += 0.08
1259
+ # SSIM confirms big structural change
1260
+ ssim = diff.get("ssim", 1.0)
1261
+ if ssim < 0.5:
1262
+ demo += 0.08
1263
+ if diff["after"]["ndvi"] > 0.03 and diff["before"]["ndvi"] < 0.02:
1264
+ demo += 0.06
1265
+ if area > 500:
1266
+ demo += 0.04
1267
  else:
1268
  if feat_a["texture_std"] > 30:
1269
  demo += 0.18
 
2012
  # - keeps sensitivity on smaller images
2013
  # - suppresses speckle noise on larger images
2014
  if min_area is None:
2015
+ min_area = int(max(200, min(1000, img_area * 0.00008)))
2016
 
2017
  for i in range(1, num_labels):
2018
  raw_area = stats[i, cv2.CC_STAT_AREA]
app/model_inference.py CHANGED
@@ -19,7 +19,7 @@ _MODEL = None
19
  _PROCESSOR = None
20
  _DEVICE = None
21
  _MODEL_ID = "deepang/adaptformer-LEVIR-CD"
22
- _TILE_SIZE = 512
23
  _AVAILABLE = None
24
 
25
 
@@ -68,8 +68,9 @@ def _load_model():
68
  def predict_change_mask(img1, img2, threshold=0.5):
69
  """
70
  Run AdaptFormer inference on two RGB numpy arrays (H, W, 3).
71
- Images are split into overlapping tiles, predicted individually,
72
- and stitched back into a full-resolution binary mask.
 
73
 
74
  Returns (uint8 mask [0 or 255], float32 score map [0-1]).
75
  """
@@ -82,7 +83,8 @@ def predict_change_mask(img1, img2, threshold=0.5):
82
 
83
  h, w = img1.shape[:2]
84
  tile = _TILE_SIZE
85
- stride = tile * 3 // 4
 
86
 
87
  pad_h = (tile - h % tile) % tile
88
  pad_w = (tile - w % tile) % tile
@@ -94,6 +96,12 @@ def predict_change_mask(img1, img2, threshold=0.5):
94
  score_sum = np.zeros((ph, pw), dtype=np.float32)
95
  count = np.zeros((ph, pw), dtype=np.float32)
96
 
 
 
 
 
 
 
97
  with torch.no_grad():
98
  for y0 in range(0, ph - tile + 1, stride):
99
  for x0 in range(0, pw - tile + 1, stride):
@@ -110,7 +118,6 @@ def predict_change_mask(img1, img2, threshold=0.5):
110
  logits = outputs.logits
111
  probs = torch.softmax(logits, dim=1)
112
 
113
- # Class 1 = change
114
  prob_map = probs[0, 1].cpu().numpy()
115
 
116
  out_h, out_w = prob_map.shape
@@ -118,10 +125,10 @@ def predict_change_mask(img1, img2, threshold=0.5):
118
  prob_map = cv2.resize(prob_map, (tile, tile),
119
  interpolation=cv2.INTER_LINEAR)
120
 
121
- score_sum[y0:y0+tile, x0:x0+tile] += prob_map
122
- count[y0:y0+tile, x0:x0+tile] += 1.0
123
 
124
- count = np.maximum(count, 1.0)
125
  avg_score = score_sum / count
126
  avg_score = avg_score[:h, :w]
127
 
 
19
  _PROCESSOR = None
20
  _DEVICE = None
21
  _MODEL_ID = "deepang/adaptformer-LEVIR-CD"
22
+ _TILE_SIZE = 256 # LEVIR-CD native patch size
23
  _AVAILABLE = None
24
 
25
 
 
68
  def predict_change_mask(img1, img2, threshold=0.5):
69
  """
70
  Run AdaptFormer inference on two RGB numpy arrays (H, W, 3).
71
+ Images are split into overlapping 256x256 tiles (matching LEVIR-CD
72
+ training resolution), predicted individually, and stitched back into
73
+ a full-resolution binary mask.
74
 
75
  Returns (uint8 mask [0 or 255], float32 score map [0-1]).
76
  """
 
83
 
84
  h, w = img1.shape[:2]
85
  tile = _TILE_SIZE
86
+ overlap = tile // 4 # 64px overlap
87
+ stride = tile - overlap # 192
88
 
89
  pad_h = (tile - h % tile) % tile
90
  pad_w = (tile - w % tile) % tile
 
96
  score_sum = np.zeros((ph, pw), dtype=np.float32)
97
  count = np.zeros((ph, pw), dtype=np.float32)
98
 
99
+ # Blending weight: raised-cosine window avoids hard tile boundary seams
100
+ ramp = np.linspace(0, 1, overlap)
101
+ flat = np.ones(tile - 2 * overlap)
102
+ profile = np.concatenate([ramp, flat, ramp[::-1]])
103
+ weight_2d = np.outer(profile, profile).astype(np.float32)
104
+
105
  with torch.no_grad():
106
  for y0 in range(0, ph - tile + 1, stride):
107
  for x0 in range(0, pw - tile + 1, stride):
 
118
  logits = outputs.logits
119
  probs = torch.softmax(logits, dim=1)
120
 
 
121
  prob_map = probs[0, 1].cpu().numpy()
122
 
123
  out_h, out_w = prob_map.shape
 
125
  prob_map = cv2.resize(prob_map, (tile, tile),
126
  interpolation=cv2.INTER_LINEAR)
127
 
128
+ score_sum[y0:y0+tile, x0:x0+tile] += prob_map * weight_2d
129
+ count[y0:y0+tile, x0:x0+tile] += weight_2d
130
 
131
+ count = np.maximum(count, 1e-6)
132
  avg_score = score_sum / count
133
  avg_score = avg_score[:h, :w]
134
 
templates/index.html CHANGED
@@ -4,7 +4,7 @@
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <title>AI Change Detection</title>
7
- <link rel="stylesheet" href="/static/css/style.css?v=22" />
8
  </head>
9
  <body>
10
  <div class="app">
@@ -360,6 +360,6 @@
360
  </div>
361
  </div>
362
 
363
- <script src="/static/js/app.js?v=37"></script>
364
  </body>
365
  </html>
 
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <title>AI Change Detection</title>
7
+ <link rel="stylesheet" href="/static/css/style.css?v=23" />
8
  </head>
9
  <body>
10
  <div class="app">
 
360
  </div>
361
  </div>
362
 
363
+ <script src="/static/js/app.js?v=38"></script>
364
  </body>
365
  </html>