Skip to content
SR-Forge

Metrics

MSE

Bases: Loss

Mean Squared Error. Per-pixel (y - x)**2 averaged over non-batch dims, honouring optional y_mask. Inherits the default calculate_score — only overrides pointwise.

PSNR

Bases: MSE

Peak Signal-to-Noise Ratio. Higher is better.

Implements the Level-2 wrapping protocol by exposing :meth:pointwise (inherited from :class:MSE, returns per-pixel squared error). The PSNR-specific log10 transform lives in :meth:calculate_score and runs after reduction, because log10(mean(...)) is not commutable with per-pixel weighting.

A Level-2 wrapper like :class:UncertaintyLoss(PSNR()) is accepted by construction (the pointwise method exists), but numerically equals :class:UncertaintyLoss(MSE()) — the log10 happens after the wrapper's reduction, not before. If that's not the semantics you want, override :meth:pointwise explicitly in a subclass.

pointwise(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor

Per-pixel squared error — same as MSE. Exposed explicitly (rather than implicit via MRO) so PSNR's participation in the Level-2 wrapping protocol is discoverable from this class.

Charbonnier

Bases: Loss

Masked Charbonnier loss (smooth L1). Lower is better. Inherits the default calculate_score — only overrides pointwise.

L1

Bases: Loss

Mean Absolute Error. Inherits the default calculate_score — only overrides pointwise.

TotalVariation

Bases: Loss

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 Loss 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. 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, mask: Optional[torch.Tensor] = None)

x: [B, C, H, W] mask (optional): [B, 1, H, W] or [B, H, W]; 1 = valid Returns per-sample TV normalized by the number of valid gradient locations.

TBE

Bases: Loss

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

UncertaintyLoss

Bases: Loss

Heteroscedastic-NLL data term for any pointwise base loss.

Multiplies the base's per-element error by exp(-log_var) (the predicted precision, since precision = 1/variance = exp(-log_var)). The result is the data term of the heteroscedastic NLL:

::

UncertaintyLoss(x, y, log_var) = prefactor · base.pointwise(x, y) · exp(-log_var)

Note: the parameter is named log_var because that's semantically what the network predicts — the log of the variance. log_var = log(σ²), so exp(-log_var) = 1/σ².

The +log_var regularizer (the log-determinant of the Gaussian / Laplacian density) is NOT included — pair this with a :class:Regularizer (penalty="identity") on the same log_var field in the same combiner. Splitting the data term and the regularizer makes shared-uncertainty multi-task setups composable without double-counting the regularizer when several base metrics share one log_var.

Parameters:

Name Type Description Default
base_metric Loss

A :class:Loss instance whose pointwise(x, y) returns a per-element score map. Wrapping a base without pointwise raises :class:TypeError at construction.

required
family str

Selects the NLL prefactor.

  • "laplace" (default) — prefactor 1.0. Pair with L1 / Charbonnier base for the Laplacian NLL.
  • "gauss" — prefactor 0.5. Pair with MSE base for the Gaussian NLL (factor comes from 1/(2σ²)).

The prefactor is a multiplicative constant; it doesn't change the optimum, only the numerical loss value. Pick "gauss" when you want loss values that match a real log-likelihood (e.g. for comparison across papers).

'laplace'
logvar_clamp tuple

(min, max) bounds applied to log_var before exp(-log_var). Without this, an outlier prediction can push exp(-log_var) to 0 or infinity, producing vanishing / exploding gradients. Default (-14.0, 14.0), which keeps exp(-log_var) ∈ [≈1.2e-6, ≈1.2e6] — well within float32 safe range. Pass None to disable clamping.

(-14.0, 14.0)
weight float

Scalar weight passed to the base Loss constructor.

1.0
**kwargs

Forwarded to the base Loss constructor (name, reduction, etc.).

{}

Example (YAML)::

loss:
  _target: srforge.loss.LossCombiner
  params:
    losses:
      - _target: srforge.loss.metrics.UncertaintyLoss
        params:
          base_metric: {_target: srforge.loss.metrics.MSE}
          family: gauss
        io: {inputs: {x: sr, y: hr, log_var: head_log_var}}
      - _target: srforge.loss.metrics.Regularizer
        params: {penalty: identity}
        io: {inputs: {x: head_log_var}}

pointwise(x: torch.Tensor, y: torch.Tensor, log_var: torch.Tensor) -> torch.Tensor

Per-pixel data term: prefactor · base.pointwise(x, y) · exp(-log_var).

log_var is clamped to :attr:logvar_clamp first (if enabled) to prevent exp(-log_var) from over/underflowing.

The +log_var regularizer is NOT included — pair this with a :class:Regularizer (penalty="identity") on the same log_var field.

calculate_score(x: torch.Tensor, y: torch.Tensor, log_var: torch.Tensor, y_mask: torch.Tensor = None) -> torch.Tensor

Custom :meth:calculate_score because of the extra log_var argument. Pointwise → masked-reduce flow with log_var threaded through to :meth:pointwise.

Regularizer

Bases: Loss

Single-tensor magnitude penalty.

Takes one tensor field, applies a pointwise penalty function, and reduces the result to a per-sample scalar via :meth:Loss.reduce. Useful whenever you want to penalise the magnitude (or some function of the magnitude) of an auxiliary model output as part of the total loss.

Built-in penalties:

============== ============================ ================================== penalty= Function applied to x Common name ============== ============================ ================================== "identity" x Mean of the field "abs" |x| L1 norm / sparsity "square" L2 norm / magnitude "exp" exp(x) Penalises positive log-quantities "entropy" -x·log(x) Element-wise entropy "huber" smooth-L1 with huber_delta Quadratic near 0, linear elsewhere callable any Tensor → Tensor Custom — same-shape output required ============== ============================ ==================================

Multiple :class:Regularizer instances can be combined in a :class:LossCombiner with different penalties and/or fields — each is a regular Loss with its own weight and IO binding.

Parameters:

Name Type Description Default
penalty

Either a built-in name (see table) or a callable Tensor → Tensor that returns the same shape as its input. Default "identity".

'identity'
huber_delta float

Transition point for penalty="huber". Ignored for all other penalties. Default 1.0.

1.0

Example::

# L1 sparsity penalty on an attention map
Regularizer(penalty="abs").set_io({"inputs": {"x": "attention"}})

# L2 magnitude penalty (with mask)
Regularizer(penalty="square").set_io(
    {"inputs": {"x": "noise_pred", "y_mask": "valid_mask"}}
)

# Huber with custom delta
Regularizer(penalty="huber", huber_delta=0.5)

# Custom — log-L1 (robust soft-L1)
Regularizer(penalty=lambda x: torch.log1p(x.abs()))

CorrectedLoss

Bases: Loss

Shift- and photometrically-corrected wrapper around any base :class:Loss.

Modern wrapper analogue of :class:cLossCombiner. Same math, but applied to a single base loss (rather than a combined batch), and using :func:torch.Tensor.unfold for the shift extraction — no Python-level for-loop, no intermediate torch.stack copy.

Per sample:

  1. Slice the central SR patch (border-pixel margin on each spatial side).
  2. Extract all (2*border+1)² shifted HR patches via a single view-based unfold.
  3. Optional photometric correction: b = mean(hr_patch - sr_patch) is added to sr_patch so per-shift intensity offset is normalised out.
  4. Build a synthetic :class:~srforge.data.Entry with canonical field names (:data:_PRED_FIELD, :data:_TARGET_FIELD, :data:_MASK_FIELD) and call the base loss via its standard __call__/forward pipeline. The base's own IO binding routes those fields to its parameters — no manual parameter-name resolution needed in this wrapper.
  5. Pick per-pixel min (if base.best_min) or max over shifts — i.e. each pixel gets credited for its best-aligned shift.

Two IO bindings, two roles:

  • Outer io (on :class:CorrectedLoss itself) maps the caller's :class:Entry fields to x / y / y_mask — same as any other Loss.
  • Inner io (on base) maps the base's parameters to the canonical field names listed below. For bases with canonical (x, y, y_mask) parameter names, the default identity map suffices and no inner io is needed. For bases with custom parameter names, declare an inner io block routing them to the canonical field names.

Example with a base that uses (sr, hr, mask) naming::

loss:
  _target: srforge.loss.metrics.CorrectedLoss
  params:
    base_metric:
      _target: my_module.MyLoss          # params: (sr, hr, mask)
      io:
        inputs:                          # ← inner IO routes
          sr: x                          #    base params to
          hr: y                          #    canonical inner
          mask: y_mask                   #    field names
    border: 3
  io:
    inputs: {x: pred_field, y: hr_field, y_mask: valid_mask}

The output equals cLossCombiner([base_metric]).calculate_score(...)[base_metric.name] modulo floating-point rounding (verified by tests/loss/test_corrected_loss.py). Prefer this wrapper for new code; keep :class:cLossCombiner for backward compatibility and the rare case of N corrected losses sharing one shift loop.

Parameters:

Name Type Description Default
base_metric Loss

The base :class:Loss to apply per shift.

required
border int

How many pixels on each spatial side define the shift range. (2*border + 1)² total shifts. Default 3 ⇒ 49 shifts (PROBA-V challenge convention).

3
do_correction bool

Apply the additive photometric bias before the base-loss call. Default True.

True

CrossEntropy

Bases: Loss

__init__(eps: float = 1e-12, **kwargs)

eps: small constant to avoid log(0)

pointwise(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor

Per-element cross-entropy: -y * log(x + eps). The clamping happens via the eps constant added inside log, which preserves the gradient unlike x.clamp(min=eps).

MGE

Bases: Loss

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) or (B,1,H,W) mask in {0,1} (or float in [0,1]). If per_channel=False and mask has C>1, it will be merged to (B,1,H,W).

SAM

Bases: Loss

__init__(eps: float = 1e-08, clamp: float = 1.0 - 1e-07, unit: Literal['radians', 'degrees'] = 'radians', **kwargs)

Spectral Angle Mapper (SAM) loss.

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).

pointwise(x: DictOrTensor, y: DictOrTensor) -> torch.Tensor

Per-pixel spectral angle, in radians, shape [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.

Unit conversion to degrees happens AFTER reduction in :meth:calculate_score; the pointwise output is always in radians so wrappers see a consistent scale.

Parameters:

Name Type Description Default
x, y

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

Raises:

Type Description
ValueError

If x and y have different types, unsupported shapes, or share no common band keys.

calculate_score(x: DictOrTensor, y: DictOrTensor) -> torch.Tensor

Per-sample SAM angle in the requested unit.

Override (rather than inherit the default) because the signature accepts DictOrTensor instead of Tensor, AND the optional radians→degrees post-conversion happens AFTER reduction.

Returns

Tensor [B], in radians or degrees per self.unit.