Skip to content

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
  • Produced by: image/statistics → statistics
for a grid cell (1-based), or the id given with the region.for a grid cell (1-based), or the id given with the region.
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, rc
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, rc
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.
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"
]
}
}
}