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)