depth_fm.objectives.losses¶
losses ¶
Loss functions for Mars DepthFM training.
[REFACTOR NOTE] 1226 LOC; each loss class is 200+ LOC with internal helpers.
Candidate for future split into one file per loss (e.g. photoclinometric.py,
absolute_depth.py, laplacian.py, ordinal_ranking.py, combined.py).
Out of scope for the structural refactor that produced this layout.
Conditional Flow Matching velocity regression
L_FM = || v_theta(z_t, t; z_img) - (z_depth - z_img) ||^2
Auxiliary losses (all operate in pixel space on the predicted clean depth): Surface normals consistency — angular error between Sobel normals Focal Frequency Loss (FFL) — penalises frequency-domain discrepancies Multi-Scale Gradient Loss — L1 slope error at 1×, 2×, 4× downsampling
PhotoclinometricLoss ¶
Bases: Module
State-of-the-art photoclinometric loss for planetary DTM estimation.
Improvements over simple Lambertian
- Lunar-Lambert reflectance model (McEwen 1991; Hapke 2012): Blends Lambertian and Lommel-Seeliger components with a learnable weight, correctly modeling the limb-darkening behaviour of regolith.
- Multi-scale rendering at 1×, 2×, 4× — enforces macro-scale topographic consistency alongside fine detail.
- Combined Pearson + z-normalised SSIM loss. Pearson captures global correlation; SSIM captures local structural similarity (Wang et al. 2004). Both components are applied to per-image z-score normalised inputs, making the loss invariant to global luminance and contrast mismatch between the render and the ortho. This matters here because the scene intensity/ambient parameters are fitted with a pure Lambertian OLS, while the render uses Lunar-Lambert — so a systematic luminance offset is expected and should not be penalised.
- Unclamped render. Because both loss terms are scale- and shift-invariant, clamping the render to [-1, 1] would only destroy gradient information in saturated shadow pixels — which is exactly where photoclinometric information is densest.
- Variance floor and proper epsilon handling to prevent NaN gradients when renders are near-flat (e.g. shadow regions).
The loss is
L = (1 - α) * (1 - Pearson) + α * (1 - SSIM_z)
averaged over scales, where α = 0.5 by default and SSIM_z is SSIM computed on z-score normalised inputs.
NaN-safety
Every code path is hardened against NaN under DDP + gradient accumulation + mixed precision. Key invariants: - NEVER early-return with a detached zero — always flow through self.lunar_lambert_logit so DDP gradient sync sees every param. - All internal math forced to float32 to avoid bf16/fp16 overflow from the spatial_scale multiplication (up to 256×). - sqrt() always gets eps inside to prevent ∞ gradients. - F.normalize always gets eps > 0. - Lommel-Seeliger cos_e clamped to prevent denominator collapse.
Source code in src/depth_fm/objectives/losses.py
surface_normals ¶
render_from_depth ¶
render_from_depth(depth: Tensor, sun_vector: Tensor, intensity: Tensor, ambient: Tensor) -> tuple[torch.Tensor, torch.Tensor]
Public: render a depth map under the current Lunar-Lambert model.
Handles input normalisation/broadcasting so callers can pass per-sample sun vectors and scalar exposure parameters directly.
Returns:
| Name | Type | Description |
|---|---|---|
render |
Tensor
|
(B, 1, H, W) unclamped radiance. |
normals |
Tensor
|
(B, 3, H, W) unit surface normals. |
Source code in src/depth_fm/objectives/losses.py
FlowMatchingVelocityLoss ¶
Bases: Module
Conditional flow matching velocity loss.
Given a predicted velocity v_pred and the target velocity (z_depth - z_img), compute the L2 loss, optionally weighted by confidence maps.
Source code in src/depth_fm/objectives/losses.py
forward ¶
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
v_pred
|
Tensor
|
predicted velocity (B, C, h, w) |
required |
v_target
|
Tensor
|
target velocity z_depth - z_img (B, C, h, w) |
required |
confidence
|
Tensor
|
optional weight map (B, 1, H, W) — will be downsampled |
None
|
Returns:
| Type | Description |
|---|---|
Tensor
|
scalar loss |
Source code in src/depth_fm/objectives/losses.py
SurfaceNormalsLoss ¶
Bases: Module
Surface normals consistency loss.
Computes surface normals from the spatial gradients (Sobel) of predicted and GT depth maps, then measures angular difference via cosine similarity.
This operates in PIXEL space (decoded depth), not latent space.
DepthFM uses the "normal consistency" formulation from: Fan et al., "Three-filters-to-normal", IEEE RA-L 2021 which computes normals using finite differences and compares with cosine loss.
Source code in src/depth_fm/objectives/losses.py
MultiScaleGradientLoss ¶
Bases: Module
Multi-scale gradient matching loss for high-frequency topography detail.
Penalises L1 differences between predicted and GT spatial gradients at multiple downsampling scales. This is inherently scale-invariant (derivative differences, not absolute values) and forces the model to learn crisp slopes, crater rims, and dune ripples regardless of residual absolute-scale errors.
Loss
L_grad = mean over scales of: mean(|∇x_pred - ∇x_gt| + |∇y_pred - ∇y_gt|)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scales
|
tuple[int, ...]
|
tuple of integer downsampling factors (applied as average pooling). |
(1, 2, 4)
|
Source code in src/depth_fm/objectives/losses.py
FocalFrequencyLoss ¶
Bases: Module
Wrapper around the focal-frequency-loss package.
Penalises discrepancies in the Fourier domain, explicitly forcing the network to recover high-frequency details that MSE smooths over.
Requires: pip install focal-frequency-loss Paper: https://arxiv.org/pdf/2012.12821
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
loss_weight
|
float
|
Overall weight for this loss term. |
1.0
|
alpha
|
float
|
Focal factor — higher values focus more on hard frequencies. |
1.0
|
Source code in src/depth_fm/objectives/losses.py
AbsoluteDepthLoss ¶
Bases: Module
Direct Huber regression in [-1, 1] depth space.
Fills the 'absolute accuracy' gap: every other pixel-space loss in the pipeline is scale/shift-invariant (normals, gradient, photoclinometric). Without a direct loss on absolute values, a model could have perfect slopes and renders but systematically wrong elevations.
Huber combines
- L2 behaviour near convergence (|r| <= delta): smooth gradients, fast final-stage descent.
- L1 behaviour in the tails (|r| > delta): robust to outliers from stereo GT artefacts and nodata boundaries.
For inputs bounded in [-1, 1], delta=0.1 treats errors under ~10 % of the data range as 'near optimum' (L2) and larger errors as 'outliers' (L1).
Source code in src/depth_fm/objectives/losses.py
per_pixel_loss ¶
Un-reduced Huber loss map, shape (B, 1, H, W).
Source code in src/depth_fm/objectives/losses.py
LaplacianLoss ¶
Bases: Module
Second-order curvature consistency via discrete Laplacian.
Captures shape information that first-order gradients miss. Critical for Mars because its defining landforms are curvature features: - Crater bowls (negative curvature) - Central peaks (positive curvature) - Volcanic calderas, rims, scarps
A model matching all first-order gradients could still get crater concavity qualitatively wrong; the Laplacian forces correct concave/convex structure.
L = mean_over_valid(|∇²pred - ∇²gt|)
Source code in src/depth_fm/objectives/losses.py
OrdinalRankingLoss ¶
Bases: Module
Relative-ordering consistency over sampled pixel pairs.
For each sampled pair (i, j): if gt[i] - gt[j] > margin then pred[i] should exceed pred[j]. Uses a hinge loss:
L_pair = ReLU( -sign(gt_i - gt_j) * (pred_i - pred_j) )
averaged over pairs where |gt_i - gt_j| > margin (i.e. pairs whose GT ordering is unambiguous).
Why this helps on Mars¶
Mars DTM ground truth from stereo reconstruction has systematic noise (registration, interpolation, crater-wall occlusion). Absolute values in noisy regions are unreliable, but pairwise ORDERING is robust — if crater floor is lower than rim in the GT, it is almost certainly lower in reality even if the absolute depths are off. Ordinal loss gives a supervision signal that degrades gracefully with GT noise.
Based on: Chen et al., 'Single-Image Depth Perception in the Wild', NeurIPS 2016 (DIW) — adapted here to dense GT by random pair sampling.
Source code in src/depth_fm/objectives/losses.py
sample_pairs ¶
sample_pairs(B: int, N: int, device: device, generator: Generator | None = None, confidence: Tensor | None = None) -> tuple[torch.Tensor, torch.Tensor]
Sample (idx_i, idx_j) flat indices, each shape (B, num_pairs).
Deterministic if a generator is provided — used by viz for reproducible figures.
Source code in src/depth_fm/objectives/losses.py
pair_stats ¶
pair_stats(pred: Tensor, gt: Tensor, idx_i: Tensor, idx_j: Tensor, confidence: Tensor = None) -> dict
Per-pair losses, ordering labels, violation flags.
Returns dict with
losses (B, num_pairs) — hinge loss, 0 for ambiguous pairs ordered (B, num_pairs) bool — |gt_diff| > margin and valid violations (B, num_pairs) bool — ordered and pred disagrees gt_diff (B, num_pairs) — gt_i - gt_j pr_diff (B, num_pairs) — pred_i - pred_j
Source code in src/depth_fm/objectives/losses.py
CombinedLoss ¶
CombinedLoss(velocity_weight: float = 1.0, normals_weight: float = 0.1, normals_start_step: int = 2000, use_confidence_weighting: bool = False, freq_weight: float = 0.0, freq_start_step: int = 0, freq_alpha: float = 1.0, grad_weight: float = 0.0, grad_start_step: int = 0, grad_scales: tuple[int, ...] = (1, 2, 4), photo_weight: float = 0.0, photo_start_step: int = 2000, huber_weight: float = 3.0, huber_start_step: int = 0, huber_delta: float = 0.1, laplacian_weight: float = 0.05, laplacian_start_step: int = 0, ordinal_weight: float = 1.0, ordinal_start_step: int = 0, ordinal_margin: float = 0.02, ordinal_num_pairs: int = 10000)
Bases: Module
Combined training loss for Mars DepthFM.
L_total = w_vel * L_FM + w_norm * L_normals (after normals_start_step) + w_freq * L_FFL (after freq_start_step) + w_grad * L_grad (after grad_start_step) + w_photo * L_photo (after photo_start_step) + w_huber * L_Huber (after huber_start_step) [NEW] + w_lap * L_Laplacian (after laplacian_start_step) [NEW] + w_ord * L_Ordinal (after ordinal_start_step) [NEW]
Pixel-space losses (normals, FFL, grad, photo, huber, laplacian,
ordinal) require the caller to pass pred_depth_pixels and
gt_depth_pixels decoded WITHOUT no_grad so that gradients flow
back through the decoder to v_pred.
Source code in src/depth_fm/objectives/losses.py
979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 | |
needs_pixel_decode ¶
Return True if any pixel-space loss is active at this step.
Source code in src/depth_fm/objectives/losses.py
forward ¶
forward(v_pred: Tensor, v_target: Tensor, pred_depth_pixels: Tensor = None, gt_depth_pixels: Tensor = None, pred_depth_physical: Tensor = None, confidence: Tensor = None, real_ortho: Tensor | None = None, sun_vector: Tensor | None = None, global_step: int = 0, ambient: Tensor = None, intensity: Tensor = None) -> dict
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
v_pred
|
Tensor
|
predicted velocity in latent space (B, C, h, w) |
required |
v_target
|
Tensor
|
target velocity (B, C, h, w) |
required |
pred_depth_pixels
|
Tensor
|
decoded predicted clean depth (B, 3, H, W) |
None
|
gt_depth_pixels
|
Tensor
|
decoded GT depth (B, 3, H, W) |
None
|
confidence
|
Tensor
|
optional confidence map (B, 1, H, W) |
None
|
global_step
|
int
|
current training step |
0
|
Returns:
| Type | Description |
|---|---|
dict
|
dict with component losses and "total" |
Source code in src/depth_fm/objectives/losses.py
1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 | |