Artificial Wasteland · groundtruth

The Edge That Came Back Sharper

Photograph a straight edge tilted a few degrees and this page hands back your own imaging chain's spatial frequency response: the frequency at which it has lost half the contrast, in cycles per pixel of the frame you were given, and in cycles per millimetre of paper once you have printed the target and measured the printed ruler with a ruler of your own. Before it will report anything about you, the same estimator has to recover four blurs whose true answers are closed forms and refuse six specimens it is supposed to refuse. A response that peaks above 1.000 is arithmetic no lens performs. If yours does, the page shows you the bands the sharpener left beside the edge in your own picture.

Before it measures you, it measures something it cannot argue with

Four synthetic edges are drawn in your browser right now. Each one is an ideal step blurred by a known point spread function and then integrated over each pixel's square footprint, sixteen by sixteen sub-samples per pixel, and written out as ordinary 8-bit sRGB. Their true response is a closed form that the code drawing them never uses and the code measuring them never sees:

MTF(f) = optical(f) × sinc(f cos θ) × sinc(f sin θ),   optical(f) = e−2π2σ2f2,   f in cycles per pixel

The second and third factors are the pixel itself: a square detector of width w has a sinc transfer with its first zero at 1/w, which for a one-pixel-wide detector is one cycle per pixel. That term is the reason this anchor is a test and not a tautology. For the first specimen the blur alone crosses half at cycles per pixel, and the blur times the pixel crosses at . Those two candidate answers are per cent apart, and an estimator that quietly ignored the detector footprint would land on the wrong one. The estimator landed on .

The slate: cycles per pixel

running…

Tolerance is 2 per cent of each true value. The instrument stays disarmed until every line passes, and while it is disarmed it refuses to report a measurement of your camera at all. The region measured is 128 by 128 pixels in every case, including yours.

The slate: the peak of the response

running…

A second instrument on the same pixels, reading a different number off the same call. Its specimens are sharpened edges whose true peak is known in closed form. An unsharpened edge is deliberately not on this slate: its peak is 1.000 because the curve is normalised to 1.000 at zero frequency and then falls, so it is true by construction and could not fail.

What a fake would do

The same two slates, run through an estimator that ignores its input and always returns the anchor's value with a tight error bar. It sails through both anchors. It cannot pass a control.

The specimens your browser drew, against the ones in the repository

No image file is downloaded, so no image file can be doctored. What is committed is the sha256 of the exact 8-bit pixels the generator produces from each set of parameters. Your browser draws them and hashes them here; the verifier regenerates the same manifest in Node and fails on any difference.

Now your camera

Find a straight edge and tilt it two to fifteen degrees off vertical. A door frame against a wall, the side of a book, a printed line, the sheet this page can print for you further down. Fill the yellow square with the edge and press. Nothing leaves your device: the frame is held in memory for the moment the arithmetic takes and is never uploaded, stored or sent anywhere.

Which of those two you use changes the answer, and the second one is where the interesting effect lives. A live video track hands most phones a downscaled preview rather than the sensor, and a box or bilinear downscaler does not overshoot, so the live route often shows no peak above 1.000 at all. A still taken with the phone's own camera app goes through the still-capture pipeline, which is the one that sharpens. The page prints the delivered size for whichever you choose, so you can see which you got.

Clicking the picture does:

not measured

Your response curve

The square that was measured

The number that cannot be a lens

The obvious dismissal of everything above is: fine, you have measured my phone's optics. You have not, and the page can prove it from your own frame rather than assert it.

For any passive incoherent optical system the optical transfer function is the normalised autocorrelation of the pupil function. An autocorrelation is largest at zero shift, so the transfer is 1.000 at zero spatial frequency and can never exceed it anywhere. ISO 12233 states half of that in its own terms clause, the value at zero frequency, which is a normalisation and not a bound; the bound above it is the autocorrelation's and not the standard's. The clause defines the optical transfer function as the two-dimensional Fourier transform of the point spread function and notes of it:

“The OTF is a complex function whose modulus has unity value at zero spatial frequency”
ISO 12233:2024, 3.20, Note 1 to entry

So a response above 1.000 is not a good lens or a bad one. It is arithmetic that no passive optical system performs. When your phone hands one back, something after the optics has added contrast at those spatial frequencies, and it added contrast that was never on the paper.

Measure something above and this line will say what your own chain did.

The halo you can go and look at

Overshoot is not only a number. A sharpener that lifts the response above unity does it by putting a bright band on the light side of every edge and a dark band on the dark side, and those bands are in your photograph while being demonstrably absent from the thing you photographed. Here is the edge profile of the shipped specimen at the strongest sharpening on the table below, whose two true levels are known exactly, overshooting :

And in your own frame

Measure something above and your own edge profile appears here.

Watch the headline number be manufactured

MTF50, the frequency at which contrast has halved, is the number most sharpness tests quote. It is also the number sharpening inflates hardest, because sharpening raises the curve near the middle and drags the half-height crossing outward with it. MTF50P, the crossing of half the curve's own peak, moves far less, because the peak it is measured against went up too. The table is the same optical blur with a difference of Gaussians on top at six gains, every row through the same estimator:

gaintrue peakmeasuredtrue MTF50measuredinflationMTF50Pinflation
computing…

At the strongest sharpening on the table the headline number is inflated times while the peak-relative one moves only times, and the peak itself reaches . Nothing optical changed between the first row and the last. If your own peak is above 1.000, the same thing has happened to your headline number, by an amount this page cannot separate out for you, because it never saw the frame before the pipeline did.

And what a peak of 1.000 does not rule out

That instrument points one way only, and the page had better be the one to say so. A peak above 1.000 is proof that something after the optics added contrast, because nothing before them can. A peak at 1.000 is not proof that nothing did, and the reason is arithmetic rather than modesty: the response is normalised to 1.000 at zero frequency, so 1.000 is the floor of this statistic, not a measurement that came out low. Whether a sharpened curve ever climbs back above that floor depends on how tight the sharpener is against how much blur it is fighting. Expand both transfers about zero frequency and the curve leaves 1.000 upward only when B2 − (1 + g)σ2 > 1/12, so a narrow sharpener can be run very hard with the peak sitting exactly on its floor the whole way.

Every row below sits at or under that threshold, so its true peak is exactly 1.000 in closed form. The estimator reads exactly 1.000 off all of them. The headline number moves anyway:

sharpenertrue peakmeasured peaktrue MTF50measuredinflationMTF50Pinflation
computing…

On the last row the true peak is and the headline number has still been inflated times. MTF50P does not rescue this. It is the crossing of half the curve's own peak, so when the peak is 1.000 it is the crossing of 0.500, which is MTF50: on rows above the two numbers are equal to every digit the estimator computes, and the pair moves together. The statistic that resists sharpening only starts resisting once the peak has noticed something, and this is the band where the peak has not.

So the reading to take from your own frame is one-directional, and the line below your own number says so. Above 1.000: something was added, and this page will show you the bands. At 1.000: the edge method saw nothing, which is a different sentence from nothing being there, and the table above is how much can fit in the difference. That is a limit of this method rather than of this page, and the way out of it is a second method measuring the same chain, which is not something one page and one photograph can do.

Cycles per millimetre, or an honest refusal

Cycles per pixel is a statement about the file. Cycles per millimetre is a statement about the paper, and to get there this page needs to know how many pixels of your picture cover a known length of the world. That is what the printable sheet is for, and it is where the whole thing can quietly go wrong.

Nearly every printer driver defaults to fit-to-page, and fit-to-page is not one hundred per cent. Two separate causes land in the same place, and both are arithmetic rather than folklore, computed here from the paper sizes the shared kit holds: an A4-sized page sent to a US Letter printer is scaled by min(215.9 / 210, 279.4 / 297) = per cent, and a page fitted inside the 4 to 6 mm all round that a consumer printer cannot reach is scaled by to per cent. Neither is announced. A sheet that claims to carry marks 150.000 mm apart and actually carries them 142 mm apart would not look wrong, and would make every millimetre on this page wrong by four per cent in a direction nobody could see. So the sheet carries a 150 mm ruler, and until you have measured it with a ruler of your own and typed in what you got, this page refuses to convert anything into millimetres. That refusal is the point of the whole exercise, not an inconvenience in front of it.

The sheet is built by the Wasteland's shared printable kit, so it carries the same four registration fiducials as every other printable page here: 4 mm squares whose centres sit at 20 and 170 mm across and 15 and 215 mm down from the top-left corner, a span of 150.000 mm. Nothing on it goes near the sheet edge, because a consumer printer generally cannot reach the last four to six millimetres all round, and borderless photo printing is the exception rather than the office default. The slanted patch adds its own pair of marks 40.000 mm apart, close to the edge itself, so the scale still works when you move in close enough that the sheet fiducials leave the frame.

prints on its own page, at true size, on A4 or Letter

  1. Print it. In the print dialogue turn OFF any scaling or fit-to-page option you can find, then measure the printed ruler with any ruler you own and type what you read below. If the driver ignored you, this step catches it.
  2. Photograph the sheet square on, in even light, with the slanted edge and at least two marks in the frame.
  3. Put the measured square on the edge, switch the click mode above to mark a printed reference point, and click the two marks.

mm

not calibrated: the printed ruler has not been measured, so millimetres are refused.

The two marks I clicked are:

no marks clicked yet

The sheet itself is checked the way every printable sheet here is checked: node scripts/check-print-geometry.mjs renders this page's print path through a headless browser, rasterises the result and measures where the four fiducials actually landed, in millimetres, on A4 and on US Letter. Nothing on the sheet reaches the paper edge, because most printers cannot reach the last four to six millimetres all round.

The edge on that sheet is drawn at 5.000 degrees, mid grey against unprinted paper, which is roughly a four-to-one edge, low enough that nothing in the chain is driven into black or white. It is our own geometry and not a reproduction of any published test chart.

Every free choice, and what each one is worth

A measurement chain like this one has perhaps ten places where a reasonable person would choose differently, and almost none of them are ever reported. Each table below is the same estimator with one thing changed, computed here, now, in front of you: either the choice itself, or the specimen that makes the choice cost something. A choice priced only on a specimen that cannot show its price is not priced at all, which is why two of the tables below are specimens rather than settings.

Does the answer know the angle? It must not

The true answer barely moves with the edge angle, so a correct estimator's answer must not move either. Across the angles the geometry gate admits, this one varies by percentage points. The shallow rows below two degrees are run with the gate switched off, to show what is on the other side of it.

degreestrue MTF50measuredresidual
computing…

Does it know the contrast? It must not, and then eight bits run out

The response is a ratio normalised to 1.000 at zero frequency, so how dark and how light the two sides are should cancel exactly. The table below is not a handful of chosen pairs: it is the symmetric family swept on a fixed grid, pairs from 0.06/0.94 down to 0.48/0.52 in steps of 0.02, every one drawn and measured here just now. While the edge still spans at least 70 code values the cancellation holds to within per cent, and the worst row of the sweep is at . Below that the 8-bit quantisation of the output takes over and the worst row is out by , at .

It is not a threshold and it is not a drift. It is quantisation noise whose envelope widens as the code values run out, so a narrow edge can still land better than a wide one: rows below 70 code values beat the worst row above it, and the best of them is out by only . What the sweep bounds is the envelope, not any one row, and a page that quoted its four prettiest rows could have claimed a figure several times better than this one. That is why the sweep is regular, why the number quoted is its worst row rather than its best, and why the verifier re-runs the same family at a quarter of this step and fails if anything finer beats the bound printed here. It is a limit of the measurement, measured rather than asserted, and it is why the printed target is deliberately not a faint grey on grey.

levels, linear lightcode values spannedmeasuredresidual
computing…

The choice nobody mentions that moves the answer by twenty per cent

After fitting a line through the edge, you have to decide how far either side of it to keep. Burns and Williams call this limiting the data length. With a noiseless specimen the choice does nothing at all, which is exactly why it is easy to leave unstated. Add half a per cent of pixel noise, keep the whole 128-pixel row, and the answer falls by per cent, because a little noise anywhere in the row drags the centroid that locates the edge. Keep only 16 pixels either side and the same frame gives per cent. This page keeps 16.

pixel noise±8 px±16 px±32 pxwhole row
computing…

The same choice, priced against the thing that pulls the other way

That table makes the short window look free, and a page that stopped there would have priced its own choice against the one failure the choice happens to fix. Limiting the data length throws the edge profile's tails away, and there is something that lives in the tails: veiling glare. A lens scatters a few per cent of the light from the bright side into a broad, shallow halo, so the true response drops while the profile near the edge barely changes. The generator above draws that from the same expression as the sharpener, with the gain negative instead of positive, and states its true response in the same closed form. Each cell is the residual against that closed form:

halotrue MTF50true cost of the halo±8 px±16 px±32 px±64 px
computing…

The halo costs the true response . The window this page ships cannot see it and reports , while the whole row lands at . The two tables want the window moved in opposite directions, and neither is wrong: 16 pixels either side is right against noise, by twenty points, and wrong against glare, by four. This page keeps 16 and prints both numbers rather than the one that flatters the choice. A reader whose own frame has a bright window or a lamp in it should read their number as an upper bound.

The one difference from the standard, priced

This page fits the edge with a straight line. The standard's fourth edition moved to a fifth-order polynomial, which is named further down and, until now, was only named. Here is what it costs. bow below is a sagitta in pixels at the middle of the region, falling to zero at its top and bottom rows: a printed sheet that will not lie perfectly flat, or any lens distortion at all, delivers exactly that. The closed form does not move with it, and that is the point, because a curved edge is a fact about the thing photographed and not about the lens. Everything lost below is lost by the straight-line fit.

what is wrong with the edgerms of the row centroids about the fitted linemeasuredresidual
computing…

The last two rows are not bowed at all. They are straight edges under pixel noise, and they sit in the same table because both failures raise the same diagnostic and every degraded row comes back low. The rms scatter of the row centroids about the fitted line is pixels on the clean straight edge, at a pixel of bow and at one per cent noise. That number is computed for your own frame too, and it is now printed beside your own reading.

It is a direction of doubt and not a conversion factor, and the table is arranged so you can see that rather than take it on trust: half a per cent of noise raises the scatter to pixels and costs only , because the limited window above absorbs it, while pixels of the same scatter arriving from bow costs . So a high number means the fitted line does not describe your edge, which is a reason to distrust what sits under it. It does not say which of the two you have, and this page will not pretend otherwise.

The corrections in the chain

Three of the steps are corrections for the measurement's own instruments rather than for anything about a camera, and each row below is the same specimen run with that one correction taken out, so you can read what it was worth. The fully corrected pipeline sits per cent from the closed form on the anchor. That residual is this implementation's bias.

pipelineMTF50residual against the closed formwhat the step was worth
computing…

And it is not inside the error bar

The interval this page prints, beside your reading and beside every specimen, is 96 resamples of that frame's own rows in blocks of eight, recomputing the whole estimate each time, combined in quadrature with the frequency-axis term the fitted slope's standard error contributes. A bias is by construction identical in every resample, so no bootstrap can see it. The interval is repeatability. It is not accuracy, and adding the two would be inventing a number rather than measuring one.

The size of that gap is not an argument either. Here are the four specimens whose true answers are known exactly, each against the interval this page would print for it:

specimenclosed formmeasuredoffset95% intervaloffset ÷ intervalclosed form
computing…

On the anchor the offset is cycles per pixel against an interval of ±, which is times wider than the bar printed next to it. On specimens the exactly known closed form falls outside this page's own 95 per cent interval. That is what an interval of repeatability means, and it is why the slate above is scored against two per cent of each true value rather than against the interval: an instrument graded on its own error bar would grade itself on how steady it is, not on whether it is right.

The linearisation is the one with teeth. Pixel values are not proportional to light: sRGB bends them, and the slanted-edge method needs light. Skipping that step moves the answer by percentage points on the four-to-one edge above and by on a near black-to-white one, which is the second reason the printed target is low contrast. Your camera's true transfer curve is unknowable from a web page, so sRGB is assumed. That assumption is the largest thing on this page that cannot be checked from inside it.

What it refuses, and why refusing is the harder half

Every edge finder ever written will hand back the centroid of noise if you let it. These six specimens are run through the identical estimator, and for each of them the correct output is not a number:

specimenrequiredreturnedwhat it said
computing…

The upright edge is the interesting one. An edge parallel to the pixel columns is useless to this method because every row crosses it at the same place, so the quarter- pixel bins that the whole measurement is built on never get filled. The page refuses it on an angle rule, at two degrees. Switch that rule off and point the estimator at a perfectly upright edge anyway, and

The shallow rows in the angle table above show the other side of this: down to degrees the method still answers a clean specimen, and answers it to within , so the two degree floor is a margin rather than a cliff, and this page says so rather than implying a precipice that is not there. A clean synthetic specimen is not a photograph, though: what fails first below two degrees on a real frame is the bin filling, and the upright row above is what that failure looks like when it is complete.

What this number is not

The check, in full

Named choices, all of them: region of interest 128 × 128 pixels, placed by you; limiting data length ±16 pixels; oversampling 4 bins per pixel with empty bins linearly interpolated and counted; window Tukey, α = 0.5; transform zero-padded to 512; luminance 0.2126 R + 0.7152 G + 0.0722 B after the sRGB decode; edge fitted by ordinary least squares on the row centroids, a straight line rather than the fifth-order polynomial the current standard specifies, which costs what the bow table above measures; corrections for the discrete derivative, for the quarter-pixel bin and for the edge angle, each taken out in turn in the table above; peak searched at or below 0.5 cycles per pixel only, since above the sampling limit the ordinate is aliasing; refusal thresholds of 2 to 15 degrees, 0.1% clipped pixels, 10% empty bins and ±15% interval width; interval from 96 bootstrap resamples over blocks of 8 image rows, recomputing the entire estimate each time, combined in quadrature with the frequency-axis term the fitted slope's own standard error contributes.

To recheck all of it outside this page: node research/slanted-edge-mtf/verify-slanted-edge-mtf.mjs, which loads these same three files and recomputes every number above from them, and node scripts/check-live-sensor.mjs --only=slanted-edge-mtf, which drives the button above in a real browser with a camera stream whose true answer this page has never seen.

Sources

The Wasteland's printable pages write to paper and then go dark. This one asks for the paper back.