Skip to content

Measuring fine detail

A picture can keep its colours and its composition and still lose what makes it real: the weave of a fabric smeared by a model, a product enlarged from a small file, a photo saved once too often as JPEG, grain that was never in the shot, halos left by sharpening. Seen as a thumbnail, none of it shows. Image Detail (image/detail) looks at the pixels one small patch at a time, at full resolution, and returns numbers for that fine detail. It also says where to look: a rectangle around the most detailed patch, ready for Crop, so a vision model can judge that area at full size instead of a scaled-down picture. No model is asked: the same picture gives the same numbers every time, at no credit cost.

Domain The question
A shop’s catalogue did the generated packshot keep the weave of the fabric, or smear it?
Generated and edited pictures is this picture as detailed as the reference it was made from?
Upscaling and delivery did the enlargement or the compression lose detail the original had?
Review by a vision model which area of the picture should the model see at full size?
Publishing and archives is this scan sharp, or soft, grainy or over-sharpened?

A mustard linen jacket on a light studio backdrop, 1024 × 1024, with three rectangles drawn on it, and the three crops beside it. 441 patches of 24 px examined, one every 48 px; 330 flat enough to read residuals on, 111 too structured (seams, buttons, folds). Medians over the eligible patches: gradient 0.0093, residual 3 × 3 0.0030, 7 × 7 0.0036; residual correlation x −0.040, y −0.139. low_gradient, the flattest, is a patch of backdrop at 639, 0 (gradient 0.0034). detail, the most detailed, is the cuff with its button at 543, 639 (gradient 0.1613, residual 0.0262). bright_transition, bright and detailed, is the collar and its label at 303, 0. Without a mask the flattest patch is the backdrop, and the medians include it.

The node lays square patches (24 px, from Patch Size) over the picture every 48 px (Stride; the step grows on a large picture so that at most Max Patches, 4096, are examined). For each patch it reads the brightness of its pixels (luma, 0 black to 1 white) and computes:

  • the gradient: how much the brightness changes from one pixel to the next — high on edges and texture, near zero on a smooth area;
  • the residuals: what is left of each pixel once the average of its 3 × 3 neighbourhood is taken away (and, separately, of its 7 × 7 neighbourhood) — the finest detail, at two scales. Their spread is residual_mad_3 and residual_mad_7 (a robust standard deviation);
  • the residual correlation (residual_corr_x, residual_corr_y, −1 to 1): whether neighbouring residuals go together. Independent grain gives about −0.17 on a 3 × 3 window; smoothing gives a clearly positive value, because each pixel then resembles its neighbour.

Residuals are only meaningful where the picture is flat: on an edge they measure the edge. So the global figures are medians over the eligible patches — opaque, inside the mask if there is one, neither too dark nor too bright (luma_min 0.025, luma_max 0.975) and flat enough (gradient at most Flat Gradient Max, 0.055). The report counts every patch it left out and why. The same medians are given per tone band — five bands of brightness, 0–0.2 to 0.8–1 — since shadows and highlights do not carry detail the same way.

Then it chooses patches to look at, one per role, in the order of Roles (select), at least Min Separation (128 px) apart:

Role The patch
low_gradient the flattest eligible patch: where residuals read cleanly — grain, noise, compression
detail the most detailed opaque patch: texture, a seam, a button — where sharpness shows
bright_transition the opaque patch where brightness and detail together are highest: a label, a lit edge

Each comes with a context, a rectangle of Context Size (384 px) centred on it and kept inside the picture. Four outputs carry the result:

Output What it is
detail the whole result as JSON (madoo.image-detail/v1): patches, global, bands, selected
selected_regions the context of every chosen patch, best first: [{"role": "detail", "x": 543, "y": 639, "width": 384, "height": 384}]
best_region the first of them: wire it into Crop’s Region
quality_check the check, for a Quality Gate: it fails when no patch is eligible

The crop Image Detail chose on the jacket: the cuff with its wooden button and the linen around it, 384 × 384 at full size.

A vision model shown a whole 1024 × 1024 picture sees it scaled down; the weave of a fabric is gone before it is judged. Wire best_region into the Region of Crop, the same picture into Crop’s Image, and the crop into Image Analysis: the model sees 384 × 384 pixels of the cuff at full size. On the jacket it answered linen: a subtly slubby woven surface with natural creases, sharp, no artefacts.

Crop also reads the whole detail document (it cuts the first chosen context) and each item of selected_regions: enumerate them with the JSON Enumerator to cut every chosen area. The measuring steps are free; only the vision step costs credits.

The same 120 px of linen in six versions, enlarged 2×, each measured inside the cut-out of the original jacket. The photo: 133 of 229 patches eligible, residual 3 × 3 0.0143, 7 × 7 / 3 × 3 1.3, correlation x +0.27, most detailed patch 0.161. Blurred 1.2 px: 206 eligible, residual 0.0031, ratio 3.0, correlation +0.61, 0.084. Enlarged 2× from half: 191, 0.0037, 3.0, +0.59, 0.103. JPEG quality 15: 164, 0.0026, 4.2, +0.28, 0.158; the blocks show. Noise added: 14, 0.0203, 1.1, −0.04, 0.166. Sharpened: 0 eligible, not observable; the most detailed patch 0.338.

One number alone says little: a linen and a silk, a studio and a phone camera, all have their own fine detail. What the node tells reliably is how a picture differs from its reference — the original photo, the product shot a model was given, the file before it was enlarged or compressed — when both are measured with the same settings. The jacket above was changed in five ways and each version measured inside the cut-out of the original:

Version Eligible Residual 3 × 3 7 × 7 / 3 × 3 Correlation x Most detailed patch
the photo 133 0.0143 1.3 +0.27 0.161
blurred 1.2 px 206 0.0031 3.0 +0.61 0.084
enlarged 2× from half 191 0.0037 3.0 +0.59 0.103
JPEG quality 15 164 0.0026 4.2 +0.28 0.158
noise added 14 0.0203 1.1 −0.04 0.166
sharpened 0 — — — 0.338
  • Detail lost — a blur, an enlargement, heavy compression: the 3 × 3 residual falls to a fraction of the reference’s (here a fifth to a quarter), the 7 × 7 one falls less, so their ratio rises. More patches become flat enough to be eligible. Blur and enlargement also raise the correlation; JPEG leaves it, but its 8 × 8 blocks are flat inside, which the 3 × 3 residual reads as almost no detail.
  • Grain added: the 3 × 3 residual rises, the ratio falls towards 1 and the correlation towards zero; the grain makes most patches too structured to be eligible.
  • Sharpened: the most detailed patch doubles its gradient, and nothing stays flat enough to read residuals on.

These are the changes on this jacket; the size of each change depends on the picture and on how much it was altered. Measure the reference and the new picture with the same settings — the same mask, if there is one — and compare them: see the recipe below.

A linen jacket and a rust dress, each measured on the whole picture and only inside its Remove Background cut-out, with the flattest patch drawn in blue. The jacket: whole picture 330 of 441 patches eligible, residual 3 × 3 0.0030, flattest patch on the backdrop at 639, 0; inside the subject 133 eligible, residual 0.0143, flattest patch on the chest at 399, 111. The dress: whole picture 407 eligible, residual 0.0076, flattest patch on the backdrop at 447, 0; inside 117 eligible, residual 0.0121, flattest patch on the skirt at 399, 640.

On a packshot most of the flat patches are backdrop, so the medians describe the studio light, not the product: residuals of 0.0030 on the jacket’s whole picture, 0.0143 on the linen alone. Connect a Mask — the cut-out of Remove Background, or any black and white mask of the same size — and only patches whose every pixel is inside it count. The flattest patch then moves from the backdrop to the cloth.

A patch counts as inside when every pixel of the mask under it is above Mask Threshold (128) — its alpha, or its brightness on a mask without transparency. Patches across the outline are left out (outside_mask), so a thin subject keeps few patches. A mask of another size fails the step (MASK_SIZE_MISMATCH): it is never stretched. A cut-out can also be measured on its own, without a mask: a patch with a pixel less opaque than Alpha Min (250), or within 3 px of one, is left out.

Three pictures with few or no patches to read residuals on. A teapot with a spotted glaze measured inside its cut-out: 0 of 69 patches eligible, 69 too structured; not observable, the check fails with NOT_OBSERVABLE. An oil painting: 37 of 696 eligible, 659 too structured; medians over 37 patches only. The jacket sharpened, inside its cut-out: 0 of 229 eligible; not observable. In each the most detailed patch is still chosen.

Some pictures have no flat area: a spotted glaze puts an edge in every patch, a painting carries its canvas and its brushwork everywhere, a sharpened picture has halos on every fold. When no patch is eligible the global figures say status: "not_observable" with the reason no_eligible_patches, and the check fails with NOT_OBSERVABLE; the step itself succeeds, and the patches to look at are still chosen among the opaque ones — the most detailed patch does not need to be flat. Only low_gradient chooses among eligible patches: asked alone on such a picture, it finds none, and the node gives the centre of the picture instead (role: "center_fallback"), as it does for a picture with no opaque pixel. With few eligible patches (global.n), read the medians with care.

For a reference that has eligible patches, a new picture with none is itself a finding: it was made much more structured — sharpened, or grained. For a surface that is patterned by nature, Flat Gradient Max can be raised (0.1, 0.2), as long as the reference and the new picture are measured with the same value.

  • Upright. A phone photo stored sideways is turned as its EXIF orientation says; every rectangle is in the pixels of the upright picture, as Crop expects.
  • In sRGB. A picture with a colour profile is converted to sRGB first (profile_policy: "converted"); one without is read as sRGB and says so (assumed_srgb). Set Untagged Images to fail to refuse it (PROFILE_REQUIRED).
  • At full resolution. The picture is never scaled down: fine detail is exactly what scaling would remove. The brightness is computed patch by patch, never for the whole picture at once; settings.step says the step used.
  • SDR only. An HDR picture (the PQ or HLG curve) fails with HDR_NOT_SUPPORTED: convert it to SDR first.

Pictures up to 40 megapixels are read; a larger one fails with IMAGE_TOO_LARGE before it is decoded.

A closer look for a vision model. Photo → Image Detail → best_region into Crop’s Region, the photo into Crop’s Image → Image Analysis with the question. The model judges the most detailed area at full size.

Every area that matters. Image Detail with Roles ["detail", "low_gradient", "bright_transition"] → JSON Enumerator over selected_regions → Crop → Image Analysis per crop, and an aggregator to gather the answers.

Did the generated picture keep the detail? Two Image Detail steps with the same settings — one on the reference, one on the generated picture — each followed by Extract (path global.residual_mad_3, expected_type number) → Calculate (expression generated / reference) → a Predicate (below 0.5) and two Filters: a picture that kept less than half of the reference’s fine detail goes to review, the others go on. Measure both inside the product’s cut-out to leave the backdrop out.

Only the product. Packshot → Remove Background → the cut-out into Image Detail’s Mask, the packshot into its Image: the numbers and the flattest patch are the product’s.

Something to read residuals on. Image Detail’s quality_check into a Quality Gate, with its own Check ID: a picture with no flat area is held back before a step that needs one.

Setting Default What it does
Roles (select) all three which patches to choose, in order: low_gradient, detail, bright_transition
Context Size (context_size) 384 the side of the rectangle around each chosen patch, at least Patch Size
Max Regions (max_regions) 3 how many patches to choose, 1 to 3
Min Separation (min_separation) 128 the least distance between two chosen patches, in pixels
Patch Size (patch_size) 24 the side of each measured patch, 8 to 128
Stride (stride) 48 the distance between patches; it grows on large pictures
Max Patches (max_patches) 4096 the most patches examined
Flat Gradient Max (flat_gradient_max) 0.055 a patch with more gradient is too structured to be eligible
Luma Min, Luma Max (luma_min, luma_max) 0.025, 0.975 darker or brighter patches are left out
Alpha Min (alpha_min) 250 a patch with a pixel less opaque than this is left out
Mask Threshold (mask_threshold) 128 a mask pixel above this is inside
Untagged Images (untagged) assume_srgb read a picture without a profile as sRGB, or fail
Check ID (check_id) image-detail the check’s name in a Quality Gate

A wrong setting — a Context Size below Patch Size, a role that does not exist, a value out of range — is reported when the workflow is saved, with INVALID_IMAGE_DETAIL_PARAMETER.

  • It does not name the cause. Residuals include the weave of a fabric, sensor noise, film grain, sharpening and compression alike. A number says how much fine detail there is and how it is shaped, not where it came from; that is what the comparison with a reference, and the closer look of a vision model, are for.
  • It does not judge quality on its own. There is no “good” residual: a silk is smoother than a tweed. The limits come from your reference pictures.
  • It does not fix the picture. To restore detail use an AI upscale or edit; then measure again.
  • Brightness and colour — exposure, a white backdrop, a colour cast — come from Image Statistics; where the subject is from Measure Subject.

The fine detail lab runs the closer look on the jacket, for about one credit (Remove Background and Image Analysis; the measuring steps are free): Remove Background cuts the jacket out, Image Detail measures the linen inside the cut-out — 133 of 441 patches eligible, the flattest patch now on the chest — and chooses the cuff as the most detailed area; Crop cuts it at full size and Image Analysis judges it: linen, sharp, no artefacts.

The lab workflow: the garment photo goes to Remove Background and to Image Detail, which receives the cut-out as its mask and gives 133 of 441 eligible patches and the regions detail and low_gradient; Crop cuts the best region, the cuff at 384 px, and Image Analysis judges it: linen, sharp.