kerojohan commited on
Commit
f036731
·
1 Parent(s): cc94020

Align Space detector logic with main

Browse files
Files changed (2) hide show
  1. app.py +29 -36
  2. detect_cave.py +36 -49
app.py CHANGED
@@ -169,65 +169,58 @@ def _process_array(img_rgb: np.ndarray):
169
  best_mask = candidate_hw
170
 
171
  # Post-selection expansion
172
- pre_expansion_mask = best_mask.copy()
173
  best_area_frac = np.count_nonzero(best_mask) / (h * w)
174
  if best_area_frac < 0.25:
175
- orig_mean = float(gray_f32[best_mask > 0].mean())
176
- br_size = max(9, int(min(h, w) * 0.02) | 1)
177
- br_k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (br_size, br_size))
178
- reach_r = max(15, int(min(h, w) * 0.04))
179
- reach_k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,
180
- (2*reach_r+1, 2*reach_r+1))
181
- base_pct = min(50, max(30, int(scores.get("area_frac", 0.1) * 100 * 4)))
182
- relax_thr = int(np.percentile(proc["denoised"], base_pct))
183
  _, relax_dark = cv2.threshold(proc["denoised"], relax_thr, 255,
184
  cv2.THRESH_BINARY_INV)
 
 
 
 
185
  relax_dark = cv2.morphologyEx(relax_dark, cv2.MORPH_CLOSE, br_k)
186
  n_rd, labels_rd, _, _ = cv2.connectedComponentsWithStats(relax_dark, 8)
187
- seed_reach = cv2.dilate(best_mask, reach_k)
188
- overlap_labels = set(np.unique(labels_rd[seed_reach > 0])) - {0}
189
  if overlap_labels:
190
  expanded = np.zeros_like(best_mask)
191
  for lb in overlap_labels:
192
  expanded[labels_rd == lb] = 255
193
- clip_lc = actual_lc if actual_lc > lc else lc
194
- clip_rc = actual_rc if actual_rc < rc else rc
195
- if clip_lc > int(w * 0.05):
196
- expanded[:, :clip_lc] = 0
197
- if clip_rc < int(w * 0.95):
198
- expanded[:, clip_rc+1:] = 0
199
  n_exp, labels_exp, stats_exp, _ = cv2.connectedComponentsWithStats(
200
  expanded, 8)
201
  if n_exp > 1:
202
  largest_exp = 1 + np.argmax(stats_exp[1:, cv2.CC_STAT_AREA])
203
  expanded = ((labels_exp == largest_exp) * 255).astype(np.uint8)
204
  exp_area_frac = np.count_nonzero(expanded) / (h * w)
205
- exp_mean = float(gray_f32[expanded > 0].mean())
206
- if (exp_area_frac <= 0.40
207
- and exp_area_frac > best_area_frac * 0.8
208
- and exp_mean < orig_mean + 0.15):
209
- best_mask = expanded
210
- best_area_frac = exp_area_frac
 
 
 
 
 
 
 
211
 
212
  # GrabCut
213
- pre_gc = np.count_nonzero(best_mask) / (h * w)
214
- pre_exp_frac = np.count_nonzero(pre_expansion_mask) / (h * w)
215
- use_conservative = (pre_gc > pre_exp_frac * 1.3)
216
- gc_result = grabcut_refine(
217
- gray_u8, best_mask,
218
- conservative_mask=pre_expansion_mask if use_conservative else None,
219
- expand_ratio=2.5,
220
- )
221
  if np.count_nonzero(gc_result) > 0:
222
  best_mask = gc_result
223
 
224
  refined = refine_mask(best_mask, gray_f32)
225
 
226
- # Hard-clip to actual illumination columns — mirrors process_image in detect_cave.py
227
- if actual_lc > int(w * 0.05):
228
- refined[:, :actual_lc] = 0
229
- if actual_rc < int(w * 0.95):
230
- refined[:, actual_rc + 1:] = 0
231
 
232
  result_rgb, mask_rgb, valid_rgb, cands_rgb = _draw_result_arrays(
233
  gray_u8, refined, scores, wmap, pn, candidates, all_sc
 
169
  best_mask = candidate_hw
170
 
171
  # Post-selection expansion
 
172
  best_area_frac = np.count_nonzero(best_mask) / (h * w)
173
  if best_area_frac < 0.25:
174
+ relax_pct = min(50, max(30, int(scores.get("area_frac", 0.1) * 100 * 4)))
175
+ relax_thr = int(np.percentile(proc["denoised"], relax_pct))
 
 
 
 
 
 
176
  _, relax_dark = cv2.threshold(proc["denoised"], relax_thr, 255,
177
  cv2.THRESH_BINARY_INV)
178
+ br_k = cv2.getStructuringElement(
179
+ cv2.MORPH_ELLIPSE,
180
+ (max(9, int(min(h, w) * 0.02) | 1), max(9, int(min(h, w) * 0.02) | 1)),
181
+ )
182
  relax_dark = cv2.morphologyEx(relax_dark, cv2.MORPH_CLOSE, br_k)
183
  n_rd, labels_rd, _, _ = cv2.connectedComponentsWithStats(relax_dark, 8)
184
+ overlap_labels = set(np.unique(labels_rd[best_mask > 0])) - {0}
 
185
  if overlap_labels:
186
  expanded = np.zeros_like(best_mask)
187
  for lb in overlap_labels:
188
  expanded[labels_rd == lb] = 255
189
+ if lc > int(w * 0.05):
190
+ expanded[:, :lc] = 0
191
+ if rc < int(w * 0.95):
192
+ expanded[:, rc+1:] = 0
 
 
193
  n_exp, labels_exp, stats_exp, _ = cv2.connectedComponentsWithStats(
194
  expanded, 8)
195
  if n_exp > 1:
196
  largest_exp = 1 + np.argmax(stats_exp[1:, cv2.CC_STAT_AREA])
197
  expanded = ((labels_exp == largest_exp) * 255).astype(np.uint8)
198
  exp_area_frac = np.count_nonzero(expanded) / (h * w)
199
+ if exp_area_frac <= 0.40 and exp_area_frac > best_area_frac * 0.8:
200
+ exp_mean = float(gray_f32[expanded > 0].mean())
201
+ orig_mean = float(gray_f32[best_mask > 0].mean())
202
+ orig_pts = np.argwhere(best_mask > 0).astype(np.float32)
203
+ exp_pts = np.argwhere(expanded > 0).astype(np.float32)
204
+ orig_cy_m, orig_cx_m = orig_pts.mean(axis=0)
205
+ exp_cy_m, exp_cx_m = exp_pts.mean(axis=0)
206
+ centroid_shift = (
207
+ np.sqrt((exp_cx_m - orig_cx_m) ** 2 + (exp_cy_m - orig_cy_m) ** 2)
208
+ / min(h, w)
209
+ )
210
+ if exp_mean < orig_mean + 0.15 and centroid_shift <= 0.20:
211
+ best_mask = expanded
212
 
213
  # GrabCut
214
+ gc_result = grabcut_refine(gray_u8, best_mask, expand_ratio=2.0)
 
 
 
 
 
 
 
215
  if np.count_nonzero(gc_result) > 0:
216
  best_mask = gc_result
217
 
218
  refined = refine_mask(best_mask, gray_f32)
219
 
220
+ if lc > int(w * 0.05):
221
+ refined[:, :lc] = 0
222
+ if rc < int(w * 0.95):
223
+ refined[:, rc + 1:] = 0
 
224
 
225
  result_rgb, mask_rgb, valid_rgb, cands_rgb = _draw_result_arrays(
226
  gray_u8, refined, scores, wmap, pn, candidates, all_sc
detect_cave.py CHANGED
@@ -874,67 +874,54 @@ def process_image(input_path, output_dir):
874
 
875
  # ── Post-selection expansion ──────────────────────────────────────────────
876
  # Grow selected mask into connected dark pixels at a relaxed threshold.
877
- # Uses a dilated seed (4% reach) so nearby dark components separated by
878
- # a thin lighter band are bridged.
879
- # pre_expansion_mask is saved for GrabCut's conservative-FG initialisation.
880
- pre_expansion_mask = best_mask.copy()
881
  best_area_frac = np.count_nonzero(best_mask) / (h * w)
882
  if best_area_frac < 0.25:
883
- orig_mean = float(gray_f32[best_mask > 0].mean())
884
- br_size = max(9, int(min(h, w) * 0.02) | 1)
885
- br_k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (br_size, br_size))
886
- reach_r = max(15, int(min(h, w) * 0.04))
887
- reach_k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,
888
- (2*reach_r+1, 2*reach_r+1))
889
-
890
- base_pct = min(50, max(30, int(scores.get("area_frac", 0.1) * 100 * 4)))
891
- relax_thr = int(np.percentile(proc["denoised"], base_pct))
892
  _, relax_dark = cv2.threshold(proc["denoised"], relax_thr, 255,
893
  cv2.THRESH_BINARY_INV)
 
 
 
894
  relax_dark = cv2.morphologyEx(relax_dark, cv2.MORPH_CLOSE, br_k)
895
  n_rd, labels_rd, _, _ = cv2.connectedComponentsWithStats(relax_dark, 8)
896
- seed_reach = cv2.dilate(best_mask, reach_k)
897
- overlap_labels = set(np.unique(labels_rd[seed_reach > 0])) - {0}
898
  if overlap_labels:
899
  expanded = np.zeros_like(best_mask)
900
  for lb in overlap_labels:
901
  expanded[labels_rd == lb] = 255
902
- # When the profile doesn't rise until well past the capped lc,
903
- # there is a significant lateral zone → clip at the actual rise
904
- # column to prevent the expansion from leaking into it.
905
- clip_lc = actual_lc if actual_lc > lc else lc
906
- clip_rc = actual_rc if actual_rc < rc else rc
907
- if clip_lc > int(w * 0.05):
908
- expanded[:, :clip_lc] = 0
909
- if clip_rc < int(w * 0.95):
910
- expanded[:, clip_rc+1:] = 0
911
  n_exp, labels_exp, stats_exp, _ = cv2.connectedComponentsWithStats(
912
  expanded, 8)
913
  if n_exp > 1:
914
  largest_exp = 1 + np.argmax(stats_exp[1:, cv2.CC_STAT_AREA])
915
  expanded = ((labels_exp == largest_exp) * 255).astype(np.uint8)
916
  exp_area_frac = np.count_nonzero(expanded) / (h * w)
917
- exp_mean = float(gray_f32[expanded > 0].mean())
918
- if (exp_area_frac <= 0.40
919
- and exp_area_frac > best_area_frac * 0.8
920
- and exp_mean < orig_mean + 0.15):
921
- print(f" [{bn}] expanded {best_area_frac*100:.1f}% "
922
- f"{exp_area_frac*100:.1f}%")
923
- best_mask = expanded
924
- best_area_frac = exp_area_frac
 
 
 
 
 
 
 
 
925
 
926
  # ── GrabCut boundary refinement ───────────────────────────────────────────
927
- # Pass pre_expansion_mask as conservative FG when the mask has grown
928
- # significantly — this anchors the definite-FG model on the clean core
929
- # and lets GrabCut decide whether to include the dark interior or not.
930
  pre_gc = np.count_nonzero(best_mask) / (h * w)
931
- pre_exp_frac = np.count_nonzero(pre_expansion_mask) / (h * w)
932
- use_conservative = (pre_gc > pre_exp_frac * 1.3)
933
- gc_result = grabcut_refine(
934
- gray_u8, best_mask,
935
- conservative_mask=pre_expansion_mask if use_conservative else None,
936
- expand_ratio=2.5
937
- )
938
  post_gc = np.count_nonzero(gc_result) / (h * w)
939
  if post_gc > 0:
940
  print(f" [{bn}] grabcut {pre_gc*100:.1f}% → {post_gc*100:.1f}%")
@@ -942,13 +929,13 @@ def process_image(input_path, output_dir):
942
 
943
  refined = refine_mask(best_mask, gray_f32)
944
 
945
- # Hard-clip final mask to the actual illumination columns (not the capped
946
- # lc/rc) to exclude IR vignette/penumbra that GrabCut or contour smoothing
947
- # may have reintroduced after the expansion clip.
948
- if actual_lc > int(w * 0.05):
949
- refined[:, :actual_lc] = 0
950
- if actual_rc < int(w * 0.95):
951
- refined[:, actual_rc + 1:] = 0
952
 
953
  draw_result(gray_u8, refined, scores,
954
  out_r, out_m, out_dv,
 
874
 
875
  # ── Post-selection expansion ──────────────────────────────────────────────
876
  # Grow selected mask into connected dark pixels at a relaxed threshold.
877
+ # Captures the full entrance when the initial candidate covers only the core.
 
 
 
878
  best_area_frac = np.count_nonzero(best_mask) / (h * w)
879
  if best_area_frac < 0.25:
880
+ relax_pct = min(50, max(30, int(scores.get("area_frac", 0.1) * 100 * 4)))
881
+ relax_thr = int(np.percentile(proc["denoised"], relax_pct))
 
 
 
 
 
 
 
882
  _, relax_dark = cv2.threshold(proc["denoised"], relax_thr, 255,
883
  cv2.THRESH_BINARY_INV)
884
+ br_k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,
885
+ (max(9, int(min(h,w)*0.02)|1),
886
+ max(9, int(min(h,w)*0.02)|1)))
887
  relax_dark = cv2.morphologyEx(relax_dark, cv2.MORPH_CLOSE, br_k)
888
  n_rd, labels_rd, _, _ = cv2.connectedComponentsWithStats(relax_dark, 8)
889
+ overlap_labels = set(np.unique(labels_rd[best_mask > 0])) - {0}
 
890
  if overlap_labels:
891
  expanded = np.zeros_like(best_mask)
892
  for lb in overlap_labels:
893
  expanded[labels_rd == lb] = 255
894
+ # Clip to valid columns to prevent re-introducing lateral dark zones
895
+ if lc > int(w * 0.05):
896
+ expanded[:, :lc] = 0
897
+ if rc < int(w * 0.95):
898
+ expanded[:, rc+1:] = 0
 
 
 
 
899
  n_exp, labels_exp, stats_exp, _ = cv2.connectedComponentsWithStats(
900
  expanded, 8)
901
  if n_exp > 1:
902
  largest_exp = 1 + np.argmax(stats_exp[1:, cv2.CC_STAT_AREA])
903
  expanded = ((labels_exp == largest_exp) * 255).astype(np.uint8)
904
  exp_area_frac = np.count_nonzero(expanded) / (h * w)
905
+ if exp_area_frac <= 0.40 and exp_area_frac > best_area_frac * 0.8:
906
+ exp_mean = float(gray_f32[expanded > 0].mean())
907
+ orig_mean = float(gray_f32[best_mask > 0].mean())
908
+ # Reject expansion if centroid drifted far — guards against
909
+ # bridging to a disconnected dark zone (e.g. vegetation corner).
910
+ orig_pts = np.argwhere(best_mask > 0).astype(np.float32)
911
+ exp_pts = np.argwhere(expanded > 0).astype(np.float32)
912
+ orig_cy_m, orig_cx_m = orig_pts.mean(axis=0)
913
+ exp_cy_m, exp_cx_m = exp_pts.mean(axis=0)
914
+ centroid_shift = (np.sqrt((exp_cx_m - orig_cx_m)**2
915
+ + (exp_cy_m - orig_cy_m)**2)
916
+ / min(h, w))
917
+ if exp_mean < orig_mean + 0.15 and centroid_shift <= 0.20:
918
+ print(f" [{bn}] expanded {best_area_frac*100:.1f}% → "
919
+ f"{exp_area_frac*100:.1f}%")
920
+ best_mask = expanded
921
 
922
  # ── GrabCut boundary refinement ───────────────────────────────────────────
 
 
 
923
  pre_gc = np.count_nonzero(best_mask) / (h * w)
924
+ gc_result = grabcut_refine(gray_u8, best_mask, expand_ratio=2.0)
 
 
 
 
 
 
925
  post_gc = np.count_nonzero(gc_result) / (h * w)
926
  if post_gc > 0:
927
  print(f" [{bn}] grabcut {pre_gc*100:.1f}% → {post_gc*100:.1f}%")
 
929
 
930
  refined = refine_mask(best_mask, gray_f32)
931
 
932
+ # Hard-clip final mask to valid illumination columns.
933
+ # Removes penumbra/vignetting zones from the result even when the initial
934
+ # candidate or GrabCut extended into them.
935
+ if lc > int(w * 0.05):
936
+ refined[:, :lc] = 0
937
+ if rc < int(w * 0.95):
938
+ refined[:, rc + 1:] = 0
939
 
940
  draw_result(gray_u8, refined, scores,
941
  out_r, out_m, out_dv,