Add segmentation post-processing: dark water fix + wetland reclassify
Two heuristic rules applied after SegEarth-OV3 inference: 1. Dark water: if background pixels have mean_rgb < 0.24 and std < 0.08, reclassify as water. Fixes GTA-UAV satellite dark ocean (57% → ~15% bg). 2. Wetland reclassify (GTA-UAV only): split false-positive wetland pixels by color — green-dominant → vegetation, else → bare soil. Fixes 14.3% muddy/wetland false positives on GTA-V hillside terrain. Config flags: seg_fix_dark_water (default True), seg_reclassify_wetland (default False, enabled in run_gta_uav.py). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -63,6 +63,8 @@ def main() -> None:
|
|||||||
save_vis=True,
|
save_vis=True,
|
||||||
save_safetensors=True,
|
save_safetensors=True,
|
||||||
cleanup_npy=True,
|
cleanup_npy=True,
|
||||||
|
seg_fix_dark_water=True,
|
||||||
|
seg_reclassify_wetland=True,
|
||||||
resume=True,
|
resume=True,
|
||||||
source=source,
|
source=source,
|
||||||
log_level="INFO",
|
log_level="INFO",
|
||||||
|
|||||||
@@ -300,3 +300,76 @@ def _infer_segformer(
|
|||||||
logits.float(), size=(H, W), mode="bilinear", align_corners=False,
|
logits.float(), size=(H, W), mode="bilinear", align_corners=False,
|
||||||
)
|
)
|
||||||
return upsampled.argmax(dim=1, keepdim=True).cpu().to(torch.uint8)
|
return upsampled.argmax(dim=1, keepdim=True).cpu().to(torch.uint8)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Post-processing heuristics
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def postprocess_segmentation(
|
||||||
|
seg_ids: torch.Tensor,
|
||||||
|
images_raw: torch.Tensor,
|
||||||
|
water_class: int = 4,
|
||||||
|
dark_water_mean_thr: float = 0.24,
|
||||||
|
dark_water_std_thr: float = 0.08,
|
||||||
|
reclassify_wetland: bool = False,
|
||||||
|
wetland_class: int = 14,
|
||||||
|
vegetation_class: int = 3,
|
||||||
|
bare_soil_class: int = 11,
|
||||||
|
green_thr: float = 0.35,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""Fix known segmentation failure modes with simple heuristics.
|
||||||
|
|
||||||
|
Applied per-image after model inference.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
1. Dark water: if a background (0) image has mean_rgb < dark_water_mean_thr
|
||||||
|
and std < dark_water_std_thr, reclassify all background pixels as water.
|
||||||
|
2. Wetland reclassification (optional, for GTA-UAV): reclassify wetland pixels
|
||||||
|
by local color — green-dominant → vegetation, else → bare soil.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
seg_ids: [B, 1, H, W] uint8 class IDs.
|
||||||
|
images_raw: [B, 3, H, W] float32 [0, 1] original RGB.
|
||||||
|
water_class: class ID for water.
|
||||||
|
dark_water_mean_thr: mean RGB threshold (0-1) below which bg → water.
|
||||||
|
dark_water_std_thr: std threshold below which bg → water.
|
||||||
|
reclassify_wetland: if True, split wetland into vegetation/bare_soil.
|
||||||
|
wetland_class: class ID for muddy/wetland.
|
||||||
|
vegetation_class: class ID for vegetation.
|
||||||
|
bare_soil_class: class ID for bare soil.
|
||||||
|
green_thr: green-channel ratio threshold for vegetation vs bare soil.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
seg_ids: [B, 1, H, W] uint8, corrected in-place.
|
||||||
|
"""
|
||||||
|
B = seg_ids.shape[0]
|
||||||
|
seg = seg_ids.clone()
|
||||||
|
|
||||||
|
for i in range(B):
|
||||||
|
s = seg[i, 0] # [H, W] uint8
|
||||||
|
rgb = images_raw[i] # [3, H, W] float32
|
||||||
|
|
||||||
|
# Rule 1: dark uniform images → background becomes water
|
||||||
|
bg_mask = s == 0
|
||||||
|
if bg_mask.any():
|
||||||
|
bg_pixels = rgb[:, bg_mask] # [3, N]
|
||||||
|
mean_val = bg_pixels.mean().item()
|
||||||
|
std_val = bg_pixels.std().item()
|
||||||
|
if mean_val < dark_water_mean_thr and std_val < dark_water_std_thr:
|
||||||
|
s[bg_mask] = water_class
|
||||||
|
|
||||||
|
# Rule 2: reclassify wetland by color
|
||||||
|
if reclassify_wetland:
|
||||||
|
wet_mask = s == wetland_class
|
||||||
|
if wet_mask.any():
|
||||||
|
g = rgb[1, wet_mask] # green channel
|
||||||
|
r = rgb[0, wet_mask]
|
||||||
|
# Green-dominant → vegetation, else → bare soil
|
||||||
|
is_green = g > (r + green_thr * 0.5)
|
||||||
|
reclassed = torch.where(is_green, vegetation_class, bare_soil_class)
|
||||||
|
s[wet_mask] = reclassed.to(torch.uint8)
|
||||||
|
|
||||||
|
seg[i, 0] = s
|
||||||
|
|
||||||
|
return seg
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ class PipelineConfig:
|
|||||||
save_concat: bool = False,
|
save_concat: bool = False,
|
||||||
save_safetensors: bool = True,
|
save_safetensors: bool = True,
|
||||||
cleanup_npy: bool = False,
|
cleanup_npy: bool = False,
|
||||||
|
seg_fix_dark_water: bool = True,
|
||||||
|
seg_reclassify_wetland: bool = False,
|
||||||
resume: bool = True,
|
resume: bool = True,
|
||||||
subset: str | None = None,
|
subset: str | None = None,
|
||||||
source: str | None = None,
|
source: str | None = None,
|
||||||
@@ -30,6 +32,8 @@ class PipelineConfig:
|
|||||||
self.save_concat = save_concat
|
self.save_concat = save_concat
|
||||||
self.save_safetensors = save_safetensors
|
self.save_safetensors = save_safetensors
|
||||||
self.cleanup_npy = cleanup_npy
|
self.cleanup_npy = cleanup_npy
|
||||||
|
self.seg_fix_dark_water = seg_fix_dark_water
|
||||||
|
self.seg_reclassify_wetland = seg_reclassify_wetland
|
||||||
self.resume = resume
|
self.resume = resume
|
||||||
self.subset = subset
|
self.subset = subset
|
||||||
self.source = source
|
self.source = source
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ from src.augmentor.dataset import (
|
|||||||
)
|
)
|
||||||
from src.augmentor.inference import (
|
from src.augmentor.inference import (
|
||||||
compute_edges_from_depth, infer_depth_batch, infer_chmv2_batch,
|
compute_edges_from_depth, infer_depth_batch, infer_chmv2_batch,
|
||||||
infer_segmentation_batch,
|
infer_segmentation_batch, postprocess_segmentation,
|
||||||
)
|
)
|
||||||
from src.augmentor.io_utils import (
|
from src.augmentor.io_utils import (
|
||||||
npy_path, vis_path,
|
npy_path, vis_path,
|
||||||
@@ -302,6 +302,11 @@ def run_segmentation_stage(
|
|||||||
segs = infer_segmentation_batch(
|
segs = infer_segmentation_batch(
|
||||||
model, seg_config, batch["image_raw"], device,
|
model, seg_config, batch["image_raw"], device,
|
||||||
)
|
)
|
||||||
|
if pipeline_conf.seg_fix_dark_water or pipeline_conf.seg_reclassify_wetland:
|
||||||
|
segs = postprocess_segmentation(
|
||||||
|
segs, batch["image_raw"],
|
||||||
|
reclassify_wetland=pipeline_conf.seg_reclassify_wetland,
|
||||||
|
)
|
||||||
for j in range(segs.shape[0]):
|
for j in range(segs.shape[0]):
|
||||||
save_segmentation_async(
|
save_segmentation_async(
|
||||||
segs[j], Path(batch["output_root"][j]),
|
segs[j], Path(batch["output_root"][j]),
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from src.augmentor.inference import (
|
|||||||
infer_chmv2_batch,
|
infer_chmv2_batch,
|
||||||
infer_depth_batch,
|
infer_depth_batch,
|
||||||
infer_segmentation_batch,
|
infer_segmentation_batch,
|
||||||
|
postprocess_segmentation,
|
||||||
_SOBEL_X,
|
_SOBEL_X,
|
||||||
_SOBEL_Y,
|
_SOBEL_Y,
|
||||||
)
|
)
|
||||||
@@ -220,3 +221,59 @@ class TestInferSegmentationBatch:
|
|||||||
result = infer_segmentation_batch(pipeline, seg_config, sample_batch, torch.device("cpu"))
|
result = infer_segmentation_batch(pipeline, seg_config, sample_batch, torch.device("cpu"))
|
||||||
assert result.shape == (B, 1, H, W)
|
assert result.shape == (B, 1, H, W)
|
||||||
assert (result == 0).all()
|
assert (result == 0).all()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Post-processing heuristics
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestPostprocessSegmentation:
|
||||||
|
def test_dark_water_reclassified(self) -> None:
|
||||||
|
"""Dark uniform background → water."""
|
||||||
|
# Dark image (mean ~0.15, std ~0.02)
|
||||||
|
rgb = torch.full((1, 3, 32, 32), 0.15)
|
||||||
|
rgb += torch.randn_like(rgb) * 0.02
|
||||||
|
rgb.clamp_(0, 1)
|
||||||
|
seg = torch.zeros(1, 1, 32, 32, dtype=torch.uint8) # all background
|
||||||
|
result = postprocess_segmentation(seg, rgb)
|
||||||
|
assert (result == 4).all() # water
|
||||||
|
|
||||||
|
def test_bright_background_unchanged(self) -> None:
|
||||||
|
"""Bright background should NOT become water."""
|
||||||
|
rgb = torch.full((1, 3, 32, 32), 0.6)
|
||||||
|
seg = torch.zeros(1, 1, 32, 32, dtype=torch.uint8)
|
||||||
|
result = postprocess_segmentation(seg, rgb)
|
||||||
|
assert (result == 0).all() # stays background
|
||||||
|
|
||||||
|
def test_non_background_unchanged(self) -> None:
|
||||||
|
"""Non-background classes untouched even if dark."""
|
||||||
|
rgb = torch.full((1, 3, 32, 32), 0.1)
|
||||||
|
seg = torch.full((1, 1, 32, 32), 3, dtype=torch.uint8) # vegetation
|
||||||
|
result = postprocess_segmentation(seg, rgb)
|
||||||
|
assert (result == 3).all()
|
||||||
|
|
||||||
|
def test_wetland_reclassify_green(self) -> None:
|
||||||
|
"""Green wetland → vegetation when reclassify_wetland=True."""
|
||||||
|
rgb = torch.zeros(1, 3, 32, 32)
|
||||||
|
rgb[0, 1] = 0.6 # green dominant
|
||||||
|
rgb[0, 0] = 0.2 # low red
|
||||||
|
seg = torch.full((1, 1, 32, 32), 14, dtype=torch.uint8) # wetland
|
||||||
|
result = postprocess_segmentation(seg, rgb, reclassify_wetland=True)
|
||||||
|
assert (result == 3).all() # vegetation
|
||||||
|
|
||||||
|
def test_wetland_reclassify_brown(self) -> None:
|
||||||
|
"""Brown wetland → bare soil when reclassify_wetland=True."""
|
||||||
|
rgb = torch.zeros(1, 3, 32, 32)
|
||||||
|
rgb[0, 0] = 0.5 # red dominant
|
||||||
|
rgb[0, 1] = 0.3 # low green
|
||||||
|
seg = torch.full((1, 1, 32, 32), 14, dtype=torch.uint8)
|
||||||
|
result = postprocess_segmentation(seg, rgb, reclassify_wetland=True)
|
||||||
|
assert (result == 11).all() # bare soil
|
||||||
|
|
||||||
|
def test_wetland_unchanged_without_flag(self) -> None:
|
||||||
|
"""Wetland stays if reclassify_wetland=False."""
|
||||||
|
rgb = torch.zeros(1, 3, 32, 32)
|
||||||
|
rgb[0, 1] = 0.6
|
||||||
|
seg = torch.full((1, 1, 32, 32), 14, dtype=torch.uint8)
|
||||||
|
result = postprocess_segmentation(seg, rgb, reclassify_wetland=False)
|
||||||
|
assert (result == 14).all()
|
||||||
|
|||||||
Reference in New Issue
Block a user