Skip to content
SR-Forge

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.

TBE

Bases: Metric

The Blur Effect non-reference metric to measure how blurry is the image.

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, aggregate and 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 Dict[str, Tensor] of per-band tensors [B,H,W]/[B,1,H,W] (only common keys used) or a stacked [B, C, H, W] tensor.

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_mask used 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] where Metric promises [B]; reduce now 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.