madoo.image-statistics/v1
Madoo image statistics — Tone and colour of an image, of grid cells or regions, optionally inside a mask: luma quantiles, near-black and near-white shares, luminance, Oklab means, colour per tone band (Image Statistics).
| Reference | madoo.image-statistics/v1 |
| Schema id | urn:madoo:schema:image-statistics:v1 |
| Version | 1.0.0 |
| Served at | GET /api/v1/json-schemas/madoo.image-statistics/v1 · MCP get_json_schema |
Where it appears
Section titled “Where it appears”- Produced by:
image/statistics→statistics
Fields
Section titled “Fields”| Field | Type | Required | Rules | Description |
|---|---|---|---|---|
schema |
"madoo.image-statistics/v1" |
yes | ||
algorithm_version |
string | yes | ||
image |
object | yes | ||
image.width |
integer | yes | ≥ 1 | After EXIF orientation. |
image.height |
integer | yes | ≥ 1 | |
image.orientation_applied |
boolean | yes | True when the EXIF orientation rotated or mirrored the image. | |
color |
object | yes | ||
color.working_space |
"srgb-8" |
yes | ||
color.profile_policy |
one of "srgb", "converted", "assumed_srgb" |
yes | srgb: the file is sRGB; converted: its ICC profile was converted to sRGB; assumed_srgb: an RGB image without a profile, read as sRGB. | |
color.source_depth |
integer or null | yes | Bits per channel of the file. | |
color.analysis_depth |
8 |
yes | ||
warnings |
array of string | yes | Facts that limit how the numbers may be read: a JPEG source (compression can contaminate detail measurements), several frames or pages (the first was measured), a source deeper than 8 bits (measured at 8 bits). | |
settings |
object | yes | ||
settings.grid |
object or null | |||
settings.grid.rows |
integer | |||
settings.grid.cols |
integer | |||
settings.alpha_min |
integer | yes | ≥ 0; ≤ 255 | |
settings.mask |
boolean | yes | ||
settings.mask_threshold |
integer or null | |||
settings.max_samples |
integer | yes | ≥ 1 | |
settings.bands |
array of number | yes | min items 2 | |
settings.near_black |
number | yes | ||
settings.near_white |
number | yes | ||
global |
object | yes | ||
global.index |
integer | yes | ≥ 0 | 0 for the global region, then 1..n in reading order. |
global.id |
string | yes | global, r |
|
global.cell |
object | yes | Pixels of the oriented image, origin top left; covers columns x..x+width-1 and rows y..y+height-1. | |
global.cell.x |
integer | yes | ≥ 0 | |
global.cell.y |
integer | yes | ≥ 0 | |
global.cell.width |
integer | yes | ≥ 1 | |
global.cell.height |
integer | yes | ≥ 1 | |
global.mask_share |
number or null | ≥ 0; ≤ 1 | Share of the region inside the mask; present only when a mask is connected. | |
global.sampling |
object | yes | ||
global.sampling.method |
one of "grid", "full" |
yes | ||
global.sampling.step |
integer | yes | ≥ 1 | |
global.sampling.eligible_pixels |
integer | yes | ≥ 0 | Pixels of the region that may be measured: opaque enough and inside the mask. |
global.sampling.sampled_pixels |
integer | yes | ≥ 0 | |
global.status |
one of "ok", "not_observable" |
yes | not_observable: the measurement is undefined here (see reason); its numbers are null. Zero is a valid measurement, never a stand-in for null. | |
global.reason |
one of null, "no_eligible_pixels", "too_few_samples", "zero_variance", "no_eligible_patches", "mask_empty_in_region" |
yes | ||
global.luma |
object | yes | Y’ Rec.709 on encoded sRGB, 0.2126 R’ + 0.7152 G’ + 0.0722 B’, in [0, 1]. Quantile p = the sorted value at index floor((N-1)p), no interpolation. | |
global.luma.p01 |
number or null | ≥ 0; ≤ 1 | ||
global.luma.p05 |
number or null | ≥ 0; ≤ 1 | ||
global.luma.p25 |
number or null | ≥ 0; ≤ 1 | ||
global.luma.p50 |
number or null | ≥ 0; ≤ 1 | ||
global.luma.p75 |
number or null | ≥ 0; ≤ 1 | ||
global.luma.p95 |
number or null | ≥ 0; ≤ 1 | ||
global.luma.p99 |
number or null | ≥ 0; ≤ 1 | ||
global.luma.mean |
number or null | ≥ 0; ≤ 1 | ||
global.luma.near_black_share |
number or null | ≥ 0; ≤ 1 | Share of sampled pixels with luma <= near_black. | |
global.luma.near_white_share |
number or null | ≥ 0; ≤ 1 | Share of sampled pixels with luma >= near_white. | |
global.luminance |
object | yes | Y Rec.709 on linearized sRGB (relative luminance), in [0, 1]. | |
global.luminance.mean |
number or null | ≥ 0; ≤ 1 | ||
global.luminance.p50 |
number or null | ≥ 0; ≤ 1 | ||
global.oklab |
object | yes | ||
global.oklab.mean_l |
number or null | |||
global.oklab.mean_a |
number or null | |||
global.oklab.mean_b |
number or null | |||
global.bands |
array of objects | yes | One entry per Oklab L band set by the bands parameter, in increasing order. | |
global.bands[].l_min |
number | yes | ||
global.bands[].l_max |
number | yes | Exclusive, except for the last band. | |
global.bands[].n |
integer | yes | ≥ 0 | |
global.bands[].share |
number or null | yes | ≥ 0; ≤ 1 | n divided by the sampled pixels of the region. |
global.bands[].chroma |
object | Oklab chroma quantiles of the band (same rule as luma). | ||
global.bands[].chroma.p01 |
number or null | |||
global.bands[].chroma.p05 |
number or null | |||
global.bands[].chroma.p25 |
number or null | |||
global.bands[].chroma.p50 |
number or null | |||
global.bands[].chroma.p75 |
number or null | |||
global.bands[].chroma.p95 |
number or null | |||
global.bands[].chroma.p99 |
number or null | |||
global.bands[].neutral_n |
integer | ≥ 0 | Pixels with chroma < 0.06. | |
global.bands[].neutral_a_p50 |
number or null | Median a of the neutral pixels; null when there are none. | ||
global.bands[].neutral_b_p50 |
number or null | |||
global.bands[].hue_bins |
array of objects | min items 12; max items 12 | The pixels with chroma >= 0.03 by hue atan2(b, a), bin k covering [30k, 30(k+1)) degrees. Neutral (chroma < 0.06) and hue samples overlap on purpose. | |
global.bands[].hue_bins[].start |
integer | yes | ≥ 0; ≤ 330 | |
global.bands[].hue_bins[].count |
integer | yes | ≥ 0 | |
global.bands[].hue_bins[].share |
number or null | yes | ≥ 0; ≤ 1 | count divided by the pixels of the band. |
global.bands[].hue_bins[].chroma_p50 |
number or null | yes | null when count is 0. | |
regions |
array of objects | yes | max items 256 | |
regions[].index |
integer | yes | ≥ 0 | 0 for the global region, then 1..n in reading order. |
regions[].id |
string | yes | global, r |
|
regions[].cell |
object | yes | Pixels of the oriented image, origin top left; covers columns x..x+width-1 and rows y..y+height-1. | |
regions[].cell.x |
integer | yes | ≥ 0 | |
regions[].cell.y |
integer | yes | ≥ 0 | |
regions[].cell.width |
integer | yes | ≥ 1 | |
regions[].cell.height |
integer | yes | ≥ 1 | |
regions[].mask_share |
number or null | ≥ 0; ≤ 1 | Share of the region inside the mask; present only when a mask is connected. | |
regions[].sampling |
object | yes | ||
regions[].sampling.method |
one of "grid", "full" |
yes | ||
regions[].sampling.step |
integer | yes | ≥ 1 | |
regions[].sampling.eligible_pixels |
integer | yes | ≥ 0 | Pixels of the region that may be measured: opaque enough and inside the mask. |
regions[].sampling.sampled_pixels |
integer | yes | ≥ 0 | |
regions[].status |
one of "ok", "not_observable" |
yes | not_observable: the measurement is undefined here (see reason); its numbers are null. Zero is a valid measurement, never a stand-in for null. | |
regions[].reason |
one of null, "no_eligible_pixels", "too_few_samples", "zero_variance", "no_eligible_patches", "mask_empty_in_region" |
yes | ||
regions[].luma |
object | yes | Y’ Rec.709 on encoded sRGB, 0.2126 R’ + 0.7152 G’ + 0.0722 B’, in [0, 1]. Quantile p = the sorted value at index floor((N-1)p), no interpolation. | |
regions[].luma.p01 |
number or null | ≥ 0; ≤ 1 | ||
regions[].luma.p05 |
number or null | ≥ 0; ≤ 1 | ||
regions[].luma.p25 |
number or null | ≥ 0; ≤ 1 | ||
regions[].luma.p50 |
number or null | ≥ 0; ≤ 1 | ||
regions[].luma.p75 |
number or null | ≥ 0; ≤ 1 | ||
regions[].luma.p95 |
number or null | ≥ 0; ≤ 1 | ||
regions[].luma.p99 |
number or null | ≥ 0; ≤ 1 | ||
regions[].luma.mean |
number or null | ≥ 0; ≤ 1 | ||
regions[].luma.near_black_share |
number or null | ≥ 0; ≤ 1 | Share of sampled pixels with luma <= near_black. | |
regions[].luma.near_white_share |
number or null | ≥ 0; ≤ 1 | Share of sampled pixels with luma >= near_white. | |
regions[].luminance |
object | yes | Y Rec.709 on linearized sRGB (relative luminance), in [0, 1]. | |
regions[].luminance.mean |
number or null | ≥ 0; ≤ 1 | ||
regions[].luminance.p50 |
number or null | ≥ 0; ≤ 1 | ||
regions[].oklab |
object | yes | ||
regions[].oklab.mean_l |
number or null | |||
regions[].oklab.mean_a |
number or null | |||
regions[].oklab.mean_b |
number or null | |||
regions[].bands |
array of objects | yes | One entry per Oklab L band set by the bands parameter, in increasing order. | |
regions[].bands[].l_min |
number | yes | ||
regions[].bands[].l_max |
number | yes | Exclusive, except for the last band. | |
regions[].bands[].n |
integer | yes | ≥ 0 | |
regions[].bands[].share |
number or null | yes | ≥ 0; ≤ 1 | n divided by the sampled pixels of the region. |
regions[].bands[].chroma |
object | Oklab chroma quantiles of the band (same rule as luma). | ||
regions[].bands[].chroma.p01 |
number or null | |||
regions[].bands[].chroma.p05 |
number or null | |||
regions[].bands[].chroma.p25 |
number or null | |||
regions[].bands[].chroma.p50 |
number or null | |||
regions[].bands[].chroma.p75 |
number or null | |||
regions[].bands[].chroma.p95 |
number or null | |||
regions[].bands[].chroma.p99 |
number or null | |||
regions[].bands[].neutral_n |
integer | ≥ 0 | Pixels with chroma < 0.06. | |
regions[].bands[].neutral_a_p50 |
number or null | Median a of the neutral pixels; null when there are none. | ||
regions[].bands[].neutral_b_p50 |
number or null | |||
regions[].bands[].hue_bins |
array of objects | min items 12; max items 12 | The pixels with chroma >= 0.03 by hue atan2(b, a), bin k covering [30k, 30(k+1)) degrees. Neutral (chroma < 0.06) and hue samples overlap on purpose. | |
regions[].bands[].hue_bins[].start |
integer | yes | ≥ 0; ≤ 330 | |
regions[].bands[].hue_bins[].count |
integer | yes | ≥ 0 | |
regions[].bands[].hue_bins[].share |
number or null | yes | ≥ 0; ≤ 1 | count divided by the pixels of the band. |
regions[].bands[].hue_bins[].chroma_p50 |
number or null | yes | null when count is 0. |
The complete schema
Section titled “The complete schema”madoo.image-statistics/v1 (JSON Schema)
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "urn:madoo:schema:image-statistics:v1", "title": "Madoo image statistics", "description": "What Image Statistics (image/statistics) measured: tone and colour of the whole image and of each region (grid cell, given rectangle), optionally only inside a mask. Deterministic for the same image, algorithm_version and settings.", "type": "object", "required": [ "schema", "algorithm_version", "image", "color", "warnings", "settings", "global", "regions" ], "properties": { "schema": { "const": "madoo.image-statistics/v1" }, "algorithm_version": { "type": "string" }, "image": { "$ref": "#/$defs/image" }, "color": { "$ref": "#/$defs/color" }, "warnings": { "$ref": "#/$defs/warnings" }, "settings": { "type": "object", "required": [ "alpha_min", "max_samples", "bands", "near_black", "near_white", "mask" ], "properties": { "grid": { "type": [ "object", "null" ], "properties": { "rows": { "type": "integer" }, "cols": { "type": "integer" } } }, "alpha_min": { "type": "integer", "minimum": 0, "maximum": 255 }, "mask": { "type": "boolean" }, "mask_threshold": { "type": [ "integer", "null" ] }, "max_samples": { "type": "integer", "minimum": 1 }, "bands": { "type": "array", "items": { "type": "number" }, "minItems": 2 }, "near_black": { "type": "number" }, "near_white": { "type": "number" } } }, "global": { "type": "object", "required": [ "index", "id", "cell", "sampling", "status", "reason", "luma", "luminance", "oklab", "bands" ], "properties": { "index": { "type": "integer", "minimum": 0, "description": "0 for the global region, then 1..n in reading order." }, "id": { "type": "string", "description": "global, r<row>c<col> for a grid cell (1-based), or the id given with the region." }, "cell": { "$ref": "#/$defs/rect" }, "mask_share": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1, "description": "Share of the region inside the mask; present only when a mask is connected." }, "sampling": { "$ref": "#/$defs/sampling" }, "status": { "$ref": "#/$defs/status" }, "reason": { "$ref": "#/$defs/reason" }, "luma": { "type": "object", "description": "Y' Rec.709 on encoded sRGB, 0.2126 R' + 0.7152 G' + 0.0722 B', in [0, 1]. Quantile p = the sorted value at index floor((N-1)p), no interpolation.", "properties": { "p01": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1 }, "p05": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1 }, "p25": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1 }, "p50": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1 }, "p75": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1 }, "p95": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1 }, "p99": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1 }, "mean": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1 }, "near_black_share": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1, "description": "Share of sampled pixels with luma <= near_black." }, "near_white_share": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1, "description": "Share of sampled pixels with luma >= near_white." } } }, "luminance": { "type": "object", "description": "Y Rec.709 on linearized sRGB (relative luminance), in [0, 1].", "properties": { "mean": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1 }, "p50": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1 } } }, "oklab": { "type": "object", "properties": { "mean_l": { "type": [ "number", "null" ] }, "mean_a": { "type": [ "number", "null" ] }, "mean_b": { "type": [ "number", "null" ] } } }, "bands": { "type": "array", "description": "One entry per Oklab L band set by the bands parameter, in increasing order.", "items": { "type": "object", "required": [ "l_min", "l_max", "n", "share" ], "properties": { "l_min": { "type": "number" }, "l_max": { "type": "number", "description": "Exclusive, except for the last band." }, "n": { "type": "integer", "minimum": 0 }, "share": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1, "description": "n divided by the sampled pixels of the region." }, "chroma": { "type": "object", "description": "Oklab chroma quantiles of the band (same rule as luma).", "properties": { "p01": { "type": [ "number", "null" ] }, "p05": { "type": [ "number", "null" ] }, "p25": { "type": [ "number", "null" ] }, "p50": { "type": [ "number", "null" ] }, "p75": { "type": [ "number", "null" ] }, "p95": { "type": [ "number", "null" ] }, "p99": { "type": [ "number", "null" ] } } }, "neutral_n": { "type": "integer", "minimum": 0, "description": "Pixels with chroma < 0.06." }, "neutral_a_p50": { "type": [ "number", "null" ], "description": "Median a of the neutral pixels; null when there are none." }, "neutral_b_p50": { "type": [ "number", "null" ] }, "hue_bins": { "type": "array", "minItems": 12, "maxItems": 12, "description": "The pixels with chroma >= 0.03 by hue atan2(b, a), bin k covering [30k, 30(k+1)) degrees. Neutral (chroma < 0.06) and hue samples overlap on purpose.", "items": { "type": "object", "required": [ "start", "count", "share", "chroma_p50" ], "properties": { "start": { "type": "integer", "minimum": 0, "maximum": 330 }, "count": { "type": "integer", "minimum": 0 }, "share": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1, "description": "count divided by the pixels of the band." }, "chroma_p50": { "type": [ "number", "null" ], "description": "null when count is 0." } } } } } } } } }, "regions": { "type": "array", "items": { "type": "object", "required": [ "index", "id", "cell", "sampling", "status", "reason", "luma", "luminance", "oklab", "bands" ], "properties": { "index": { "type": "integer", "minimum": 0, "description": "0 for the global region, then 1..n in reading order." }, "id": { "type": "string", "description": "global, r<row>c<col> for a grid cell (1-based), or the id given with the region." }, "cell": { "$ref": "#/$defs/rect" }, "mask_share": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1, "description": "Share of the region inside the mask; present only when a mask is connected." }, "sampling": { "$ref": "#/$defs/sampling" }, "status": { "$ref": "#/$defs/status" }, "reason": { "$ref": "#/$defs/reason" }, "luma": { "type": "object", "description": "Y' Rec.709 on encoded sRGB, 0.2126 R' + 0.7152 G' + 0.0722 B', in [0, 1]. Quantile p = the sorted value at index floor((N-1)p), no interpolation.", "properties": { "p01": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1 }, "p05": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1 }, "p25": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1 }, "p50": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1 }, "p75": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1 }, "p95": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1 }, "p99": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1 }, "mean": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1 }, "near_black_share": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1, "description": "Share of sampled pixels with luma <= near_black." }, "near_white_share": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1, "description": "Share of sampled pixels with luma >= near_white." } } }, "luminance": { "type": "object", "description": "Y Rec.709 on linearized sRGB (relative luminance), in [0, 1].", "properties": { "mean": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1 }, "p50": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1 } } }, "oklab": { "type": "object", "properties": { "mean_l": { "type": [ "number", "null" ] }, "mean_a": { "type": [ "number", "null" ] }, "mean_b": { "type": [ "number", "null" ] } } }, "bands": { "type": "array", "description": "One entry per Oklab L band set by the bands parameter, in increasing order.", "items": { "type": "object", "required": [ "l_min", "l_max", "n", "share" ], "properties": { "l_min": { "type": "number" }, "l_max": { "type": "number", "description": "Exclusive, except for the last band." }, "n": { "type": "integer", "minimum": 0 }, "share": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1, "description": "n divided by the sampled pixels of the region." }, "chroma": { "type": "object", "description": "Oklab chroma quantiles of the band (same rule as luma).", "properties": { "p01": { "type": [ "number", "null" ] }, "p05": { "type": [ "number", "null" ] }, "p25": { "type": [ "number", "null" ] }, "p50": { "type": [ "number", "null" ] }, "p75": { "type": [ "number", "null" ] }, "p95": { "type": [ "number", "null" ] }, "p99": { "type": [ "number", "null" ] } } }, "neutral_n": { "type": "integer", "minimum": 0, "description": "Pixels with chroma < 0.06." }, "neutral_a_p50": { "type": [ "number", "null" ], "description": "Median a of the neutral pixels; null when there are none." }, "neutral_b_p50": { "type": [ "number", "null" ] }, "hue_bins": { "type": "array", "minItems": 12, "maxItems": 12, "description": "The pixels with chroma >= 0.03 by hue atan2(b, a), bin k covering [30k, 30(k+1)) degrees. Neutral (chroma < 0.06) and hue samples overlap on purpose.", "items": { "type": "object", "required": [ "start", "count", "share", "chroma_p50" ], "properties": { "start": { "type": "integer", "minimum": 0, "maximum": 330 }, "count": { "type": "integer", "minimum": 0 }, "share": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1, "description": "count divided by the pixels of the band." }, "chroma_p50": { "type": [ "number", "null" ], "description": "null when count is 0." } } } } } } } } }, "maxItems": 256 } }, "$defs": { "rect": { "type": "object", "required": [ "x", "y", "width", "height" ], "properties": { "x": { "type": "integer", "minimum": 0 }, "y": { "type": "integer", "minimum": 0 }, "width": { "type": "integer", "minimum": 1 }, "height": { "type": "integer", "minimum": 1 } }, "description": "Pixels of the oriented image, origin top left; covers columns x..x+width-1 and rows y..y+height-1." }, "image": { "type": "object", "required": [ "width", "height", "orientation_applied" ], "properties": { "width": { "type": "integer", "minimum": 1, "description": "After EXIF orientation." }, "height": { "type": "integer", "minimum": 1 }, "orientation_applied": { "type": "boolean", "description": "True when the EXIF orientation rotated or mirrored the image." } } }, "color": { "type": "object", "required": [ "working_space", "profile_policy", "source_depth", "analysis_depth" ], "properties": { "working_space": { "const": "srgb-8" }, "profile_policy": { "enum": [ "srgb", "converted", "assumed_srgb" ], "description": "srgb: the file is sRGB; converted: its ICC profile was converted to sRGB; assumed_srgb: an RGB image without a profile, read as sRGB." }, "source_depth": { "type": [ "integer", "null" ], "description": "Bits per channel of the file." }, "analysis_depth": { "const": 8 } } }, "sampling": { "type": "object", "required": [ "method", "step", "eligible_pixels", "sampled_pixels" ], "properties": { "method": { "enum": [ "grid", "full" ] }, "step": { "type": "integer", "minimum": 1 }, "eligible_pixels": { "type": "integer", "minimum": 0, "description": "Pixels of the region that may be measured: opaque enough and inside the mask." }, "sampled_pixels": { "type": "integer", "minimum": 0 } } }, "warnings": { "type": "array", "items": { "type": "string" }, "description": "Facts that limit how the numbers may be read: a JPEG source (compression can contaminate detail measurements), several frames or pages (the first was measured), a source deeper than 8 bits (measured at 8 bits)." }, "status": { "enum": [ "ok", "not_observable" ], "description": "not_observable: the measurement is undefined here (see reason); its numbers are null. Zero is a valid measurement, never a stand-in for null." }, "reason": { "type": [ "string", "null" ], "enum": [ null, "no_eligible_pixels", "too_few_samples", "zero_variance", "no_eligible_patches", "mask_empty_in_region" ] } }}