kerojohan commited on
Commit ·
f036731
1
Parent(s): cc94020
Align Space detector logic with main
Browse files- app.py +29 -36
- 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 |
-
|
| 176 |
-
|
| 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 |
-
|
| 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 |
-
|
| 194 |
-
|
| 195 |
-
if
|
| 196 |
-
expanded[:, :
|
| 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 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
|
| 212 |
# GrabCut
|
| 213 |
-
|
| 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 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 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 |
-
#
|
| 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 |
-
|
| 884 |
-
|
| 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 |
-
|
| 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 |
-
#
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
|
| 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 |
-
|
| 918 |
-
|
| 919 |
-
|
| 920 |
-
|
| 921 |
-
|
| 922 |
-
|
| 923 |
-
|
| 924 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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
|
| 946 |
-
#
|
| 947 |
-
#
|
| 948 |
-
if
|
| 949 |
-
refined[:, :
|
| 950 |
-
if
|
| 951 |
-
refined[:,
|
| 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,
|