/* * This file is part of libplacebo. * * libplacebo is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * libplacebo is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with libplacebo. If not, see . */ #include #include "common.h" #include "hash.h" #include #include bool pl_color_system_is_ycbcr_like(enum pl_color_system sys) { switch (sys) { case PL_COLOR_SYSTEM_UNKNOWN: case PL_COLOR_SYSTEM_RGB: case PL_COLOR_SYSTEM_XYZ: return false; case PL_COLOR_SYSTEM_BT_601: case PL_COLOR_SYSTEM_BT_709: case PL_COLOR_SYSTEM_SMPTE_240M: case PL_COLOR_SYSTEM_BT_2020_NC: case PL_COLOR_SYSTEM_BT_2020_C: case PL_COLOR_SYSTEM_BT_2100_PQ: case PL_COLOR_SYSTEM_BT_2100_HLG: case PL_COLOR_SYSTEM_DOLBYVISION: case PL_COLOR_SYSTEM_YCGCO: return true; case PL_COLOR_SYSTEM_COUNT: break; }; pl_unreachable(); } bool pl_color_system_is_linear(enum pl_color_system sys) { switch (sys) { case PL_COLOR_SYSTEM_UNKNOWN: case PL_COLOR_SYSTEM_RGB: case PL_COLOR_SYSTEM_BT_601: case PL_COLOR_SYSTEM_BT_709: case PL_COLOR_SYSTEM_SMPTE_240M: case PL_COLOR_SYSTEM_BT_2020_NC: case PL_COLOR_SYSTEM_YCGCO: return true; case PL_COLOR_SYSTEM_BT_2020_C: case PL_COLOR_SYSTEM_BT_2100_PQ: case PL_COLOR_SYSTEM_BT_2100_HLG: case PL_COLOR_SYSTEM_DOLBYVISION: case PL_COLOR_SYSTEM_XYZ: return false; case PL_COLOR_SYSTEM_COUNT: break; }; pl_unreachable(); } enum pl_color_system pl_color_system_guess_ycbcr(int width, int height) { if (width >= 1280 || height > 576) { // Typical HD content return PL_COLOR_SYSTEM_BT_709; } else { // Typical SD content return PL_COLOR_SYSTEM_BT_601; } } bool pl_bit_encoding_equal(const struct pl_bit_encoding *b1, const struct pl_bit_encoding *b2) { return b1->sample_depth == b2->sample_depth && b1->color_depth == b2->color_depth && b1->bit_shift == b2->bit_shift; } const struct pl_color_repr pl_color_repr_unknown = {0}; const struct pl_color_repr pl_color_repr_rgb = { .sys = PL_COLOR_SYSTEM_RGB, .levels = PL_COLOR_LEVELS_FULL, }; const struct pl_color_repr pl_color_repr_sdtv = { .sys = PL_COLOR_SYSTEM_BT_601, .levels = PL_COLOR_LEVELS_LIMITED, }; const struct pl_color_repr pl_color_repr_hdtv = { .sys = PL_COLOR_SYSTEM_BT_709, .levels = PL_COLOR_LEVELS_LIMITED, }; const struct pl_color_repr pl_color_repr_uhdtv = { .sys = PL_COLOR_SYSTEM_BT_2020_NC, .levels = PL_COLOR_LEVELS_LIMITED, }; const struct pl_color_repr pl_color_repr_jpeg = { .sys = PL_COLOR_SYSTEM_BT_601, .levels = PL_COLOR_LEVELS_FULL, }; bool pl_color_repr_equal(const struct pl_color_repr *c1, const struct pl_color_repr *c2) { return c1->sys == c2->sys && c1->levels == c2->levels && c1->alpha == c2->alpha && c1->dovi == c2->dovi && pl_bit_encoding_equal(&c1->bits, &c2->bits); } static struct pl_bit_encoding pl_bit_encoding_merge(const struct pl_bit_encoding *orig, const struct pl_bit_encoding *new) { return (struct pl_bit_encoding) { .sample_depth = PL_DEF(orig->sample_depth, new->sample_depth), .color_depth = PL_DEF(orig->color_depth, new->color_depth), .bit_shift = PL_DEF(orig->bit_shift, new->bit_shift), }; } void pl_color_repr_merge(struct pl_color_repr *orig, const struct pl_color_repr *new) { *orig = (struct pl_color_repr) { .sys = PL_DEF(orig->sys, new->sys), .levels = PL_DEF(orig->levels, new->levels), .alpha = PL_DEF(orig->alpha, new->alpha), .dovi = PL_DEF(orig->dovi, new->dovi), .bits = pl_bit_encoding_merge(&orig->bits, &new->bits), }; } enum pl_color_levels pl_color_levels_guess(const struct pl_color_repr *repr) { if (repr->sys == PL_COLOR_SYSTEM_DOLBYVISION) return PL_COLOR_LEVELS_FULL; if (repr->levels) return repr->levels; return pl_color_system_is_ycbcr_like(repr->sys) ? PL_COLOR_LEVELS_LIMITED : PL_COLOR_LEVELS_FULL; } float pl_color_repr_normalize(struct pl_color_repr *repr) { float scale = 1.0; struct pl_bit_encoding *bits = &repr->bits; if (bits->bit_shift) { scale /= (1LL << bits->bit_shift); bits->bit_shift = 0; } // If one of these is set but not the other, use the set one int tex_bits = PL_DEF(bits->sample_depth, 8); int col_bits = PL_DEF(bits->color_depth, tex_bits); tex_bits = PL_DEF(tex_bits, col_bits); if (pl_color_levels_guess(repr) == PL_COLOR_LEVELS_LIMITED) { // Limit range is always shifted directly scale *= (float) (1LL << tex_bits) / (1LL << col_bits); } else { // Full range always uses the full range available scale *= ((1LL << tex_bits) - 1.) / ((1LL << col_bits) - 1.); } bits->color_depth = bits->sample_depth; return scale; } bool pl_color_primaries_is_wide_gamut(enum pl_color_primaries prim) { switch (prim) { case PL_COLOR_PRIM_UNKNOWN: case PL_COLOR_PRIM_BT_601_525: case PL_COLOR_PRIM_BT_601_625: case PL_COLOR_PRIM_BT_709: case PL_COLOR_PRIM_BT_470M: case PL_COLOR_PRIM_EBU_3213: return false; case PL_COLOR_PRIM_BT_2020: case PL_COLOR_PRIM_APPLE: case PL_COLOR_PRIM_ADOBE: case PL_COLOR_PRIM_PRO_PHOTO: case PL_COLOR_PRIM_CIE_1931: case PL_COLOR_PRIM_DCI_P3: case PL_COLOR_PRIM_DISPLAY_P3: case PL_COLOR_PRIM_V_GAMUT: case PL_COLOR_PRIM_S_GAMUT: case PL_COLOR_PRIM_FILM_C: case PL_COLOR_PRIM_ACES_AP0: case PL_COLOR_PRIM_ACES_AP1: return true; case PL_COLOR_PRIM_COUNT: break; } pl_unreachable(); } enum pl_color_primaries pl_color_primaries_guess(int width, int height) { // HD content if (width >= 1280 || height > 576) return PL_COLOR_PRIM_BT_709; switch (height) { case 576: // Typical PAL content, including anamorphic/squared return PL_COLOR_PRIM_BT_601_625; case 480: // Typical NTSC content, including squared case 486: // NTSC Pro or anamorphic NTSC return PL_COLOR_PRIM_BT_601_525; default: // No good metric, just pick BT.709 to minimize damage return PL_COLOR_PRIM_BT_709; } } // HLG 75% value (scene-referred) #define HLG_75 3.17955 float pl_color_transfer_nominal_peak(enum pl_color_transfer trc) { switch (trc) { case PL_COLOR_TRC_UNKNOWN: case PL_COLOR_TRC_BT_1886: case PL_COLOR_TRC_SRGB: case PL_COLOR_TRC_LINEAR: case PL_COLOR_TRC_GAMMA18: case PL_COLOR_TRC_GAMMA20: case PL_COLOR_TRC_GAMMA22: case PL_COLOR_TRC_GAMMA24: case PL_COLOR_TRC_GAMMA26: case PL_COLOR_TRC_GAMMA28: case PL_COLOR_TRC_PRO_PHOTO: case PL_COLOR_TRC_ST428: return 1.0; case PL_COLOR_TRC_PQ: return 10000.0 / PL_COLOR_SDR_WHITE; case PL_COLOR_TRC_HLG: return 12.0 / HLG_75; case PL_COLOR_TRC_V_LOG: return 46.0855; case PL_COLOR_TRC_S_LOG1: return 6.52; case PL_COLOR_TRC_S_LOG2: return 9.212; case PL_COLOR_TRC_COUNT: break; } pl_unreachable(); } const struct pl_hdr_metadata pl_hdr_metadata_empty = {0}; const struct pl_hdr_metadata pl_hdr_metadata_hdr10 ={ .prim = { .red = {0.708, 0.292}, .green = {0.170, 0.797}, .blue = {0.131, 0.046}, .white = {0.31271, 0.32902}, }, .min_luma = 0, .max_luma = 10000, .max_cll = 10000, .max_fall = 0, // unknown }; static const float PQ_M1 = 2610./4096 * 1./4, PQ_M2 = 2523./4096 * 128, PQ_C1 = 3424./4096, PQ_C2 = 2413./4096 * 32, PQ_C3 = 2392./4096 * 32; float pl_hdr_rescale(enum pl_hdr_scaling from, enum pl_hdr_scaling to, float x) { if (from == to) return x; if (!x) // micro-optimization for common value return x; x = fmaxf(x, 0.0f); // Convert input to PL_SCALE_RELATIVE switch (from) { case PL_HDR_PQ: x = powf(x, 1.0f / PQ_M2); x = fmaxf(x - PQ_C1, 0.0f) / (PQ_C2 - PQ_C3 * x); x = powf(x, 1.0f / PQ_M1); x *= 10000.0f; // fall through case PL_HDR_NITS: x /= PL_COLOR_SDR_WHITE; // fall through case PL_HDR_NORM: goto output; case PL_HDR_SQRT: x *= x; goto output; case PL_HDR_SCALING_COUNT: break; } pl_unreachable(); output: // Convert PL_SCALE_RELATIVE to output switch (to) { case PL_HDR_NORM: return x; case PL_HDR_SQRT: return sqrtf(x); case PL_HDR_NITS: return x * PL_COLOR_SDR_WHITE; case PL_HDR_PQ: x *= PL_COLOR_SDR_WHITE / 10000.0f; x = powf(x, PQ_M1); x = (PQ_C1 + PQ_C2 * x) / (1.0f + PQ_C3 * x); x = powf(x, PQ_M2); return x; case PL_HDR_SCALING_COUNT: break; } pl_unreachable(); } static inline bool pl_hdr_bezier_equal(const struct pl_hdr_bezier *a, const struct pl_hdr_bezier *b) { return a->target_luma == b->target_luma && a->knee_x == b->knee_x && a->knee_y == b->knee_y && a->num_anchors == b->num_anchors && !memcmp(a->anchors, b->anchors, sizeof(a->anchors[0]) * a->num_anchors); } bool pl_hdr_metadata_equal(const struct pl_hdr_metadata *a, const struct pl_hdr_metadata *b) { return pl_raw_primaries_equal(&a->prim, &b->prim) && a->min_luma == b->min_luma && a->max_luma == b->max_luma && a->max_cll == b->max_cll && a->max_fall == b->max_fall && a->scene_max[0] == b->scene_max[0] && a->scene_max[1] == b->scene_max[1] && a->scene_max[2] == b->scene_max[2] && a->scene_avg == b->scene_avg && pl_hdr_bezier_equal(&a->ootf, &b->ootf) && a->max_pq_y == b->max_pq_y && a->avg_pq_y == b->avg_pq_y; } void pl_hdr_metadata_merge(struct pl_hdr_metadata *orig, const struct pl_hdr_metadata *update) { pl_raw_primaries_merge(&orig->prim, &update->prim); if (!orig->min_luma) orig->min_luma = update->min_luma; if (!orig->max_luma) orig->max_luma = update->max_luma; if (!orig->max_cll) orig->max_cll = update->max_cll; if (!orig->max_fall) orig->max_fall = update->max_fall; if (!orig->scene_max[1]) memcpy(orig->scene_max, update->scene_max, sizeof(orig->scene_max)); if (!orig->scene_avg) orig->scene_avg = update->scene_avg; if (!orig->ootf.target_luma) orig->ootf = update->ootf; if (!orig->max_pq_y) orig->max_pq_y = update->max_pq_y; if (!orig->avg_pq_y) orig->avg_pq_y = update->avg_pq_y; } bool pl_hdr_metadata_contains(const struct pl_hdr_metadata *data, enum pl_hdr_metadata_type type) { bool has_hdr10 = data->max_luma; bool has_hdr10plus = data->scene_avg && (data->scene_max[0] || data->scene_max[1] || data->scene_max[2]); bool has_cie_y = data->max_pq_y && data->avg_pq_y; switch (type) { case PL_HDR_METADATA_NONE: return true; case PL_HDR_METADATA_ANY: return has_hdr10 || has_hdr10plus || has_cie_y; case PL_HDR_METADATA_HDR10: return has_hdr10; case PL_HDR_METADATA_HDR10PLUS: return has_hdr10plus; case PL_HDR_METADATA_CIE_Y: return has_cie_y; case PL_HDR_METADATA_TYPE_COUNT: break; } pl_unreachable(); } const struct pl_color_space pl_color_space_unknown = {0}; const struct pl_color_space pl_color_space_srgb = { .primaries = PL_COLOR_PRIM_BT_709, .transfer = PL_COLOR_TRC_SRGB, }; const struct pl_color_space pl_color_space_bt709 = { .primaries = PL_COLOR_PRIM_BT_709, .transfer = PL_COLOR_TRC_BT_1886, }; const struct pl_color_space pl_color_space_hdr10 = { .primaries = PL_COLOR_PRIM_BT_2020, .transfer = PL_COLOR_TRC_PQ, }; const struct pl_color_space pl_color_space_bt2020_hlg = { .primaries = PL_COLOR_PRIM_BT_2020, .transfer = PL_COLOR_TRC_HLG, }; const struct pl_color_space pl_color_space_monitor = { .primaries = PL_COLOR_PRIM_BT_709, // sRGB primaries .transfer = PL_COLOR_TRC_UNKNOWN, // unknown SDR response }; bool pl_color_space_is_hdr(const struct pl_color_space *csp) { return csp->hdr.max_luma > PL_COLOR_SDR_WHITE || pl_color_transfer_is_hdr(csp->transfer); } bool pl_color_space_is_black_scaled(const struct pl_color_space *csp) { switch (csp->transfer) { case PL_COLOR_TRC_UNKNOWN: case PL_COLOR_TRC_SRGB: case PL_COLOR_TRC_LINEAR: case PL_COLOR_TRC_GAMMA18: case PL_COLOR_TRC_GAMMA20: case PL_COLOR_TRC_GAMMA22: case PL_COLOR_TRC_GAMMA24: case PL_COLOR_TRC_GAMMA26: case PL_COLOR_TRC_GAMMA28: case PL_COLOR_TRC_PRO_PHOTO: case PL_COLOR_TRC_ST428: case PL_COLOR_TRC_HLG: return true; case PL_COLOR_TRC_BT_1886: case PL_COLOR_TRC_PQ: case PL_COLOR_TRC_V_LOG: case PL_COLOR_TRC_S_LOG1: case PL_COLOR_TRC_S_LOG2: return false; case PL_COLOR_TRC_COUNT: break; } pl_unreachable(); } void pl_color_space_merge(struct pl_color_space *orig, const struct pl_color_space *new) { if (!orig->primaries) orig->primaries = new->primaries; if (!orig->transfer) orig->transfer = new->transfer; pl_hdr_metadata_merge(&orig->hdr, &new->hdr); } bool pl_color_space_equal(const struct pl_color_space *c1, const struct pl_color_space *c2) { return c1->primaries == c2->primaries && c1->transfer == c2->transfer && pl_hdr_metadata_equal(&c1->hdr, &c2->hdr); } // Estimates luminance from maxRGB by looking at how monochromatic MaxSCL is static void luma_from_maxrgb(const struct pl_color_space *csp, enum pl_hdr_scaling scaling, float *out_max, float *out_avg) { const float maxscl = PL_MAX3(csp->hdr.scene_max[0], csp->hdr.scene_max[1], csp->hdr.scene_max[2]); if (!maxscl) return; struct pl_raw_primaries prim = csp->hdr.prim; pl_raw_primaries_merge(&prim, pl_raw_primaries_get(csp->primaries)); const pl_matrix3x3 rgb2xyz = pl_get_rgb2xyz_matrix(&prim); const float max_luma = rgb2xyz.m[1][0] * csp->hdr.scene_max[0] + rgb2xyz.m[1][1] * csp->hdr.scene_max[1] + rgb2xyz.m[1][2] * csp->hdr.scene_max[2]; const float coef = max_luma / maxscl; *out_max = pl_hdr_rescale(PL_HDR_NITS, scaling, max_luma); *out_avg = pl_hdr_rescale(PL_HDR_NITS, scaling, coef * csp->hdr.scene_avg); } static inline bool metadata_compat(enum pl_hdr_metadata_type metadata, enum pl_hdr_metadata_type compat) { return metadata == PL_HDR_METADATA_ANY || metadata == compat; } void pl_color_space_nominal_luma_ex(const struct pl_nominal_luma_params *params) { if (!params || (!params->out_min && !params->out_max && !params->out_avg)) return; const struct pl_color_space *csp = params->color; const enum pl_hdr_scaling scaling = params->scaling; float min_luma = 0, max_luma = 0, avg_luma = 0; if (params->metadata != PL_HDR_METADATA_NONE) { // Initialize from static HDR10 metadata, in all cases min_luma = pl_hdr_rescale(PL_HDR_NITS, scaling, csp->hdr.min_luma); max_luma = pl_hdr_rescale(PL_HDR_NITS, scaling, csp->hdr.max_luma); } if (metadata_compat(params->metadata, PL_HDR_METADATA_HDR10PLUS) && pl_hdr_metadata_contains(&csp->hdr, PL_HDR_METADATA_HDR10PLUS)) { luma_from_maxrgb(csp, scaling, &max_luma, &avg_luma); } if (metadata_compat(params->metadata, PL_HDR_METADATA_CIE_Y) && pl_hdr_metadata_contains(&csp->hdr, PL_HDR_METADATA_CIE_Y)) { max_luma = pl_hdr_rescale(PL_HDR_PQ, scaling, csp->hdr.max_pq_y); avg_luma = pl_hdr_rescale(PL_HDR_PQ, scaling, csp->hdr.avg_pq_y); } // Clamp to sane value range const float hdr_min = pl_hdr_rescale(PL_HDR_NITS, scaling, PL_COLOR_HDR_BLACK); const float hdr_max = pl_hdr_rescale(PL_HDR_PQ, scaling, 1.0f); max_luma = max_luma ? PL_CLAMP(max_luma, hdr_min, hdr_max) : 0; min_luma = min_luma ? PL_CLAMP(min_luma, hdr_min, hdr_max) : 0; if ((max_luma && min_luma >= max_luma) || min_luma >= hdr_max) min_luma = max_luma = 0; // sanity // PQ is always scaled down to absolute black, ignoring HDR metadata if (csp->transfer == PL_COLOR_TRC_PQ) min_luma = hdr_min; // Baseline/fallback metadata, inferred entirely from the colorspace // description and built-in default assumptions if (!max_luma) { if (csp->transfer == PL_COLOR_TRC_HLG) { max_luma = pl_hdr_rescale(PL_HDR_NITS, scaling, PL_COLOR_HLG_PEAK); } else { const float peak = pl_color_transfer_nominal_peak(csp->transfer); max_luma = pl_hdr_rescale(PL_HDR_NORM, scaling, peak); } } if (!min_luma) { if (pl_color_transfer_is_hdr(csp->transfer)) { min_luma = hdr_min; } else { const float peak = pl_hdr_rescale(scaling, PL_HDR_NITS, max_luma); min_luma = pl_hdr_rescale(PL_HDR_NITS, scaling, peak / PL_COLOR_SDR_CONTRAST); } } if (avg_luma) avg_luma = PL_CLAMP(avg_luma, min_luma, max_luma); // sanity if (params->out_min) *params->out_min = min_luma; if (params->out_max) *params->out_max = max_luma; if (params->out_avg) *params->out_avg = avg_luma; } void pl_color_space_nominal_luma(const struct pl_color_space *csp, float *out_min, float *out_max) { pl_color_space_nominal_luma_ex(pl_nominal_luma_params( .color = csp, .metadata = PL_HDR_METADATA_ANY, .scaling = PL_HDR_NORM, .out_min = out_min, .out_max = out_max, )); } void pl_color_space_infer(struct pl_color_space *space) { if (!space->primaries) space->primaries = PL_COLOR_PRIM_BT_709; if (!space->transfer) space->transfer = PL_COLOR_TRC_BT_1886; // Sanitize the static HDR metadata pl_color_space_nominal_luma_ex(pl_nominal_luma_params( .color = space, .metadata = PL_HDR_METADATA_HDR10, .scaling = PL_HDR_NITS, .out_max = &space->hdr.max_luma, // Preserve tagged minimum .out_min = space->hdr.min_luma ? NULL : &space->hdr.min_luma, )); // Default the signal color space based on the nominal raw primaries if (!pl_primaries_valid(&space->hdr.prim)) space->hdr.prim = *pl_raw_primaries_get(space->primaries); } static void infer_both_ref(struct pl_color_space *space, struct pl_color_space *ref) { pl_color_space_infer(ref); if (!space->primaries) { if (pl_color_primaries_is_wide_gamut(ref->primaries)) { space->primaries = PL_COLOR_PRIM_BT_709; } else { space->primaries = ref->primaries; } } if (!space->transfer) { switch (ref->transfer) { case PL_COLOR_TRC_UNKNOWN: case PL_COLOR_TRC_COUNT: pl_unreachable(); case PL_COLOR_TRC_BT_1886: case PL_COLOR_TRC_SRGB: case PL_COLOR_TRC_GAMMA22: // Re-use input transfer curve to avoid small adaptations space->transfer = ref->transfer; break; case PL_COLOR_TRC_PQ: case PL_COLOR_TRC_HLG: case PL_COLOR_TRC_V_LOG: case PL_COLOR_TRC_S_LOG1: case PL_COLOR_TRC_S_LOG2: // Pick BT.1886 model because it models SDR contrast accurately, // and we need contrast information for tone mapping space->transfer = PL_COLOR_TRC_BT_1886; break; case PL_COLOR_TRC_PRO_PHOTO: // ProPhotoRGB and sRGB are both piecewise with linear slope space->transfer = PL_COLOR_TRC_SRGB; break; case PL_COLOR_TRC_LINEAR: case PL_COLOR_TRC_GAMMA18: case PL_COLOR_TRC_GAMMA20: case PL_COLOR_TRC_GAMMA24: case PL_COLOR_TRC_GAMMA26: case PL_COLOR_TRC_GAMMA28: case PL_COLOR_TRC_ST428: // Pick pure power output curve to avoid introducing black crush space->transfer = PL_COLOR_TRC_GAMMA22; break; } } // Infer the remaining fields after making the above choices pl_color_space_infer(space); } void pl_color_space_infer_ref(struct pl_color_space *space, const struct pl_color_space *refp) { // Make a copy of `refp` to infer missing values first struct pl_color_space ref = *refp; infer_both_ref(space, &ref); } void pl_color_space_infer_map(struct pl_color_space *src, struct pl_color_space *dst) { bool unknown_src_contrast = !src->hdr.min_luma; bool unknown_dst_contrast = !dst->hdr.min_luma; infer_both_ref(dst, src); // If the src has an unspecified gamma curve with dynamic black scaling, // default it to match the dst colorspace contrast. This does not matter in // most cases, but ensures that BT.1886 is tuned to the appropriate black // point by default. bool dynamic_src_contrast = pl_color_space_is_black_scaled(src) || src->transfer == PL_COLOR_TRC_BT_1886; if (unknown_src_contrast && dynamic_src_contrast) src->hdr.min_luma = dst->hdr.min_luma; // Do the same in reverse if both src and dst are SDR curves bool src_is_sdr = !pl_color_space_is_hdr(src); bool dst_is_sdr = !pl_color_space_is_hdr(dst); if (unknown_dst_contrast && src_is_sdr && dst_is_sdr) dst->hdr.min_luma = src->hdr.min_luma; // If the src is HLG and the output is HDR, tune the HLG peak to the output if (src->transfer == PL_COLOR_TRC_HLG && pl_color_space_is_hdr(dst)) src->hdr.max_luma = dst->hdr.max_luma; } const struct pl_color_adjustment pl_color_adjustment_neutral = { PL_COLOR_ADJUSTMENT_NEUTRAL }; void pl_chroma_location_offset(enum pl_chroma_location loc, float *x, float *y) { *x = *y = 0; // This is the majority of subsampled chroma content out there loc = PL_DEF(loc, PL_CHROMA_LEFT); switch (loc) { case PL_CHROMA_LEFT: case PL_CHROMA_TOP_LEFT: case PL_CHROMA_BOTTOM_LEFT: *x = -0.5; break; default: break; } switch (loc) { case PL_CHROMA_TOP_LEFT: case PL_CHROMA_TOP_CENTER: *y = -0.5; break; default: break; } switch (loc) { case PL_CHROMA_BOTTOM_LEFT: case PL_CHROMA_BOTTOM_CENTER: *y = 0.5; break; default: break; } } struct pl_cie_xy pl_white_from_temp(float temp) { temp = PL_CLAMP(temp, 2500, 25000); double ti = 1000.0 / temp, ti2 = ti * ti, ti3 = ti2 * ti, x; if (temp <= 7000) { x = -4.6070 * ti3 + 2.9678 * ti2 + 0.09911 * ti + 0.244063; } else { x = -2.0064 * ti3 + 1.9018 * ti2 + 0.24748 * ti + 0.237040; } return (struct pl_cie_xy) { .x = x, .y = -3 * (x*x) + 2.87 * x - 0.275, }; } bool pl_raw_primaries_equal(const struct pl_raw_primaries *a, const struct pl_raw_primaries *b) { return pl_cie_xy_equal(&a->red, &b->red) && pl_cie_xy_equal(&a->green, &b->green) && pl_cie_xy_equal(&a->blue, &b->blue) && pl_cie_xy_equal(&a->white, &b->white); } bool pl_raw_primaries_similar(const struct pl_raw_primaries *a, const struct pl_raw_primaries *b) { float delta = fabsf(a->red.x - b->red.x) + fabsf(a->red.y - b->red.y) + fabsf(a->green.x - b->green.x) + fabsf(a->green.y - b->green.y) + fabsf(a->blue.x - b->blue.x) + fabsf(a->blue.y - b->blue.y) + fabsf(a->white.x - b->white.x) + fabsf(a->white.y - b->white.y); return delta < 0.001; } void pl_raw_primaries_merge(struct pl_raw_primaries *orig, const struct pl_raw_primaries *update) { union { struct pl_raw_primaries prim; float raw[8]; } *pa = (void *) orig, *pb = (void *) update; pl_static_assert(sizeof(*pa) == sizeof(*orig)); for (int i = 0; i < PL_ARRAY_SIZE(pa->raw); i++) pa->raw[i] = PL_DEF(pa->raw[i], pb->raw[i]); } const struct pl_raw_primaries *pl_raw_primaries_get(enum pl_color_primaries prim) { /* Values from: ITU-R Recommendations BT.470-6, BT.601-7, BT.709-5, BT.2020-0 https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.470-6-199811-S!!PDF-E.pdf https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.601-7-201103-I!!PDF-E.pdf https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.709-5-200204-I!!PDF-E.pdf https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.2020-0-201208-I!!PDF-E.pdf Other colorspaces from https://en.wikipedia.org/wiki/RGB_color_space#Specifications */ // CIE standard illuminant series #define CIE_D50 {0.3457, 0.3585} #define CIE_D65 {0.3127, 0.3290} #define CIE_C {0.3100, 0.3160} #define CIE_E {1.0/3.0, 1.0/3.0} #define DCI {0.3140, 0.3510} static const struct pl_raw_primaries primaries[] = { [PL_COLOR_PRIM_BT_470M] = { .red = {0.670, 0.330}, .green = {0.210, 0.710}, .blue = {0.140, 0.080}, .white = CIE_C, }, [PL_COLOR_PRIM_BT_601_525] = { .red = {0.630, 0.340}, .green = {0.310, 0.595}, .blue = {0.155, 0.070}, .white = CIE_D65, }, [PL_COLOR_PRIM_BT_601_625] = { .red = {0.640, 0.330}, .green = {0.290, 0.600}, .blue = {0.150, 0.060}, .white = CIE_D65, }, [PL_COLOR_PRIM_BT_709] = { .red = {0.640, 0.330}, .green = {0.300, 0.600}, .blue = {0.150, 0.060}, .white = CIE_D65, }, [PL_COLOR_PRIM_BT_2020] = { .red = {0.708, 0.292}, .green = {0.170, 0.797}, .blue = {0.131, 0.046}, .white = CIE_D65, }, [PL_COLOR_PRIM_APPLE] = { .red = {0.625, 0.340}, .green = {0.280, 0.595}, .blue = {0.115, 0.070}, .white = CIE_D65, }, [PL_COLOR_PRIM_ADOBE] = { .red = {0.640, 0.330}, .green = {0.210, 0.710}, .blue = {0.150, 0.060}, .white = CIE_D65, }, [PL_COLOR_PRIM_PRO_PHOTO] = { .red = {0.7347, 0.2653}, .green = {0.1596, 0.8404}, .blue = {0.0366, 0.0001}, .white = CIE_D50, }, [PL_COLOR_PRIM_CIE_1931] = { .red = {0.7347, 0.2653}, .green = {0.2738, 0.7174}, .blue = {0.1666, 0.0089}, .white = CIE_E, }, // From SMPTE RP 431-2 [PL_COLOR_PRIM_DCI_P3] = { .red = {0.680, 0.320}, .green = {0.265, 0.690}, .blue = {0.150, 0.060}, .white = DCI, }, [PL_COLOR_PRIM_DISPLAY_P3] = { .red = {0.680, 0.320}, .green = {0.265, 0.690}, .blue = {0.150, 0.060}, .white = CIE_D65, }, // From Panasonic VARICAM reference manual [PL_COLOR_PRIM_V_GAMUT] = { .red = {0.730, 0.280}, .green = {0.165, 0.840}, .blue = {0.100, -0.03}, .white = CIE_D65, }, // From Sony S-Log reference manual [PL_COLOR_PRIM_S_GAMUT] = { .red = {0.730, 0.280}, .green = {0.140, 0.855}, .blue = {0.100, -0.05}, .white = CIE_D65, }, // From FFmpeg source code [PL_COLOR_PRIM_FILM_C] = { .red = {0.681, 0.319}, .green = {0.243, 0.692}, .blue = {0.145, 0.049}, .white = CIE_C, }, [PL_COLOR_PRIM_EBU_3213] = { .red = {0.630, 0.340}, .green = {0.295, 0.605}, .blue = {0.155, 0.077}, .white = CIE_D65, }, // From Wikipedia [PL_COLOR_PRIM_ACES_AP0] = { .red = {0.7347, 0.2653}, .green = {0.0000, 1.0000}, .blue = {0.0001, -0.0770}, .white = {0.32168, 0.33767}, }, [PL_COLOR_PRIM_ACES_AP1] = { .red = {0.713, 0.293}, .green = {0.165, 0.830}, .blue = {0.128, 0.044}, .white = {0.32168, 0.33767}, }, }; // This is the default assumption if no colorspace information could // be determined, eg. for files which have no video channel. if (!prim) prim = PL_COLOR_PRIM_BT_709; pl_assert(prim < PL_ARRAY_SIZE(primaries)); return &primaries[prim]; } // Compute the RGB/XYZ matrix as described here: // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html pl_matrix3x3 pl_get_rgb2xyz_matrix(const struct pl_raw_primaries *prim) { pl_matrix3x3 out = {{{0}}}; float S[3], X[4], Z[4]; X[0] = pl_cie_X(prim->red); X[1] = pl_cie_X(prim->green); X[2] = pl_cie_X(prim->blue); X[3] = pl_cie_X(prim->white); Z[0] = pl_cie_Z(prim->red); Z[1] = pl_cie_Z(prim->green); Z[2] = pl_cie_Z(prim->blue); Z[3] = pl_cie_Z(prim->white); // S = XYZ^-1 * W for (int i = 0; i < 3; i++) { out.m[0][i] = X[i]; out.m[1][i] = 1; out.m[2][i] = Z[i]; } pl_matrix3x3_invert(&out); for (int i = 0; i < 3; i++) S[i] = out.m[i][0] * X[3] + out.m[i][1] * 1 + out.m[i][2] * Z[3]; // M = [Sc * XYZc] for (int i = 0; i < 3; i++) { out.m[0][i] = S[i] * X[i]; out.m[1][i] = S[i] * 1; out.m[2][i] = S[i] * Z[i]; } return out; } pl_matrix3x3 pl_get_xyz2rgb_matrix(const struct pl_raw_primaries *prim) { // For simplicity, just invert the rgb2xyz matrix pl_matrix3x3 out = pl_get_rgb2xyz_matrix(prim); pl_matrix3x3_invert(&out); return out; } // LMS<-XYZ revised matrix from CIECAM97, based on a linear transform and // normalized for equal energy on monochrome inputs static const pl_matrix3x3 m_cat97 = {{ { 0.8562, 0.3372, -0.1934 }, { -0.8360, 1.8327, 0.0033 }, { 0.0357, -0.0469, 1.0112 }, }}; // M := M * XYZd<-XYZs static void apply_chromatic_adaptation(struct pl_cie_xy src, struct pl_cie_xy dest, pl_matrix3x3 *mat) { // If the white points are nearly identical, this is a wasteful identity // operation. if (fabs(src.x - dest.x) < 1e-6 && fabs(src.y - dest.y) < 1e-6) return; // XYZd<-XYZs = Ma^-1 * (I*[Cd/Cs]) * Ma // http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html // For Ma, we use the CIECAM97 revised (linear) matrix float C[3][2]; for (int i = 0; i < 3; i++) { // source cone C[i][0] = m_cat97.m[i][0] * pl_cie_X(src) + m_cat97.m[i][1] * 1 + m_cat97.m[i][2] * pl_cie_Z(src); // dest cone C[i][1] = m_cat97.m[i][0] * pl_cie_X(dest) + m_cat97.m[i][1] * 1 + m_cat97.m[i][2] * pl_cie_Z(dest); } // tmp := I * [Cd/Cs] * Ma pl_matrix3x3 tmp = {0}; for (int i = 0; i < 3; i++) tmp.m[i][i] = C[i][1] / C[i][0]; pl_matrix3x3_mul(&tmp, &m_cat97); // M := M * Ma^-1 * tmp pl_matrix3x3 ma_inv = m_cat97; pl_matrix3x3_invert(&ma_inv); pl_matrix3x3_mul(mat, &ma_inv); pl_matrix3x3_mul(mat, &tmp); } pl_matrix3x3 pl_get_adaptation_matrix(struct pl_cie_xy src, struct pl_cie_xy dst) { // Use BT.709 primaries (with chosen white point) as an XYZ reference struct pl_raw_primaries csp = *pl_raw_primaries_get(PL_COLOR_PRIM_BT_709); csp.white = src; pl_matrix3x3 rgb2xyz = pl_get_rgb2xyz_matrix(&csp); pl_matrix3x3 xyz2rgb = rgb2xyz; pl_matrix3x3_invert(&xyz2rgb); apply_chromatic_adaptation(src, dst, &xyz2rgb); pl_matrix3x3_mul(&xyz2rgb, &rgb2xyz); return xyz2rgb; } pl_matrix3x3 pl_ipt_rgb2lms(const struct pl_raw_primaries *prim) { static const pl_matrix3x3 hpe = {{ // HPE XYZ->LMS (D65) method { 0.40024f, 0.70760f, -0.08081f }, { -0.22630f, 1.16532f, 0.04570f }, { 0.00000f, 0.00000f, 0.91822f }, }}; const float c = 0.04; // 4% crosstalk pl_matrix3x3 m = {{ { 1 - 2*c, c, c }, { c, 1 - 2*c, c }, { c, c, 1 - 2*c }, }}; pl_matrix3x3_mul(&m, &hpe); // Apply chromatic adaptation to D65 if the input white point differs static const struct pl_cie_xy d65 = CIE_D65; apply_chromatic_adaptation(prim->white, d65, &m); const pl_matrix3x3 rgb2xyz = pl_get_rgb2xyz_matrix(prim); pl_matrix3x3_mul(&m, &rgb2xyz); return m; } pl_matrix3x3 pl_ipt_lms2rgb(const struct pl_raw_primaries *prim) { pl_matrix3x3 m = pl_ipt_rgb2lms(prim); pl_matrix3x3_invert(&m); return m; } // As standardized in Ebner & Fairchild IPT (1998) const pl_matrix3x3 pl_ipt_lms2ipt = {{ { 0.4000, 0.4000, 0.2000 }, { 4.4550, -4.8510, 0.3960 }, { 0.8056, 0.3572, -1.1628 }, }}; // Numerically inverted from the matrix above const pl_matrix3x3 pl_ipt_ipt2lms = {{ { 1.0, 0.0975689, 0.205226 }, { 1.0, -0.1138760, 0.133217 }, { 1.0, 0.0326151, -0.676887 }, }}; const struct pl_cone_params pl_vision_normal = {PL_CONE_NONE, 1.0}; const struct pl_cone_params pl_vision_protanomaly = {PL_CONE_L, 0.5}; const struct pl_cone_params pl_vision_protanopia = {PL_CONE_L, 0.0}; const struct pl_cone_params pl_vision_deuteranomaly = {PL_CONE_M, 0.5}; const struct pl_cone_params pl_vision_deuteranopia = {PL_CONE_M, 0.0}; const struct pl_cone_params pl_vision_tritanomaly = {PL_CONE_S, 0.5}; const struct pl_cone_params pl_vision_tritanopia = {PL_CONE_S, 0.0}; const struct pl_cone_params pl_vision_monochromacy = {PL_CONE_LM, 0.0}; const struct pl_cone_params pl_vision_achromatopsia = {PL_CONE_LMS, 0.0}; pl_matrix3x3 pl_get_cone_matrix(const struct pl_cone_params *params, const struct pl_raw_primaries *prim) { // LMS<-RGB := LMS<-XYZ * XYZ<-RGB pl_matrix3x3 rgb2lms = m_cat97; pl_matrix3x3 rgb2xyz = pl_get_rgb2xyz_matrix(prim); pl_matrix3x3_mul(&rgb2lms, &rgb2xyz); // LMS versions of the two opposing primaries, plus neutral float lms_r[3] = {1.0, 0.0, 0.0}, lms_b[3] = {0.0, 0.0, 1.0}, lms_w[3] = {1.0, 1.0, 1.0}; pl_matrix3x3_apply(&rgb2lms, lms_r); pl_matrix3x3_apply(&rgb2lms, lms_b); pl_matrix3x3_apply(&rgb2lms, lms_w); float a, b, c = params->strength; pl_matrix3x3 distort; switch (params->cones) { case PL_CONE_NONE: return pl_matrix3x3_identity; case PL_CONE_L: // Solve to preserve neutral and blue a = (lms_b[0] - lms_b[2] * lms_w[0] / lms_w[2]) / (lms_b[1] - lms_b[2] * lms_w[1] / lms_w[2]); b = (lms_b[0] - lms_b[1] * lms_w[0] / lms_w[1]) / (lms_b[2] - lms_b[1] * lms_w[2] / lms_w[1]); assert(fabs(a * lms_w[1] + b * lms_w[2] - lms_w[0]) < 1e-6); distort = (pl_matrix3x3) {{ { c, (1.0 - c) * a, (1.0 - c) * b}, { 0.0, 1.0, 0.0}, { 0.0, 0.0, 1.0}, }}; break; case PL_CONE_M: // Solve to preserve neutral and blue a = (lms_b[1] - lms_b[2] * lms_w[1] / lms_w[2]) / (lms_b[0] - lms_b[2] * lms_w[0] / lms_w[2]); b = (lms_b[1] - lms_b[0] * lms_w[1] / lms_w[0]) / (lms_b[2] - lms_b[0] * lms_w[2] / lms_w[0]); assert(fabs(a * lms_w[0] + b * lms_w[2] - lms_w[1]) < 1e-6); distort = (pl_matrix3x3) {{ { 1.0, 0.0, 0.0}, {(1.0 - c) * a, c, (1.0 - c) * b}, { 0.0, 0.0, 1.0}, }}; break; case PL_CONE_S: // Solve to preserve neutral and red a = (lms_r[2] - lms_r[1] * lms_w[2] / lms_w[1]) / (lms_r[0] - lms_r[1] * lms_w[0] / lms_w[1]); b = (lms_r[2] - lms_r[0] * lms_w[2] / lms_w[0]) / (lms_r[1] - lms_r[0] * lms_w[1] / lms_w[0]); assert(fabs(a * lms_w[0] + b * lms_w[1] - lms_w[2]) < 1e-6); distort = (pl_matrix3x3) {{ { 1.0, 0.0, 0.0}, { 0.0, 1.0, 0.0}, {(1.0 - c) * a, (1.0 - c) * b, c}, }}; break; case PL_CONE_LM: // Solve to preserve neutral a = lms_w[0] / lms_w[2]; b = lms_w[1] / lms_w[2]; distort = (pl_matrix3x3) {{ { c, 0.0, (1.0 - c) * a}, { 0.0, c, (1.0 - c) * b}, { 0.0, 0.0, 1.0}, }}; break; case PL_CONE_MS: // Solve to preserve neutral a = lms_w[1] / lms_w[0]; b = lms_w[2] / lms_w[0]; distort = (pl_matrix3x3) {{ { 1.0, 0.0, 0.0}, {(1.0 - c) * a, c, 0.0}, {(1.0 - c) * b, 0.0, c}, }}; break; case PL_CONE_LS: // Solve to preserve neutral a = lms_w[0] / lms_w[1]; b = lms_w[2] / lms_w[1]; distort = (pl_matrix3x3) {{ { c, (1.0 - c) * a, 0.0}, { 0.0, 1.0, 0.0}, { 0.0, (1.0 - c) * b, c}, }}; break; case PL_CONE_LMS: { // Rod cells only, which can be modelled somewhat as a combination of // L and M cones. Either way, this is pushing the limits of the our // color model, so this is only a rough approximation. const float w[3] = {0.3605, 0.6415, -0.002}; assert(fabs(w[0] + w[1] + w[2] - 1.0) < 1e-6); for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { distort.m[i][j] = (1.0 - c) * w[j] * lms_w[i] / lms_w[j]; if (i == j) distort.m[i][j] += c; } } break; } default: pl_unreachable(); } // out := RGB<-LMS * distort * LMS<-RGB pl_matrix3x3 out = rgb2lms; pl_matrix3x3_invert(&out); pl_matrix3x3_mul(&out, &distort); pl_matrix3x3_mul(&out, &rgb2lms); return out; } pl_matrix3x3 pl_get_color_mapping_matrix(const struct pl_raw_primaries *src, const struct pl_raw_primaries *dst, enum pl_rendering_intent intent) { // In saturation mapping, we don't care about accuracy and just want // primaries to map to primaries, making this an identity transformation. if (intent == PL_INTENT_SATURATION) return pl_matrix3x3_identity; // RGBd<-RGBs = RGBd<-XYZd * XYZd<-XYZs * XYZs<-RGBs // Equations from: http://www.brucelindbloom.com/index.html?Math.html // Note: Perceptual is treated like relative colorimetric. There's no // definition for perceptual other than "make it look good". // RGBd<-XYZd matrix pl_matrix3x3 xyz2rgb_d = pl_get_xyz2rgb_matrix(dst); // Chromatic adaptation, except in absolute colorimetric intent if (intent != PL_INTENT_ABSOLUTE_COLORIMETRIC) apply_chromatic_adaptation(src->white, dst->white, &xyz2rgb_d); // XYZs<-RGBs pl_matrix3x3 rgb2xyz_s = pl_get_rgb2xyz_matrix(src); pl_matrix3x3_mul(&xyz2rgb_d, &rgb2xyz_s); return xyz2rgb_d; } // Test the sign of 'p' relative to the line 'ab' (barycentric coordinates) static float test_point_line(const struct pl_cie_xy p, const struct pl_cie_xy a, const struct pl_cie_xy b) { return (p.x - b.x) * (a.y - b.y) - (a.x - b.x) * (p.y - b.y); } // Test if a point is entirely inside a gamut static float test_point_gamut(struct pl_cie_xy point, const struct pl_raw_primaries *prim) { float d1 = test_point_line(point, prim->red, prim->green), d2 = test_point_line(point, prim->green, prim->blue), d3 = test_point_line(point, prim->blue, prim->red); bool has_neg = d1 < -1e-6f || d2 < -1e-6f || d3 < -1e-6f, has_pos = d1 > 1e-6f || d2 > 1e-6f || d3 > 1e-6f; return !(has_neg && has_pos); } bool pl_primaries_superset(const struct pl_raw_primaries *a, const struct pl_raw_primaries *b) { return test_point_gamut(b->red, a) && test_point_gamut(b->green, a) && test_point_gamut(b->blue, a); } bool pl_primaries_valid(const struct pl_raw_primaries *prim) { // Test to see if the primaries form a valid triangle (nonzero area) float area = (prim->blue.x - prim->green.x) * (prim->red.y - prim->green.y) - (prim->red.x - prim->green.x) * (prim->blue.y - prim->green.y); return fabs(area) > 1e-6 && test_point_gamut(prim->white, prim); } static inline float xy_dist2(struct pl_cie_xy a, struct pl_cie_xy b) { const float dx = a.x - b.x, dy = a.y - b.y; return dx * dx + dy * dy; } bool pl_primaries_compatible(const struct pl_raw_primaries *a, const struct pl_raw_primaries *b) { float RR = xy_dist2(a->red, b->red), RG = xy_dist2(a->red, b->green), RB = xy_dist2(a->red, b->blue), GG = xy_dist2(a->green, b->green), GB = xy_dist2(a->green, b->blue), BB = xy_dist2(a->blue, b->blue); return RR < RG && RR < RB && GG < RG && GG < GB && BB < RB && BB < GB; } // returns the intersection of the two lines defined by ab and cd static struct pl_cie_xy intersection(struct pl_cie_xy a, struct pl_cie_xy b, struct pl_cie_xy c, struct pl_cie_xy d) { float det = (a.x - b.x) * (c.y - d.y) - (a.y - b.y) * (c.x - d.x); float t = ((a.x - c.x) * (c.y - d.y) - (a.y - c.y) * (c.x - d.x)) / det; return (struct pl_cie_xy) { .x = t ? a.x + t * (b.x - a.x) : 0.0f, .y = t ? a.y + t * (b.y - a.y) : 0.0f, }; } // x, y, z specified in clockwise order, with a, b, c being the enclosing gamut static struct pl_cie_xy clip_point(struct pl_cie_xy x, struct pl_cie_xy y, struct pl_cie_xy z, struct pl_cie_xy a, struct pl_cie_xy b, struct pl_cie_xy c) { const float d1 = test_point_line(y, a, b); const float d2 = test_point_line(y, b, c); if (d1 <= 0.0f && d2 <= 0.0f) { return y; // already inside triangle } else if (d1 > 0.0f && d2 > 0.0f) { return b; // target vertex fully enclosed } else if (d1 > 0.0f) { return intersection(a, b, y, z); } else { return intersection(x, y, b, c); } } struct pl_raw_primaries pl_primaries_clip(const struct pl_raw_primaries *src, const struct pl_raw_primaries *dst) { return (struct pl_raw_primaries) { .red = clip_point(src->green, src->red, src->blue, dst->green, dst->red, dst->blue), .green = clip_point(src->blue, src->green, src->red, dst->blue, dst->green, dst->red), .blue = clip_point(src->red, src->blue, src->green, dst->red, dst->blue, dst->green), .white = src->white, }; } /* Fill in the Y, U, V vectors of a yuv-to-rgb conversion matrix * based on the given luma weights of the R, G and B components (lr, lg, lb). * lr+lg+lb is assumed to equal 1. * This function is meant for colorspaces satisfying the following * conditions (which are true for common YUV colorspaces): * - The mapping from input [Y, U, V] to output [R, G, B] is linear. * - Y is the vector [1, 1, 1]. (meaning input Y component maps to 1R+1G+1B) * - U maps to a value with zero R and positive B ([0, x, y], y > 0; * i.e. blue and green only). * - V maps to a value with zero B and positive R ([x, y, 0], x > 0; * i.e. red and green only). * - U and V are orthogonal to the luma vector [lr, lg, lb]. * - The magnitudes of the vectors U and V are the minimal ones for which * the image of the set Y=[0...1],U=[-0.5...0.5],V=[-0.5...0.5] under the * conversion function will cover the set R=[0...1],G=[0...1],B=[0...1] * (the resulting matrix can be converted for other input/output ranges * outside this function). * Under these conditions the given parameters lr, lg, lb uniquely * determine the mapping of Y, U, V to R, G, B. */ static pl_matrix3x3 luma_coeffs(float lr, float lg, float lb) { pl_assert(fabs(lr+lg+lb - 1) < 1e-6); return (pl_matrix3x3) {{ {1, 0, 2 * (1-lr) }, {1, -2 * (1-lb) * lb/lg, -2 * (1-lr) * lr/lg }, {1, 2 * (1-lb), 0 }, }}; } // Applies hue and saturation controls to a YCbCr->RGB matrix static inline void apply_hue_sat(pl_matrix3x3 *m, const struct pl_color_adjustment *params) { // Hue is equivalent to rotating input [U, V] subvector around the origin. // Saturation scales [U, V]. float huecos = params->saturation * cos(params->hue); float huesin = params->saturation * sin(params->hue); for (int i = 0; i < 3; i++) { float u = m->m[i][1], v = m->m[i][2]; m->m[i][1] = huecos * u - huesin * v; m->m[i][2] = huesin * u + huecos * v; } } pl_transform3x3 pl_color_repr_decode(struct pl_color_repr *repr, const struct pl_color_adjustment *params) { params = PL_DEF(params, &pl_color_adjustment_neutral); pl_matrix3x3 m; switch (repr->sys) { case PL_COLOR_SYSTEM_BT_709: m = luma_coeffs(0.2126, 0.7152, 0.0722); break; case PL_COLOR_SYSTEM_BT_601: m = luma_coeffs(0.2990, 0.5870, 0.1140); break; case PL_COLOR_SYSTEM_SMPTE_240M: m = luma_coeffs(0.2122, 0.7013, 0.0865); break; case PL_COLOR_SYSTEM_BT_2020_NC: m = luma_coeffs(0.2627, 0.6780, 0.0593); break; case PL_COLOR_SYSTEM_BT_2020_C: // Note: This outputs into the [-0.5,0.5] range for chroma information. m = (pl_matrix3x3) {{ {0, 0, 1}, {1, 0, 0}, {0, 1, 0}, }}; break; case PL_COLOR_SYSTEM_BT_2100_PQ: { // Reversed from the matrix in the spec, hard-coded for efficiency // and precision reasons. Exact values truncated from ITU-T H-series // Supplement 18. static const float lm_t = 0.008609, lm_p = 0.111029625; m = (pl_matrix3x3) {{ {1.0, lm_t, lm_p}, {1.0, -lm_t, -lm_p}, {1.0, 0.560031, -0.320627}, }}; break; } case PL_COLOR_SYSTEM_BT_2100_HLG: { // Similar to BT.2100 PQ, exact values truncated from WolframAlpha static const float lm_t = 0.01571858011, lm_p = 0.2095810681; m = (pl_matrix3x3) {{ {1.0, lm_t, lm_p}, {1.0, -lm_t, -lm_p}, {1.0, 1.02127108, -0.605274491}, }}; break; } case PL_COLOR_SYSTEM_DOLBYVISION: m = repr->dovi->nonlinear; break; case PL_COLOR_SYSTEM_YCGCO: m = (pl_matrix3x3) {{ {1, -1, 1}, {1, 1, 0}, {1, -1, -1}, }}; break; case PL_COLOR_SYSTEM_UNKNOWN: // fall through case PL_COLOR_SYSTEM_RGB: m = pl_matrix3x3_identity; break; case PL_COLOR_SYSTEM_XYZ: { // For lack of anything saner to do, just assume the caller wants // DCI-P3 primaries, which is a reasonable assumption. const struct pl_raw_primaries *dst = pl_raw_primaries_get(PL_COLOR_PRIM_DCI_P3); m = pl_get_xyz2rgb_matrix(dst); // DCDM X'Y'Z' is expected to have equal energy white point (EG 432-1 Annex H) apply_chromatic_adaptation((struct pl_cie_xy)CIE_E, dst->white, &m); break; } case PL_COLOR_SYSTEM_COUNT: pl_unreachable(); } // Apply hue and saturation in the correct way depending on the colorspace. if (pl_color_system_is_ycbcr_like(repr->sys)) { apply_hue_sat(&m, params); } else if (params->saturation != 1.0 || params->hue != 0.0) { // Arbitrarily simulate hue shifts using the BT.709 YCbCr model pl_matrix3x3 yuv2rgb = luma_coeffs(0.2126, 0.7152, 0.0722); pl_matrix3x3 rgb2yuv = yuv2rgb; pl_matrix3x3_invert(&rgb2yuv); apply_hue_sat(&yuv2rgb, params); // M := RGB<-YUV * YUV<-RGB * M pl_matrix3x3_rmul(&rgb2yuv, &m); pl_matrix3x3_rmul(&yuv2rgb, &m); } // Apply color temperature adaptation, relative to BT.709 primaries if (params->temperature) { struct pl_cie_xy src = pl_white_from_temp(6500); struct pl_cie_xy dst = pl_white_from_temp(6500 + 3500 * params->temperature); pl_matrix3x3 adapt = pl_get_adaptation_matrix(src, dst); pl_matrix3x3_rmul(&adapt, &m); } pl_transform3x3 out = { .mat = m }; int bit_depth = PL_DEF(repr->bits.sample_depth, PL_DEF(repr->bits.color_depth, 8)); double ymax, ymin, cmax, cmid; double scale = (1LL << bit_depth) / ((1LL << bit_depth) - 1.0); switch (pl_color_levels_guess(repr)) { case PL_COLOR_LEVELS_LIMITED: { ymax = 235 / 256. * scale; ymin = 16 / 256. * scale; cmax = 240 / 256. * scale; cmid = 128 / 256. * scale; break; } case PL_COLOR_LEVELS_FULL: // Note: For full-range YUV, there are multiple, subtly inconsistent // standards. So just pick the sanest implementation, which is to // assume MAX_INT == 1.0. ymax = 1.0; ymin = 0.0; cmax = 1.0; cmid = 128 / 256. * scale; // *not* exactly 0.5 break; default: pl_unreachable(); } double ymul = 1.0 / (ymax - ymin); double cmul = 0.5 / (cmax - cmid); double mul[3] = { ymul, ymul, ymul }; double black[3] = { ymin, ymin, ymin }; #ifdef PL_HAVE_DOVI if (repr->sys == PL_COLOR_SYSTEM_DOLBYVISION) { // The RPU matrix already includes levels normalization, but in this // case we also have to respect the signalled color offsets for (int i = 0; i < 3; i++) { mul[i] = 1.0; black[i] = repr->dovi->nonlinear_offset[i] * scale; } } else #endif if (pl_color_system_is_ycbcr_like(repr->sys)) { mul[1] = mul[2] = cmul; black[1] = black[2] = cmid; } // Contrast scales the output value range (gain) // Brightness scales the constant output bias (black lift/boost) for (int i = 0; i < 3; i++) { mul[i] *= params->contrast; out.c[i] += params->brightness; } // Multiply in the texture multiplier and adjust `c` so that black[j] keeps // on mapping to RGB=0 (black to black) for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { out.mat.m[i][j] *= mul[j]; out.c[i] -= out.mat.m[i][j] * black[j]; } } // Finally, multiply in the scaling factor required to get the color up to // the correct representation. pl_matrix3x3_scale(&out.mat, pl_color_repr_normalize(repr)); // Update the metadata to reflect the change. repr->sys = PL_COLOR_SYSTEM_RGB; repr->levels = PL_COLOR_LEVELS_FULL; return out; } bool pl_icc_profile_equal(const struct pl_icc_profile *p1, const struct pl_icc_profile *p2) { if (p1->len != p2->len) return false; // Ignore signatures on length-0 profiles, as a special case return !p1->len || p1->signature == p2->signature; } void pl_icc_profile_compute_signature(struct pl_icc_profile *profile) { if (!profile->len) profile->signature = 0; // In theory, we could get this value from the profile header itself if // lcms is available, but I'm not sure if it's even worth the trouble. Just // hard-code this to a pl_mem_hash(), which is decently fast anyway. profile->signature = pl_mem_hash(profile->data, profile->len); }