Fernando Voltolini de Azambuja

Imaging and color measurement · Methods

Slanted-edge SFR: formulas and implementation

The code linked here is the estimator that produced the published results, not a simplified restatement of it. What is excluded is everything around it: RAW decoding, archive layout, command handling, and the parsers for the advisory tool’s output.

Inputs

The estimator takes a non-owning view of an already black-subtracted 2×2 mosaic (cfa_image_view.hpp):

struct CfaImageView {
  int width, height, row_stride_pixels;
  std::array<int, 4> color_at_position;
  std::string_view cdesc;
  std::span<const double> samples;
  double white_level;
  std::array<double, 4> black_per_channel;
};

Two things are deliberate. Black subtraction happens before this boundary, because subtracting a pedestal twice is silent and produces a plausible wrong answer. The white and black levels come along anyway, because near-saturation has to be detectable here: clipping flattens the bright side of a transition and makes a system look sharper than it is.

Position index is (y & 1) * 2 + (x & 1), and color_at_position maps that to a channel. Green occupies two of the four positions, so a region is snapped to whole 2×2 blocks before sampling (roi.cpp) — otherwise one green position would be weighted more heavily than the other.

From edge to MTF

1 — Locate the edge per scan line. For each line crossing the transition, the centroid of the derivative gives a sub-pixel crossing position. Fitting a straight line through those centroids gives the edge angle. The tilt is what makes the method work: each line samples the transition at a different sub-pixel phase.

2 — Project and bin. Every green sample is projected onto the edge normal, giving its signed distance d from the fitted line. Samples are accumulated into bins of 0.25 px, producing an oversampled edge-spread function — four times finer than the sensor pitch, from data the sensor could not resolve directly.

3 — Require two-sided support. With d10 and d90 the 10% and 90% crossings, the transition center is d_c = (d10 + d90) / 2. Let s_short and s_long be the shorter and longer distances from d_c to the ends of the measured range. The region is rejected when

2 * s_short < s_long

and otherwise the data is cropped to [d_c - s_short, d_c + s_short]. A Fourier estimate needs measured plateau on both sides of the transition; windowing cannot recover a side that was never sampled. Cropping symmetrically also puts the transition at the midpoint of the array, which is what makes the positional window in step 5 centered on it rather than on an arbitrary region boundary.

4 — Differentiate. LSF[i] = ESF[i+1] − ESF[i], the line-spread function.

5 — Window. A Hamming window tapers the ends so the finite record does not introduce its own frequency content:

w[i] = 0.54 − 0.46 * cos(2 * pi * i / (N − 1))

6 — Transform and normalize. The discrete Fourier magnitude of the windowed LSF, divided by its zero-frequency value, gives modulation against frequency. Bin k corresponds to

f_k = (k / N) / bin_spacing   cycles per pixel

7 — Correct the differencing. Step 4 is a finite difference, not a derivative, and it attenuates high frequencies by a known factor. Dividing it out is why the result is comparable to an analytic MTF rather than merely self-consistent:

MTF(f) = |DFT(w · LSF)|(f) / |DFT(w · LSF)|(0) / |sinc(f * bin_spacing)|

Reported quantities

Field Meaning
mtf50_cy_per_px Frequency where MTF first falls to 0.5
mtf50p_cy_per_px Same, relative to the curve’s own peak rather than to DC
mtf_at_nyquist Response at 0.5 cycles/pixel, the sampling limit
r1090_px 10–90% rise distance, a spatial-domain cross-check
edge_angle_deg Fitted angle; a near-zero or near-45° angle is rejected
orientation Whether the edge runs predominantly horizontally or vertically

mtf50 and mtf50p differ when the curve overshoots above its DC value — sharpening, or ringing from a strong optical low-pass filter. Carrying both keeps that distinguishable instead of hidden.

Invalid inputs

Every rejection sets accepted = false with a named rejection_reason. Common codes at the public estimator boundary include:

Reason Condition
insufficient_edge_support The two-sided rule in step 3 fails
low_contrast The transition is too shallow to locate
rise_distance_not_found The 10% or 90% crossing is not measured
dc_normalization_zero The zero-frequency term vanishes
roi_saturated One or more selected samples reaches the declared near-saturation gate
roi_too_small The clipped, CFA-balanced region is absent or below the minimum size
invalid_raw_image Image dimensions, stride, sample span, levels, or selected samples are invalid
edge_angle_out_of_range The fitted edge angle is outside the declared operating range
nyquist_not_sampled The computed frequency axis does not contain 0.5 cycles/pixel
mtf50_not_found / mtf50p_not_found The required falling crossing is not measured

No zero sentinel is accepted as an unmeasured MTF quantity. A missing Nyquist sample or falling crossing makes the result a rejection rather than a plausible placeholder.

What the tests pin

test_sfr.cpp runs entirely on synthetic edges generated in the test itself, so the numerical behavior is checkable without any camera capture.

The central case is an oracle: a Gaussian-blurred step of width sigma has MTF50 at 0.18739 / sigma cycles per pixel analytically, and the implementation recovers it to within 0.018, with edge angle to within 0.08°. A separate narrow-Gaussian oracle checks the numerical Nyquist response to within 0.03. For the broad Gaussian, the positive lower gate rejects a zero/default result and the upper gate establishes strong suppression; those two inequalities do not themselves establish Nyquist accuracy.

The remaining cases cover DFT bin placement; the differencing correction at DC and three non-zero frequencies; horizontal and vertical edges; stability when the same edge is translated within a sufficiently supported region; crossings between DC and the first non-DC bin; and explicit rejections for missing Nyquist, missing post-peak MTF50P, insufficient support, low contrast, near-saturation, invalid image geometry, out-of-range angle, non-finite samples, and the mosaic-block rule.

Numeric tolerances retain the estimator’s reviewed synthetic regression bounds; they were not tightened around the output of this public copy.

Source file: methods/slanted-edge-sfr.md