Image¶
Metrics that need images: a spatial layout [B, C, H, W], or
(SAM) a spectrum per pixel along the channel axis.
TotalVariation
¶
Bases: Metric
Total Variation (TV) metric/penalty for images or feature maps.
TV encourages spatial smoothness by penalizing differences between neighboring
pixels. With anisotropic=True, it sums absolute horizontal and vertical
gradients (|dx| + |dy|). With anisotropic=False, it uses the isotropic form
sqrt(dx^2 + dy^2), which is rotation-invariant. Lower values indicate smoother
results. Supports optional spatial masks and returns a per-sample scalar
normalized by the number of valid pixels.
Parameters¶
anisotropic : bool, optional (default: False)
If True, use anisotropic TV; if False, use isotropic TV.
**kwargs : Any
Forwarded to the base Metric class.
Inputs¶
x : torch.Tensor Tensor of shape [B, C, H, W] to regularize (e.g., an image or residual). y : torch.Tensor or None Unused placeholder for API compatibility. y_mask : torch.Tensor or None Optional binary/soft mask of shape [B, 1, H, W] (or [B, H, W]) defining valid pixels used for normalization.
Returns¶
torch.Tensor Tensor of shape [B] with the TV value per sample; lower is better.
calculate_score(x: torch.Tensor, y: Optional[torch.Tensor] = None, y_mask: Optional[torch.Tensor] = None)
¶
x: [B, C, H, W] y_mask (optional): [B, 1, H, W] or [B, H, W]; 1 = valid Returns per-sample TV normalized by the number of valid gradient locations.
SSIM
¶
Bases: Metric
calculate_score(x: torch.Tensor, y: torch.Tensor, y_mask: torch.Tensor = None)
¶
Per-sample mean SSIM.
With y_mask, the SSIM map is computed on the real inputs and
averaged only where the whole window is valid (:meth:erode_mask),
so the valid area shrinks by window_size // 2 at mask edges.
Earlier versions zeroed the masked pixels first, and two zeroed
images match perfectly: a masked pixel scored as SSIM ≈ 1, and the
score rose with the masked area.
MGE
¶
Bases: Metric
Mean Gradient Error (Sobel magnitude) with channel control.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
per_channel
|
bool
|
If True, compare gradients per-channel. If False, first sum channels -> 1 channel, then compare. |
True
|
square_diff
|
bool
|
If True, use squared difference; else absolute difference. |
True
|
eps
|
float
|
Small constant in gradient magnitude. |
1e-06
|
Inputs to calculate_score:
x: Tensor (B,C,H,W) - prediction
y: Tensor (B,C,H,W) - reference/target
y_mask (optional): (B,C,H,W), (B,1,H,W) or (B,H,W); 1 = valid, soft
values allowed. The gradient at a pixel uses its
3x3 neighbourhood, so a pixel counts only if that
whole neighbourhood is valid. With per_channel=False
the channels are averaged first, so a per-channel
mask keeps a pixel only where every channel is valid.
SAM
¶
Bases: Metric
__init__(eps: float = 1e-08, clamp: float = 1.0 - 1e-07, unit: Literal['radians', 'degrees'] = 'radians', **kwargs)
¶
Spectral Angle Mapper (SAM) metric.
SAM measures the spectral similarity between two multispectral images by computing, per pixel, the angle between the spectral vectors (across bands) and then averaging over spatial dimensions.
Supports two input formats
1) Dict-of-bands: Dict[str, Tensor] where each value is [B,H,W] or [B,1,H,W] (bands are aligned by key; only common keys are used). 2) Stacked tensor: Tensor of shape [B,C,H,W] where C is the number of bands.
Parameters¶
eps : float Small constant to avoid division by zero in the cosine computation. clamp : float Clamp value applied to cosine similarity before acos to avoid numerical issues. Cosine is clamped to [-clamp, clamp]. unit : Literal['radians', 'degrees'] Output angle units. 'radians' returns angles in radians, 'degrees' converts to degrees.
Notes¶
- Output is one value per sample: Tensor[B].
- Smaller values indicate better spectral alignment (best_min = True).
- Honours
y_mask. The angle is independent per pixel, so masking here isolates the kept region exactly — a masked score equals the score of the unmasked pixels alone. A mask may be[B, H, W],[B, 1, H, W], or per band[B, C, H, W]; a per-band mask keeps a pixel only if it is valid in every band, because the angle is computed across all of them. aggregate="micro"averages the angle over every valid pixel in the batch or epoch, instead of averaging per-image means.- A plain Level-2 metric: masking,
aggregateand dict-of-bands handling all come from the framework.
pointwise(x: DictOrTensor, y: DictOrTensor) -> torch.Tensor
¶
Per-pixel spectral angle in the requested unit, [B, 1, H, W].
The channel/spectral dim is collapsed because the angle is
computed BETWEEN the spectral vectors at each (h, w) —
a single scalar per spatial position. The leading 1 is kept
(rather than returning [B, H, W]) so the output preserves
the input's dimensionality and integrates with reduce()
+ any future per-pixel modifier (UncertaintyLoss, etc.)
without dimension-juggling.
The unit is applied here: converting to degrees multiplies every value by the same constant, and averaging commutes with that, so the per-image and pooled scores come out right either way.
The annotations admit a dict, so dict-of-bands inputs arrive in one call instead of band by band — the angle needs every band at once.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
DictOrTensor
|
Either |
required |
y
|
DictOrTensor
|
The same, matching x. When both are dicts only their common keys are compared. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If x and y have different types, unsupported shapes, or share no common band keys. |
LPIPS
¶
Bases: Metric
Learned Perceptual Image Patch Similarity (Zhang, 2018). Lower is better.
Implements the Level-2 protocol: :meth:pointwise returns the
per-pixel perceptual distance map and the inherited calculate_score
composes pointwise -> reduce.
The underlying network is built with spatial=True so it returns a
[B, 1, H, W] map instead of one number per image. Three things follow,
and the first is the reason for it:
- Masking works.
y_maskused to be accepted and silently discarded, so a masked LPIPS was an unmasked score reported as masked — identical to five decimal places with and without a mask. The Level-2 path routes the mask through :meth:reduce, which zeroes masked positions and normalises by the unmasked count. - The contract is honoured. The scalar path returned
[B, 1, 1, 1]whereMetricpromises[B];reducenow yields[B]directly instead of relying on a later collapse. - Wrappers become possible, e.g.
UncertaintyLoss(LPIPS()), which needs a per-element map and previously had none.
The map is a faithful decomposition, not an approximation: its spatial mean reproduces the scalar LPIPS to within 0.01% (upsampling round-off). Cost is negligible — at 8x3x512x512 both paths take the same time and the map itself is about 8 MB.
Masking selects which map positions are averaged, not which input pixels contribute. The network is convolutional, so a map pixel depends on input well beyond the mask boundary and a masked score still carries some context from the masked-out region. That is inherent to a perceptual metric and differs from a pointwise metric like L1, where a mask isolates the kept region exactly.
LPIPS is AlexNet-based and accepts only 1- or 3-channel input. Multispectral or hyperspectral data must be reduced to an RGB composite, or scored band-wise, before reaching this metric.
NaN-safe gradients. The lpips package normalises each feature
vector with sqrt of its squared norm, whose gradient is infinite
where that norm underflows to 0 — tiny activations, and routinely under
float16 mixed precision. Building this metric replaces that one function
with a version that keeps eps under the root and computes the norm in
float32 (see :func:_nan_safe_normalize_tensor), process-wide and once.
Values are unchanged to ~1e-10.
pointwise(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor
¶
Per-pixel perceptual distance, [B, 1, H, W].
One channel regardless of input channels: LPIPS is a distance in
feature space, not a per-channel difference. Against a per-channel
mask, :meth:reduce keeps a pixel only where every channel is valid.