Skip to content

Measuring tone and colour

A picture can look fine in a thumbnail and still be wrong: a room photo blown out by the window, a packshot on a grey that should have been white, a portrait whose face is lost in a dark painting. Image Statistics (image/statistics) puts numbers on it. It reads the pixels and returns how bright they are, how many are crushed to black or blown to white, and what colour each range of tones has — for the whole picture, for the cells of a grid, for given rectangles, or only inside a mask. No model is asked: the same picture gives the same numbers every time, at no credit cost.

Domain The question
A shop’s catalogue does this packshot sit on a white backdrop, as the marketplace asks?
Hospitality and real estate is this room photo well exposed, or blown out by the window?
Generated pictures is this picture too dark or too bright to publish?
Publishing and editorial is the subject lit, even when the picture around it is dark?
Layouts and covers which area of the picture is dark enough for white text?

A hotel room photographed in daylight, and what Image Statistics returns for it. A ramp from black to white with the quantiles of the brightness marked: p01 near black, p05 0.203, the median 0.647 in red, p95 0.923, p99 near white. Near black 0.1%, near white 0.2%, luminance mean 0.384. The mean colour, a warm grey-beige, Oklab L 0.682, a +0.009, b +0.023. Three tone bands: shadows 8.7% of the picture in dark browns, midtones 35.3% in browns and olive, highlights 56.0% in beiges and the sky blues of the window.

For the whole picture — and for each cell or region — the node returns:

  • luma, the brightness of each pixel as the eye reads it on screen (0 black, 1 white): its quantiles p01, p05, p25, p50 (the median), p75, p95, p99 and its mean;
  • the share of pixels near black (luma at or below near_black, 0.01) and near white (at or above near_white, 0.99): crushed shadows, blown-out highlights, or a white backdrop;
  • luminance, the physical amount of light (linear), mean and median;
  • the mean colour in Oklab: l lightness, a (positive leans red, negative green) and b (positive leans yellow, negative blue) — a colour cast reads at a glance;
  • tone bands: the shadows, midtones and highlights (Oklab lightness 0–0.4, 0.4–0.7, 0.7–1 by default), each with its share of the picture, its chroma quantiles (0 is grey) and the hues that fill it in twelve 30° sectors.

Six outputs carry the result:

Output What it is
statistics the whole result as JSON (madoo.image-statistics/v1): global and every region
median_luma, mean_luma the median and mean brightness of the whole picture, 0–1
clipped_black, clipped_white the share of near-black and near-white pixels, 0–1
quality_check the check, for a Quality Gate

The four numbers are ready for a Predicate (median_luma below 0.3); they describe the whole picture, or only what the mask selects.

One hotel room photo at three exposures, checked with min_median_luma 0.25, max_clipped_white 0.02 and max_clipped_black 0.05. Two stops darker: median 0.339, no pixel near white, 0.8% near black — passed. As shot: median 0.647, 0.2% near white, 0.1% near black — passed. One and a half stops brighter: median 0.991, half the picture near white — failed, CLIPPED_WHITE_TOO_HIGH.

The same room, made two stops darker and one and a half stops brighter in linear light — what a camera does with a shorter or longer exposure. The median follows the exposure; the near-white share tells the overexposed one apart at once: half of it is pure white, where the window, the curtains and the bed were.

Set limits on the node and its quality_check fails when the picture crosses them:

Limit Fails when
min_median_luma the median brightness is below it — too dark
max_median_luma the median brightness is above it — too bright
max_clipped_white more of the picture than this is near white — blown out
max_clipped_black more of the picture than this is near black — crushed
min_white_share less of the picture than this is near white — see the next section

Wire quality_check into a Quality Gate and the picture to deliver through the gate’s value: a blown-out photo is held back, the others go on. The check fails too when there is nothing to measure (NOT_OBSERVABLE: a transparent picture, an empty mask). Without limits it always passes and only reports the numbers.

The limits depend on the pictures. A night launch (median 0.147) or a Vermeer (0.162) are dark by intent; a gate set for daylight room photos would hold them back. Measure a few good pictures of your own first, then set the limits a little outside them.

Three packshots checked for a white backdrop with min_white_share 0.3. A mustard jacket on a light backdrop: with near_white 0.99, 0.00% of the picture counts as white and the check fails; with near_white 0.94, 32.9% is white and it passes; its top-left cell has a median of 0.966. A red teapot on grey (median 0.698 in the corner) and a rust dress on light grey (0.791) fail with both settings.

A marketplace that asks for a white backdrop usually means pure white, #FFFFFF. With the default near_white of 0.99, min_white_share checks exactly that: the share of the picture that is pure white. A studio white lit from one side is not: the jacket’s backdrop is a bright off-white around 0.96, and none of it reaches 0.99.

To accept a lit studio white, lower Near White to it — here 0.94: a third of the picture is backdrop and the check passes, while the teapot and the dress, on greys of 0.70 and 0.79, still fail. Measure your own backdrop first: the median of a corner cell of a 3 × 3 grid is its brightness.

near_white also decides what max_clipped_white counts as blown out. To check a white backdrop and blown-out highlights on the same picture, use two Image Statistics steps, each with its own near_white and its own check_id, both feeding the same Quality Gate.

A pool by a lake and a villa above a lake, each in a 3 × 3 grid with the median brightness and the mean colour of each cell. The pool: the sky and the lake in the top middle at 0.83, the shaded garden on the left at 0.29 to 0.37, the water at 0.66 and 0.77. The villa: the sky at 0.77 to 0.59, the lake and the facade at 0.49 to 0.52, the garden at the bottom at 0.38 to 0.46.

Grid Rows and Grid Columns (1 to 16) divide the picture into equal cells, each measured on its own as well as the whole picture. The cells come back in statistics.regions, in reading order, named r1c1, r1c2, … — read one with Extract (regions[0].luma.p50) or all with the JSON Enumerator (array path regions).

A grid answers “where”: which part of a cover is dark enough for a white title (the shaded garden of the pool, at 0.29), whether the sky of a villa is blown out, whether a generated grid of variants has one cell much darker than the others.

Instead of the grid, wire rectangles into Regions: a list [{"x": 0, "y": 0, "width": 512, "height": 512}] (up to 64; an id is optional; cell, bbox or context objects are accepted too), or the measurements of Measure Subject as they are: the box of each subject it found is measured — a product’s box, the subject of each cell. The faces of Face Detect work the same way: each face is a region (face-1, face-2, …), to tell a face in shadow from the bright scene around it.

A region that goes beyond the picture fails the step (REGION_OUT_OF_BOUNDS): it is never cut silently.

Three pictures beside their Remove Background cut-out. Vermeer’s Girl with a Pearl Earring: the whole picture has a median of 0.162, the girl alone 0.451; she covers 42.2% of it. Apollo 11’s commander on a black backdrop: 0.178 for the whole picture, 0.711 for the astronaut in his white suit, 42.8%. A rust dress on a light backdrop: 0.707 for the whole picture, 0.313 for the dress, 32.1%.

The backdrop is often most of the picture, and it decides the numbers: the Vermeer is “dark” because of its background, a dress on light grey is “bright” because of the grey. Connect a Mask — the cut-out of Remove Background, or any black and white mask of the same size — and every number describes only the pixels inside it: the girl’s face is lit (0.451), the astronaut’s suit is bright (0.711), the rust dress is a dark colour (0.313). Each region also reports mask_share, the part of it the mask covers. In a scene, the mask of Instance Segment does the same for a person, an animal or an object it finds — the main person of a portrait, without the café behind.

A pixel is inside when the mask’s alpha — or, on a mask without transparency, its brightness — is above Mask Threshold (128). A mask of another size fails the step (MASK_SIZE_MISMATCH): it is never stretched.

When the mask selects nothing — Remove Background finds no subject in a landscape, a pool or a night sky — there is nothing to measure: the region says status: "not_observable" with the reason mask_empty_in_region, the four numbers are not provided (the steps that need them are skipped, not failed) and the check fails with NOT_OBSERVABLE.

A cut-out can also be measured on its own, without a mask: its transparent pixels (alpha under Alpha Min, 250) are left out, so the numbers are the subject’s.

  • Upright. A phone photo stored sideways is turned as its EXIF orientation says before anything is measured; grid cells and regions are in the pixels of the upright picture.
  • In sRGB. A picture with a colour profile (Adobe RGB, Display P3, CMYK) is converted to sRGB first; the report says profile_policy: "converted". A picture without a profile is read as sRGB and says so (assumed_srgb); set Untagged Images to fail to refuse it instead (PROFILE_REQUIRED). A CMYK file without a profile is always refused.
  • At full resolution. The picture is never scaled down. A region with more pixels than Max Samples (about a million) is measured on a regular grid of its pixels — every second pixel in each direction, or every third — always the same ones; sampling in each region says the step and how many pixels were measured.
  • At 8 bits. A 16-bit file is measured at 8 bits per channel, with a warning in the report.
  • SDR only. An HDR picture (the PQ or HLG curve of HDR photos and videos) cannot be measured as an ordinary picture: the step fails with HDR_NOT_SUPPORTED — convert it to SDR first. A photo with a gain map (the “HDR” photos of recent phones) carries an ordinary picture too: that one is measured, with a warning.

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

An exposure gate for room photos. Room photo → Image Statistics (min_median_luma, max_clipped_white) → Quality Gate (all_must_pass, route) → delivery through the gate’s value. Photos that pass go on; the others are held back with the reason in the check.

Packshots on white. Packshot → Image Statistics (near_white set to your studio white, min_white_share) → Quality Gate. Add Color Palette with background: exclude to describe the product without its backdrop.

Only the subject. Photo → Remove Background → the cut-out into Image Statistics’ Mask, the photo into its Image. The numbers describe the person or the product, not the room.

The darkest area for a title. Cover → Image Statistics with a 3 × 3 grid → JSON Enumerator over regions → a Predicate on each cell’s luma.p50 (below 0.35) and a Filter → the cells a white title reads on.

A brightness branch. median_luma into a Predicate (below 0.3) and two Filters: a dark generated picture goes to Exposure for more light, the others go on unchanged.

Setting Default What it does
Grid Rows, Grid Columns (grid_rows, grid_cols) 1, 1 measure the cells of an equal grid too, 1 to 16 each
Untagged Images (untagged) assume_srgb read a picture without a profile as sRGB, or fail
Alpha Min (alpha_min) 250 pixels less opaque than this (0–255) are left out
Mask Threshold (mask_threshold) 128 a mask pixel above this is inside
Max Samples (max_samples) 1,048,576 pixels measured per region at most, on a regular grid
Tone Bands (bands) [0, 0.4, 0.7, 1] the boundaries of the tone bands on Oklab lightness, at most 8 bands
Near Black (near_black) 0.01 luma at or below this counts as near black
Near White (near_white) 0.99 luma at or above this counts as near white
Min White Share (min_white_share) — fail when less of the picture is near white
Max Near-White Share (max_clipped_white) — fail when more of the picture is near white
Max Near-Black Share (max_clipped_black) — fail when more of the picture is near black
Min / Max Median Brightness (min_median_luma, max_median_luma) — fail outside these medians
Check ID (check_id) image-statistics the check’s name in a Quality Gate

A wrong setting — a grid of 20 rows, bands that do not increase, a minimum above its maximum — is reported when the workflow is saved, with INVALID_IMAGE_STATISTICS_PARAMETER. The limits apply to the whole picture (or what the mask selects), not to each cell.

  • It measures pixels, not meaning. It knows the window is bright, not that it is a window. To know which part of the picture is the subject, cut it out first (Remove Background) or find its box (Measure Subject).
  • It does not judge taste. A dark painting and an underexposed photo have the same numbers; the limits are yours.
  • It does not fix the picture. To brighten or darken, use Exposure or Levels; then measure again.
  • The colours of a picture — its palette, dominant and accent colours — come from Color Palette; Image Statistics gives the mean colour and the hues of each tone band.
  • Fine detail — whether a picture is soft, grainy, over-compressed or sharpened against its reference — comes from Image Detail.

The tone lab runs two of the recipes, at no credit cost:

  • the hotel room one and a half stops too bright: median 0.991, half of it near white; the check fails with CLIPPED_WHITE_TOO_HIGH and the Quality Gate holds the photo back;
  • the mustard jacket checked for a white backdrop with Near White at 0.94: 32.9% of it is backdrop, the check passes and the packshot is released.

The lab workflow: the room photo goes to Image Statistics with exposure limits, which gives the median brightness 0.991, the near-white share 0.507 and a failed check to a Quality Gate that holds the photo back; the packshot goes to Image Statistics checking a white backdrop, which gives a near-white share of 0.329 and a check that lets the Quality Gate release it.