Spaces:
Sleeping
Sleeping
Tune segmentation: reduce over-splitting and clean polygon shapes
Browse files- Fusion: raise split threshold from 15% to 30% of plane (RADIO roof
prompts are too similar to split on small differences)
- Fusion: raise min component size from 200 to 500 px, fraction from
10% to 20% of plane
- Merge: raise small fragment threshold from 3% to 5% of building area
- GeoJSON: increase Douglas-Peucker epsilon from 1% to 2.5% of perimeter
for cleaner 4-6 vertex polygons instead of jagged 10-16
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- fusion.py +7 -5
- geo_export.py +2 -2
- pipeline.py +1 -1
fusion.py
CHANGED
|
@@ -66,11 +66,13 @@ def fuse_segmentations(
|
|
| 66 |
continue
|
| 67 |
|
| 68 |
fraction = count / plane_pixels
|
| 69 |
-
if fraction < 0.
|
| 70 |
-
#
|
|
|
|
|
|
|
| 71 |
continue
|
| 72 |
|
| 73 |
-
# This class represents >
|
| 74 |
sub_mask = plane_mask & (radio_roof_class == cls)
|
| 75 |
labeled_sub, n_components = label(sub_mask)
|
| 76 |
|
|
@@ -79,7 +81,7 @@ def fuse_segmentations(
|
|
| 79 |
comp_size = comp_mask.sum()
|
| 80 |
|
| 81 |
# Only split if the sub-region is spatially coherent and large enough
|
| 82 |
-
if comp_size > plane_pixels * 0.
|
| 83 |
fused[comp_mask] = next_id
|
| 84 |
next_id += 1
|
| 85 |
|
|
@@ -108,7 +110,7 @@ def split_disconnected_regions(label_map: np.ndarray) -> np.ndarray:
|
|
| 108 |
def merge_small_fragments(
|
| 109 |
label_map: np.ndarray,
|
| 110 |
building_mask: np.ndarray,
|
| 111 |
-
min_fraction: float = 0.
|
| 112 |
) -> np.ndarray:
|
| 113 |
"""Merge fragments smaller than min_fraction of building area.
|
| 114 |
|
|
|
|
| 66 |
continue
|
| 67 |
|
| 68 |
fraction = count / plane_pixels
|
| 69 |
+
if fraction < 0.30:
|
| 70 |
+
# Must represent >30% of the plane to justify a split.
|
| 71 |
+
# The RADIO roof-type prompts are similar ("flat/pitched/hip/gable
|
| 72 |
+
# roof plane"), so smaller fractions are noise, not real sub-features.
|
| 73 |
continue
|
| 74 |
|
| 75 |
+
# This class represents >30% of the plane — check spatial coherence
|
| 76 |
sub_mask = plane_mask & (radio_roof_class == cls)
|
| 77 |
labeled_sub, n_components = label(sub_mask)
|
| 78 |
|
|
|
|
| 81 |
comp_size = comp_mask.sum()
|
| 82 |
|
| 83 |
# Only split if the sub-region is spatially coherent and large enough
|
| 84 |
+
if comp_size > plane_pixels * 0.20 and comp_size > 500:
|
| 85 |
fused[comp_mask] = next_id
|
| 86 |
next_id += 1
|
| 87 |
|
|
|
|
| 110 |
def merge_small_fragments(
|
| 111 |
label_map: np.ndarray,
|
| 112 |
building_mask: np.ndarray,
|
| 113 |
+
min_fraction: float = 0.05,
|
| 114 |
) -> np.ndarray:
|
| 115 |
"""Merge fragments smaller than min_fraction of building area.
|
| 116 |
|
geo_export.py
CHANGED
|
@@ -101,9 +101,9 @@ def labels_to_geojson(
|
|
| 101 |
if area_sqft < min_area_sqft:
|
| 102 |
continue
|
| 103 |
|
| 104 |
-
# Simplify polygon (
|
| 105 |
perimeter = cv2.arcLength(contour, True)
|
| 106 |
-
epsilon = 0.
|
| 107 |
simplified = cv2.approxPolyDP(contour, epsilon, True)
|
| 108 |
|
| 109 |
coords = [pixel_to_geo(pt[0][0], pt[0][1], w, h, bounds) for pt in simplified]
|
|
|
|
| 101 |
if area_sqft < min_area_sqft:
|
| 102 |
continue
|
| 103 |
|
| 104 |
+
# Simplify polygon (2.5% of perimeter — clean roof plane shapes)
|
| 105 |
perimeter = cv2.arcLength(contour, True)
|
| 106 |
+
epsilon = 0.025 * perimeter
|
| 107 |
simplified = cv2.approxPolyDP(contour, epsilon, True)
|
| 108 |
|
| 109 |
coords = [pixel_to_geo(pt[0][0], pt[0][1], w, h, bounds) for pt in simplified]
|
pipeline.py
CHANGED
|
@@ -158,7 +158,7 @@ def run(
|
|
| 158 |
result.status.append("Fusing geometry + appearance...")
|
| 159 |
fused = fuse_segmentations(ransac_labels, score_map, cropped_mask)
|
| 160 |
fused = split_disconnected_regions(fused)
|
| 161 |
-
fused = merge_small_fragments(fused, cropped_mask, min_fraction=0.
|
| 162 |
result.fused_labels = fused
|
| 163 |
|
| 164 |
n_final = len(set(np.unique(fused)) - {0})
|
|
|
|
| 158 |
result.status.append("Fusing geometry + appearance...")
|
| 159 |
fused = fuse_segmentations(ransac_labels, score_map, cropped_mask)
|
| 160 |
fused = split_disconnected_regions(fused)
|
| 161 |
+
fused = merge_small_fragments(fused, cropped_mask, min_fraction=0.05)
|
| 162 |
result.fused_labels = fused
|
| 163 |
|
| 164 |
n_final = len(set(np.unique(fused)) - {0})
|