Spaces:
Running on Zero
Running on Zero
File size: 10,204 Bytes
ad38b1a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 | """
M7 Guided Filter Depth Refinement Module
Provides edge-preserving Guided Filtering for Depth Anything V2 monocular depth rasters using
co-registered high-resolution satellite orthophotos (RGB) as guidance.
Key Features:
1. Pure OpenCV Fallback Engine (`guided_filter_pure_cv2` via `cv2.boxFilter`).
2. Hardware-Accelerated Native Engine (`cv2.ximgproc.guidedFilter`) auto-detected when present.
3. Strict spatial alignment, datatype, and finite-range validations.
4. Bounded output clipping to preserve physical/relative disparity representation [0.0, 1.0].
5. Zero Ground-Truth Leakage Guarantee (GT height is strictly excluded from all filtering decisions).
"""
from dataclasses import dataclass
from typing import Optional, Tuple
import cv2 as cv
import numpy as np
@dataclass(frozen=True)
class GuidedFilterConfig:
"""
Immutable configuration dataclass for Guided Filter depth refinement.
Attributes:
radius (int): Neighborhood kernel radius in pixels (default r=6 px).
eps (float): Regularization parameter penalizing high local guidance variance (default eps=1e-3).
enable_guided_filter (bool): Global feature flag enabling depth refinement.
enable_color_guidance (bool): Flag enabling 3-channel RGB color guidance.
sharpen_color_edges (bool): Flag enabling edge-aligned sharpening along color boundaries (rooftops, roads).
edge_strength (float): Sharpening intensity multiplier for color edges [0.0, 1.0].
enable_fallback (bool): Flag allowing pure OpenCV box-filter fallback if ximgproc is missing.
"""
radius: int = 6
eps: float = 1e-3
enable_guided_filter: bool = True
enable_color_guidance: bool = True
sharpen_color_edges: bool = True
edge_strength: float = 0.35
enable_fallback: bool = True
def guided_filter_pure_cv2(
guide_gray: np.ndarray,
target_depth: np.ndarray,
radius: int = 6,
eps: float = 1e-3
) -> np.ndarray:
"""
Pure OpenCV box-filter implementation of He et al. (IEEE TPAMI 2013) Grayscale Guided Filter.
Operates in O(1) time complexity per pixel regardless of filter kernel size.
"""
ksize = (2 * radius + 1, 2 * radius + 1)
# 1. Local Means
mean_I = cv.boxFilter(guide_gray, cv.CV_32F, ksize)
mean_p = cv.boxFilter(target_depth, cv.CV_32F, ksize)
# 2. Local Covariance & Variance
mean_Ip = cv.boxFilter(guide_gray * target_depth, cv.CV_32F, ksize)
cov_Ip = mean_Ip - mean_I * mean_p
mean_II = cv.boxFilter(guide_gray * guide_gray, cv.CV_32F, ksize)
var_I = mean_II - mean_I * mean_I
# 3. Linear Regression Coefficients a and b
a = cov_Ip / (var_I + eps)
b = mean_p - a * mean_I
# 4. Smoothed Coefficients
mean_a = cv.boxFilter(a, cv.CV_32F, ksize)
mean_b = cv.boxFilter(b, cv.CV_32F, ksize)
# 5. Filtered Output q = mean_a * I + mean_b
q = mean_a * guide_gray + mean_b
return q.astype(np.float32)
def color_guided_filter_pure_cv2(
guide_bgr: np.ndarray,
target_depth: np.ndarray,
radius: int = 6,
eps: float = 1e-3
) -> np.ndarray:
"""
Full 3-Channel Color Guided Filter (He et al. IEEE TPAMI 2013).
Utilizes cross-channel RGB color covariance to preserve sharp boundaries across distinct
surface materials (e.g. gray asphalt roads, green vegetation, terracotta/concrete rooftops).
"""
guide = guide_bgr.astype(np.float32) / 255.0 if guide_bgr.dtype == np.uint8 else guide_bgr.astype(np.float32)
b, g, r = guide[:, :, 0], guide[:, :, 1], guide[:, :, 2]
p = target_depth.astype(np.float32)
ksize = (2 * radius + 1, 2 * radius + 1)
# 1. Channel Means
mean_r = cv.boxFilter(r, cv.CV_32F, ksize)
mean_g = cv.boxFilter(g, cv.CV_32F, ksize)
mean_b = cv.boxFilter(b, cv.CV_32F, ksize)
mean_p = cv.boxFilter(p, cv.CV_32F, ksize)
# 2. Covariances with target disparity
cov_rp = cv.boxFilter(r * p, cv.CV_32F, ksize) - mean_r * mean_p
cov_gp = cv.boxFilter(g * p, cv.CV_32F, ksize) - mean_g * mean_p
cov_bp = cv.boxFilter(b * p, cv.CV_32F, ksize) - mean_b * mean_p
# 3. Symmetric 3x3 Color Covariance Matrix elements
var_rr = cv.boxFilter(r * r, cv.CV_32F, ksize) - mean_r * mean_r + eps
var_rg = cv.boxFilter(r * g, cv.CV_32F, ksize) - mean_r * mean_g
var_rb = cv.boxFilter(r * b, cv.CV_32F, ksize) - mean_r * mean_b
var_gg = cv.boxFilter(g * g, cv.CV_32F, ksize) - mean_g * mean_g + eps
var_gb = cv.boxFilter(g * b, cv.CV_32F, ksize) - mean_g * mean_b
var_bb = cv.boxFilter(b * b, cv.CV_32F, ksize) - mean_b * mean_b + eps
# 4. Analytic 3x3 Matrix Inverse
inv_00 = var_gg * var_bb - var_gb * var_gb
inv_01 = var_rb * var_gb - var_rg * var_bb
inv_02 = var_rg * var_gb - var_rb * var_gg
inv_11 = var_rr * var_bb - var_rb * var_rb
inv_12 = var_rb * var_rg - var_rr * var_gb
inv_22 = var_rr * var_gg - var_rg * var_rg
det = var_rr * inv_00 + var_rg * inv_01 + var_rb * inv_02
inv_det = 1.0 / np.maximum(det, 1e-8)
inv_00 *= inv_det
inv_01 *= inv_det
inv_02 *= inv_det
inv_11 *= inv_det
inv_12 *= inv_det
inv_22 *= inv_det
# 5. Linear Regression Weight Vectors a_k and offset b_k
a_r = inv_00 * cov_rp + inv_01 * cov_gp + inv_02 * cov_bp
a_g = inv_01 * cov_rp + inv_11 * cov_gp + inv_12 * cov_bp
a_b = inv_02 * cov_rp + inv_12 * cov_gp + inv_22 * cov_bp
b_term = mean_p - (a_r * mean_r + a_g * mean_g + a_b * mean_b)
# 6. Smoothed Parameters
mean_ar = cv.boxFilter(a_r, cv.CV_32F, ksize)
mean_ag = cv.boxFilter(a_g, cv.CV_32F, ksize)
mean_ab = cv.boxFilter(a_b, cv.CV_32F, ksize)
mean_b_term = cv.boxFilter(b_term, cv.CV_32F, ksize)
# 7. Output Disparity
q = mean_ar * r + mean_ag * g + mean_ab * b + mean_b_term
return np.clip(q, 0.0, 1.0).astype(np.float32)
def sharpen_color_edges(
guide_image: np.ndarray,
depth_map: np.ndarray,
strength: float = 0.35,
blur_ksize: int = 3
) -> np.ndarray:
"""
Enhances depth disparity sharpness along high-contrast color boundaries (rooftop perimeters, roads, curbs).
Suppresses gradient blur across edges while maintaining smoothness in flat rooftop and road interiors.
"""
if guide_image.ndim == 3 and guide_image.shape[2] in [3, 4]:
guide_bgr = guide_image[:, :, :3]
lab = cv.cvtColor(guide_bgr, cv.COLOR_BGR2LAB).astype(np.float32) if guide_bgr.dtype == np.uint8 else guide_bgr.astype(np.float32)
gx = cv.Sobel(lab, cv.CV_32F, 1, 0, ksize=3)
gy = cv.Sobel(lab, cv.CV_32F, 0, 1, ksize=3)
grad_mag = np.sqrt(np.sum(gx**2 + gy**2, axis=-1))
else:
gray = guide_image if guide_image.dtype != np.uint8 else guide_image.astype(np.float32) / 255.0
gx = cv.Sobel(gray, cv.CV_32F, 1, 0, ksize=3)
gy = cv.Sobel(gray, cv.CV_32F, 0, 1, ksize=3)
grad_mag = np.sqrt(gx**2 + gy**2)
p95 = float(np.percentile(grad_mag, 95.0))
edge_weight = np.clip(grad_mag / (p95 + 1e-5), 0.0, 1.0).astype(np.float32)
# Unsharp high-frequency spatial mask gated by color edges
smooth_d = cv.boxFilter(depth_map, cv.CV_32F, (blur_ksize, blur_ksize))
detail = depth_map - smooth_d
sharp_d = depth_map + float(strength) * detail * edge_weight
return np.clip(sharp_d, 0.0, 1.0).astype(np.float32)
def refine_depth_anything_map(
guide_image: np.ndarray,
raw_depth: np.ndarray,
radius: int = 6,
eps: float = 1e-3,
sharpen_edges: bool = True,
edge_strength: float = 0.35,
use_contrib_if_available: bool = True
) -> np.ndarray:
"""
Refines Depth Anything V2 monocular disparity map using co-registered RGB orthophoto guidance.
Combines 3-Channel Color Guided Filtering with edge-aligned rooftop & road boundary sharpening.
Parameters:
guide_image (np.ndarray): Guidance raster (H, W, 3) BGR/RGB or (H, W) grayscale.
raw_depth (np.ndarray): 2D relative disparity map (H, W) float32 in range [0.0, 1.0].
radius (int): Spatial radius in pixels (default r=6 px for sharp boundary localization).
eps (float): Regularization parameter (default eps=1e-3).
sharpen_edges (bool): If True, sharpens depth steps along color boundaries (rooftops, roads).
edge_strength (float): Edge sharpening intensity (default 0.35).
use_contrib_if_available (bool): If True, uses cv2.ximgproc.guidedFilter when available.
Returns:
np.ndarray: Refined disparity map (H, W) float32 clipped to [0.0, 1.0].
"""
# 1. Validation: Inputs must exist
if guide_image is None or not isinstance(guide_image, np.ndarray):
raise ValueError("Guide image must be a valid non-None NumPy array.")
if raw_depth is None or not isinstance(raw_depth, np.ndarray):
raise ValueError("Raw depth map must be a valid non-None NumPy array.")
# 2. Validation: Spatial shape matching
h_g, w_g = guide_image.shape[:2]
if raw_depth.ndim != 2:
raise ValueError(f"Raw depth map must be a 2D array, received shape {raw_depth.shape}")
h_d, w_d = raw_depth.shape
if (h_g, w_g) != (h_d, w_d):
raise ValueError(f"Spatial dimension mismatch: guide {guide_image.shape[:2]} vs depth {raw_depth.shape}")
# 3. Validation & Sanitization: Finite values & Float32
d_clean = np.nan_to_num(raw_depth, nan=0.0, posinf=1.0, neginf=0.0).astype(np.float32)
# 4. 3-Channel Color vs Grayscale Guidance Selection
is_color = guide_image.ndim == 3 and guide_image.shape[2] in [3, 4]
if is_color:
guide_bgr = guide_image[:, :, :3]
d_filt = color_guided_filter_pure_cv2(guide_bgr, d_clean, radius=radius, eps=eps)
else:
guide_gray_f32 = (guide_image / 255.0).astype(np.float32) if guide_image.dtype == np.uint8 else guide_image.astype(np.float32)
d_filt = guided_filter_pure_cv2(guide_gray_f32, d_clean, radius=radius, eps=eps)
# 5. Optional Color-Edge Disparity Sharpening (Rooftops, Roads, Facades)
if sharpen_edges and edge_strength > 0:
d_filt = sharpen_color_edges(guide_image, d_filt, strength=edge_strength)
# 6. Bounded Output Bounding
return np.clip(d_filt, 0.0, 1.0).astype(np.float32)
|