SM737's picture
Upload folder using huggingface_hub (part 4)
a358495 verified
Raw
History Blame Contribute Delete
8.42 kB
from __future__ import annotations
from dataclasses import dataclass
from enum import IntEnum
from typing import Any
import numpy as np
from scipy import ndimage as ndi
class ShadowType(IntEnum):
NONE = 0
BUILDING_SHADOW = 1
TERRAIN_SHADOW = 2
CLOUD_SHADOW = 3
VEGETATION_SHADOW = 4
UNKNOWN_SHADOW = 5
@dataclass(frozen=True)
class ShadowEvidence:
probability: np.ndarray
mask: np.ndarray
shadow_type: np.ndarray
features: dict[str, np.ndarray]
details: dict[str, Any]
def rgb_shadow_evidence(
r: np.ndarray,
g: np.ndarray,
b: np.ndarray,
valid: np.ndarray,
*,
water_probability: np.ndarray | None = None,
building_mask: np.ndarray | None = None,
vegetation_probability: np.ndarray | None = None,
) -> ShadowEvidence:
"""Estimate illumination shadow independently from surface identity.
Darkness contributes evidence, but cannot by itself create a shadow mask.
Local illumination contrast, chromatic continuation, texture/edges, and
object adjacency provide the additional evidence.
"""
r, g, b = (np.asarray(channel, dtype="float32") for channel in (r, g, b))
valid = np.asarray(valid, dtype=bool)
luminance = 0.299 * r + 0.587 * g + 0.114 * b
local_mean = ndi.uniform_filter(np.where(valid, luminance, 0.0), size=21)
valid_density = ndi.uniform_filter(valid.astype("float32"), size=21)
local_mean = local_mean / np.maximum(valid_density, 1e-4)
illumination_drop = np.clip((local_mean - luminance) / np.maximum(local_mean, 0.04), 0.0, 1.0)
maximum = np.maximum.reduce([r, g, b])
minimum = np.minimum.reduce([r, g, b])
saturation = (maximum - minimum) / np.maximum(maximum, 1e-5)
chromatic_neutrality = 1.0 - np.clip(saturation / 0.35, 0.0, 1.0)
darkness = np.clip((0.34 - luminance) / 0.34, 0.0, 1.0)
gradient = np.hypot(ndi.sobel(luminance, axis=0), ndi.sobel(luminance, axis=1)) / 4.0
edge_support = np.clip(ndi.uniform_filter((gradient > 0.035).astype("float32"), 9) / 0.18, 0.0, 1.0)
building_adj = np.zeros_like(luminance, dtype="float32")
if building_mask is not None:
objects = np.asarray(building_mask, dtype=bool)
if objects.shape == valid.shape:
building_adj = (ndi.binary_dilation(objects, iterations=8) & ~objects).astype("float32")
vegetation_adj = np.zeros_like(luminance, dtype="float32")
if vegetation_probability is not None:
vegetation = np.asarray(vegetation_probability, dtype="float32") > 0.55
if vegetation.shape == valid.shape:
vegetation_adj = (ndi.binary_dilation(vegetation, iterations=5) & ~vegetation).astype("float32")
bright_object = (luminance > 0.82) & valid
bright_adjacency = (ndi.binary_dilation(bright_object, iterations=18) & ~bright_object).astype("float32")
water_support = np.zeros_like(valid)
within_water_drop = np.zeros_like(luminance, dtype="float32")
if water_probability is not None:
water_support = np.asarray(water_probability, dtype="float32") >= 0.55
water_density = ndi.uniform_filter(water_support.astype("float32"), size=31)
water_local = ndi.uniform_filter(np.where(water_support, luminance, 0.0), size=31) / np.maximum(water_density, 1e-4)
within_water_drop = np.clip((water_local - luminance) / np.maximum(water_local, 0.02), 0.0, 1.0).astype("float32")
# At least two independent families are required: darkness/local contrast
# plus geometry/chromatic evidence. A dark pixel alone therefore stays low.
probability = (
0.25 * darkness
+ 0.32 * illumination_drop
+ 0.13 * chromatic_neutrality
+ 0.10 * edge_support
+ 0.14 * building_adj
+ 0.06 * vegetation_adj
+ 0.10 * bright_adjacency
)
contextual_support = (building_adj > 0) | (vegetation_adj > 0) | (bright_adjacency > 0) | ((edge_support > 0.55) & (chromatic_neutrality > 0.50))
corroborated = (illumination_drop > 0.20) & contextual_support
probability = np.where(corroborated, probability, probability * 0.35)
# Uniform dark water is not a cast shadow. Water may still be shadowed when
# it has a clear illumination drop relative to neighboring water pixels.
water_shadow_support = water_support & (within_water_drop > 0.28) & contextual_support
probability = np.where(water_support & ~water_shadow_support, probability * 0.20, probability)
probability = np.where(water_shadow_support, np.maximum(probability, 0.42 + 0.45 * within_water_drop), probability)
probability = np.clip(probability, 0.0, 1.0).astype("float32")
probability[~valid] = 0.0
shadow_type = np.full(valid.shape, ShadowType.NONE, dtype="uint8")
likely = probability >= 0.52
shadow_type[likely] = ShadowType.UNKNOWN_SHADOW
shadow_type[likely & (bright_adjacency > 0)] = ShadowType.CLOUD_SHADOW
shadow_type[likely & (vegetation_adj > 0)] = ShadowType.VEGETATION_SHADOW
shadow_type[likely & (building_adj > 0)] = ShadowType.BUILDING_SHADOW
# Water and shadow are allowed to overlap. Strong independent water evidence
# is never removed from the illumination product.
shaded_water_pixels = int((likely & (np.asarray(water_probability) >= 0.60)).sum()) if water_probability is not None else 0
return ShadowEvidence(
probability=probability,
mask=likely & valid,
shadow_type=shadow_type,
features={
"darkness": darkness.astype("float32"),
"illumination_drop": illumination_drop.astype("float32"),
"chromatic_neutrality": chromatic_neutrality.astype("float32"),
"edge_support": edge_support.astype("float32"),
"building_adjacency": building_adj,
"vegetation_adjacency": vegetation_adj,
"bright_object_adjacency": bright_adjacency,
"within_water_illumination_drop": within_water_drop,
},
details={"method":"rgb_local_illumination_geometry_v1", "shaded_water_pixels": shaded_water_pixels},
)
def multispectral_shadow_evidence(
brightness: np.ndarray,
valid: np.ndarray,
spectral_water_probability: np.ndarray,
*,
vegetation_probability: np.ndarray | None = None,
builtup_probability: np.ndarray | None = None,
) -> ShadowEvidence:
brightness = np.asarray(brightness, dtype="float32")
valid = np.asarray(valid, dtype=bool)
local = ndi.uniform_filter(np.where(valid, brightness, 0.0), 21)
density = ndi.uniform_filter(valid.astype("float32"), 21)
local = local / np.maximum(density, 1e-4)
illumination_drop = np.clip((local - brightness) / np.maximum(local, 0.01), 0.0, 1.0)
finite_values = brightness[valid]
dark_scale = float(np.percentile(finite_values, 35)) if finite_values.size else 0.0
darkness = np.clip((dark_scale - brightness) / max(dark_scale, 1e-4), 0.0, 1.0)
water = np.asarray(spectral_water_probability, dtype="float32")
nonwater_support = 1.0 - water
vegetation = np.asarray(vegetation_probability, dtype="float32") if vegetation_probability is not None else np.zeros_like(water)
builtup = np.asarray(builtup_probability, dtype="float32") if builtup_probability is not None else np.zeros_like(water)
context = np.maximum(vegetation, builtup)
probability = np.clip(0.40 * illumination_drop + 0.20 * darkness + 0.22 * nonwater_support + 0.18 * context, 0.0, 1.0)
corroborated = (illumination_drop > 0.22) & ((nonwater_support > 0.55) | (context > 0.45))
probability = np.where(corroborated, probability, probability * 0.30).astype("float32")
probability[~valid] = 0.0
mask = (probability >= 0.52) & valid
shadow_type = np.full(valid.shape, ShadowType.NONE, dtype="uint8")
shadow_type[mask] = ShadowType.UNKNOWN_SHADOW
shadow_type[mask & (vegetation >= builtup) & (vegetation > 0.45)] = ShadowType.VEGETATION_SHADOW
shadow_type[mask & (builtup > vegetation) & (builtup > 0.45)] = ShadowType.BUILDING_SHADOW
return ShadowEvidence(
probability=probability, mask=mask, shadow_type=shadow_type,
features={"darkness":darkness.astype("float32"), "illumination_drop":illumination_drop.astype("float32"), "nonwater_support":nonwater_support.astype("float32")},
details={"method":"multispectral_illumination_spectral_disagreement_v1", "shaded_water_pixels":int((mask & (water >= 0.60)).sum())},
)