Skip to content

PR: Add TLCI-2012 and TLMF-2013 quality metrics - #1408

Open
cmuellner wants to merge 1 commit into
colour-science:developfrom
cmuellner:tlci
Open

PR: Add TLCI-2012 and TLMF-2013 quality metrics#1408
cmuellner wants to merge 1 commit into
colour-science:developfrom
cmuellner:tlci

Conversation

@cmuellner

Copy link
Copy Markdown
Contributor

Summary

Add EBU Tech 3355 TLCI-2012 and TLMF-2013 quality metrics.

This implements:

  • Television Lighting Consistency Index, TLCI-2012.
  • Television Luminaire Matching Factor, TLMF-2013.
  • EBU Tech 3355 camera, display, saturation, and reference-selection data.
  • EBU Tech 3355 Appendix 2 Planckian and daylight chromaticity tables used
    for TLCI CCT selection.
  • EBU Tech 3355 Appendix 3 daylight radiation vector data used for TLCI
    daylight references.
  • EBU Tech 3355 Appendix 4 test colour sample data used by TLCI/TLMF,
    including the documented BBC R&D Report 1988/2 and unpublished BBC R&D
    provenance.
  • Public colour and colour.quality API exports.
  • Documentation, examples, and bibliography entries.

Fixes #974.

Implementation Notes

TLCI/TLMF was not a straightforward transcription from the public
specification. A few parts of EBU Tech 3355 require careful interpretation:

  • The CCT selection is defined by Tech 3355, not by Colour's existing
    CCT/D_uv methods such as Ohno (2013) or Robertson (1968). Tech 3355 uses
    the Appendix 2 Planckian and daylight chromaticity tables, scans adjacent
    locus points, and computes the normal intersection using the section 1.1.1
    slope/angle procedure.
  • The Planckian and daylight CCT loci are the Appendix 2 lookup tables, not
    regenerated blackbody or CIE daylight loci. The Planckian and daylight loci
    also remain separate through the 3400-5000 K mixed-reference region.
  • The Planckian reference spectrum is Tech 3355-specific. Section 1.1.2.1
    equation [9] uses a simplified Planckian expression with wavelength in
    nanometres, a 1.435e7 nm K radiation constant, and normalisation at
    560 nm; this is not delegated to Colour's generic blackbody helper.
  • The daylight reference spectrum is also Tech 3355-specific. Section 1.1.2.2
    uses Appendix 3 daylight radiation vectors and Tech 3355 coefficients for
    M, M1, and M2; this differs from Colour's general
    sd_CIE_illuminant_D_series helper, which uses the library CIE D-series
    basis data and its standard coefficient/rounding path.
  • The 3400-5000 K mixed-reference spectrum is interpolated from fixed
    Planckian 3400 K and daylight 5000 K endpoint spectra, after normalising
    each endpoint at 560 nm. It is not generated from Planckian/daylight spectra
    at the test source CCT.
  • The Appendix 4 test-colour samples are the mandatory TLCI/TLMF data, but
    they are not simply a direct copy of the published BBC R&D Report 1988/2
    ColorChecker table. Some coloured samples differ slightly from the published
    BBC table, and the greyscale samples are attributed by Tech 3355 to
    unpublished BBC R&D data. The dataset is therefore documented as Tech 3355
    data, with BBC R&D Report 1988/2 as partial provenance.
  • The Tech 3355 d_uv calculation differs from Colour's existing
    CCT/D_uv methods such as Ohno (2013) and Robertson (1968): it uses the
    Tech 3355 reference loci, normalises the distance by 0.0054, and reports
    positive values on the magenta side and negative values on the green side.
    This follows the section 1.1.1 prose that reverses the sign for green-side
    offsets, where u_T < u_L, and the section 1.1.2.3 equations [16]-[17]
    labels (d > 0 towards magenta, d <= 0 towards green). The EBU reference
    binary appears to report this secondary d_uv value with the opposite sign,
    but the implementation follows the published specification. The reported
    d_uv also includes the Tech 3355 mixed-region adjustment between 4000 K
    and 5000 K, where the Planckian and daylight loci are not joined.
  • TLMF shares the TLCI camera/display pipeline but uses the reference luminaire
    balance with a test-luminaire exposure correction, matching the TLMF
    procedure instead of independently white-balancing both sources as TLCI does.
    The exposure correction uses the reference/test neutral luma ratio after the
    camera matrix.
  • TLCI-2012 aggregates colour differences over only the first 18 coloured
    ColorChecker samples, while TLMF-2013 uses all 24 samples, including the
    greyscale patches.
  • The camera BT.709 signal path preserves the studio-swing headroom described
    by Tech 3355 section 2, equation [61], capping at code value 255 relative
    to the nominal 16-235 range instead of clipping at nominal white.
  • The Tech 3355 colour-difference calculation follows the section 1.5
    CIEDE2000 path with unity k factors, using Colour's existing CIE 2000
    implementation for the standard colour-difference calculation.

Validation

EBU Tech 3355 does not distribute official reference test vectors. The tests
therefore use reproducible generated spectral distributions covering the main
specified algorithm paths:

  • Planckian sources.
  • Tech 3355 daylight-reference sources.
  • Mixed-reference spectra in the 3400-5000 K transition.
  • Reference-locus distance sign checks for green/magenta chromaticity
    offsets.
  • Phosphor-like LED spectra.
  • RGB LED spectra.
  • Public in-tree CIE/NIST LED spectral distributions as regression/stress
    coverage.
  • TLMF identical, close-match, mismatch, and mixed-reference pairs.

Additional local validation sets, including a 1000-case TLCI and 1000-pair
TLMF diagnostic set, were used to stress the CCT branches, mixed-reference
transition, green/magenta d_uv sign, LED spectra, fixture-like spectra, and
low-score TLMF cases. The committed tests use only deterministic,
redistributable spectra and public in-tree spectral data; they do not depend
on local measurement files.

Because no official reference vectors are published, this PR cannot claim
independent conformance from official test data. The validation strategy is
instead:

  • implement each calculation step from the published specification;
  • reuse existing Colour mathematics for spectral integration, colour
    transfer functions, and colourspace conversions;
  • keep TLCI-specific code only where Tech 3355 defines a calculation that is
    not covered by an existing Colour routine;
  • commit a smaller redistributable validation set that exercises the same
    algorithm branches.

Tests

  • Relevant pre-commit hook tools run directly: isort, ruff format,
    ruff check, codespell, blacken-docs, check-added-large-files,
    check-case-conflict, check-merge-conflict, check-symlinks,
    check-yaml, debug-statement-hook, end-of-file-fixer,
    mixed-line-ending, name-tests-test, and trailing-whitespace-fixer.
  • prettier and the RST pygrep hooks were run through prek; no
    requirements.txt files are touched by the commit.
  • uv run ruff check colour/quality/tlci.py colour/quality/datasets/tlci2012.py colour/quality/datasets/__init__.py colour/quality/tests/test_tlci.py
  • uv run pyright --threads --skipunannotated colour/quality/tlci.py colour/quality/datasets/tlci2012.py colour/quality/tests/test_tlci.py
  • uv run python -m doctest colour/quality/tlci.py
  • uv run pytest --doctest-modules colour/quality/tlci.py
  • uv run pytest colour/quality/tests/test__init__.py colour/quality/tests/test_tlci.py

Preflight

Code Style and Quality

  • Unit tests have been implemented and passed.
  • Pyright static checking has been run and passed.
  • Pre-commit hooks have been run and passed.
  • [n/a] New transformations have been added to the Automatic Colour Conversion Graph.
  • [n/a] New transformations have been exported to the relevant namespaces, e.g. colour, colour.models.

Documentation

  • New features are documented along with examples if relevant.
  • The documentation is Sphinx and numpydoc compliant.

@cmuellner

Copy link
Copy Markdown
Contributor Author

CI is currently failing for two reasons.

The GitHub Actions failure appears unrelated to this PR. The failing job is windows-latest - Python 3.12, and the failures are all urllib.error.HTTPError: HTTP Error 429: Too Many Requests in existing network-dependent tests while downloading Munsell ONNX assets / test resources via download_url(). The new TLCI/TLMF tests are not among the failures, and the other OS/Python jobs passed, so this looks like external rate limiting rather than a behavioral regression.

Codacy reports 65 new issues, mostly in the new TLCI/TLMF files:

  • 24x Bandit_B101 for pytest assert usage in colour/quality/tests/test_tlci.py.
  • 41x Prospector/pydocstyle findings, mostly docstring spacing/formatting rules
    such as D202, D200, D203, D205, D212, D413, and D415.
  • Codacy also reports one new duplication item in
    colour/quality/datasets/tlci2012.py, which appears to be related to reference
    dataset content.

My read is that many of these findings conflict with the existing project style: the test suite widely uses pytest assert, Ruff ignores S101 for test_* files, and several of the pydocstyle rules reported by Codacy are explicitly ignored by
the local Ruff configuration or conflict with existing module/class docstring patterns. That said, if maintainers prefer satisfying Codacy on this PR, I can adjust the new files accordingly or add targeted suppressions/configuration. I’m
fine with either direction; the main tradeoff is consistency with existing style versus making Codacy green without changing Codacy configuration.

@thomasmansencal thomasmansencal changed the title Add TLCI-2012 and TLMF-2013 quality metrics PR: Add TLCI-2012 and TLMF-2013 quality metrics Jun 6, 2026
@thomasmansencal

Copy link
Copy Markdown
Member

Hi @cmuellner,

Thanks a lot! Great work. #1406 is pending and is incompatible with it at the moment so, the best way would be for me to merge it and rebase #1406 on top of it and bring it up to Array API standard compatibility.

Don't worry about Codacy and the ONNX download CI issue.

@thomasmansencal thomasmansencal added this to the v0.4.8 milestone Jun 6, 2026
@cmuellner

Copy link
Copy Markdown
Contributor Author

Thanks! If you would prefer the opposite sequencing for any reason (Python Array API lands first), I’m also happy to adjust this PR accordingly.

@thomasmansencal

Copy link
Copy Markdown
Member

The issue is that we might change some of our Array API Standard implementation. I will keep you posted!

@thomasmansencal thomasmansencal left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @cmuellner!

I left a few comments, most of them being about consistency with the surrounding code. When they are addressed, I will merge, do a polish pass on everything I might have missed and rebase the Array API PR on top of develop.

Comment thread colour/quality/tlci.py Outdated
name: str
tlci: float
delta_E_a: float
delta_E: NDArrayFloat

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor

Could we rename it to delta_E_s, the way cfi2017 does, i.e., the per-sample colour differences pairing with the existing delta_E_a? Same field on the TLMF specification at line 126.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, renamed on both the TLCI and TLMF specifications.

Comment thread colour/quality/tlci.py Outdated


@dataclass()
class TLCI2012_Specification:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Major

For consistency with the sibling specifications, could we follow the <Category>_Specification_<Method> pattern (ColourRendering_Specification_CRI, ColourQuality_Specification_ANSIIESTM3018)? So ColourQuality_Specification_TLCI2012 / ColourQuality_Specification_TLMF2013.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Renamed to ColourQuality_Specification_TLCI2012 and ColourQuality_Specification_TLMF2013, and propagated to colour/__init__.py, colour/quality/__init__.py and docs/colour.quality.rst.

Comment thread colour/quality/tlci.py Outdated
"""

name: str
tlci: float

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Major

Could we name the score field Q_a, to match the other quality specifications (cri, cqs)? _Q_from_delta_E already returns Q_a, so it lines up nicely.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment thread colour/quality/tlci.py Outdated
delta_E_a: float
delta_E: NDArrayFloat
CCT: float
d_uv: float

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor

Small consistency one: d_uv -> D_uv, matching cfi2017. The docstring already explains the EBU d mapping; the only thing to add there is the sign note (see the comment on the sign below).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. The sign note is now a .. note:: block in the D_uv field docstring; see my reply on the sign convention thread for the wording and the open question.

Comment thread colour/quality/tlci.py
__email__ = "colour-developers@colour-science.org"
__status__ = "Production"

__all__ = [

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Major

This module leans heavily on private helpers (17 def _*), whereas the siblings (cfi2017, cri, cqs, ssi, tm3018) expose their computation helpers publicly via __all__. Once the trivial ones are inlined, it would be great to promote the substantive helpers (sd_planckian_TLCI2012, sd_daylight_TLCI2012, sd_reference_illuminant_TLCI2012, uv_to_CCT_TLCI2012) to public, prefix-named, and list them here, so it matches the family.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment thread colour/quality/tlci.py Outdated
np.float64(29.49254175...)
"""

camera_name = camera or "EBU Standard Camera"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor

camera or "EBU Standard Camera" could be optional(camera, "EBU Standard Camera"); it also avoids or swallowing an empty string.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment thread colour/quality/tlci.py Outdated
# radiation constant, and normalisation at 560 nm. The implementation
# returns unity at 560 nm instead of 100; the global scale is absorbed by
# the camera white-balance step.
wavelengths = np.asarray(shape.wavelengths)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pedantry

np.asarray(shape.wavelengths) is redundant, shape.wavelengths is already an array (same in the tests).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment thread colour/quality/tlci.py Outdated
return UCS_to_uv(XYZ_to_UCS(_sd_to_XYZ(sd, shape)))


@functools.cache

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor

@functools.cache on a no-argument constant builder could just be a module-level constant.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

"MSDS_CAMERA_CURVES_TLCI2012",
"MSDS_DAYLIGHT_BASIS_TLCI2012",
"SDS_TCS_TLCI2012",
"MSDS_CAMERA_SENSITIVITIES_TLCI2012",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pedantry

Small ordering thing: MSDS_CAMERA_SENSITIVITIES_TLCI2012 sits after SDS_TCS_TLCI2012 in __all__. The import block above is sorted, so it would be nice to sort __all__ to match.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment thread colour/quality/tests/test_tlci.py Outdated
return np.exp(-0.5 * ((wavelengths - centre) / width) ** 2)


def normalised_sd(values: np.ndarray, name: str) -> SpectralDistribution:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

These free helpers use the _sd suffix (normalised_sd, planckian_sd); the ecosystem convention is the sd_* prefix, if you would like to align them.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@cmuellner

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review @thomasmansencal, and apologies for the slow turnaround.

All points are addressed and the branch is rebased onto current develop.

Naming / API

  • TLCI2012_Specification / TLMF2013_SpecificationColourQuality_Specification_TLCI2012 / ColourQuality_Specification_TLMF2013, propagated to colour/__init__.py, colour/quality/__init__.py and docs/colour.quality.rst.
  • Score field → Q_a, per-sample differences → delta_E_s, d_uvD_uv.
  • Promoted sd_planckian_TLCI2012, sd_daylight_TLCI2012, sd_reference_illuminant_TLCI2012 and uv_to_CCT_TLCI2012 to public and listed them in __all__. Private helpers are down from 17 to 10. I followed the cfi2017 pattern: the helpers are listed in the module __all__, while colour.quality re-exports only the specification and the top-level function.
  • MSDS_CAMERA_CURVES_TLCI2012MSDS_CAMERA_SENSITIVITIES_TLCI2012, now built as RGB_CameraSensitivities. DATA_CAMERA_CURVES_TLCI2012 is gone.

Simplification

  • Inlined _align_sd, _align_msds, _sd_to_XYZ and _sd_to_uv; _normalise_sd_560 kept and renamed _sd_normalise_560_TLCI2012.
  • Collapsed the per-patch integration loop to a single msds_to_XYZ(..., method="Integration").
  • Replaced @functools.cache with a module-level constant, camera or ... with optional(...), and dropped the redundant np.asarray(shape.wavelengths).

Tests and docs

  • Restructured into one class per public function and per public helper, ordered to match the source, and renamed the module-level fixtures to the sd_* prefix.
  • Added coverage for the three untested paths: the _nearest_locus_sample_TLCI2012 fallback, the all-excluded raise in _Q_from_delta_E, and the non-default camera argument.
  • Added the Notes block on the Appendix 3 daylight radiation vectors documenting the deviation from CIE Colorimetry Table 6.
  • Sorted __all__ in colour/quality/datasets/__init__.py to match the import block.

Locally: ruff and ruff format clean, pyright --threads --skipunannotated reports 0 errors, and the colour/quality and colour/tests/test__all__.py suites pass.

One item is still an open decision rather than a change: the D_uv sign convention (your question on tlci.py:417). I have kept the published Tech 3355 convention and documented the disagreement with the EBU application, but I am happy to flip it if you prefer matching the tool — details in that thread.

Also, is the sequencing from June still what you want, i.e. this lands first and #1406 rebases on top? Happy to work the other way round if the Array API work has moved on since.

Implement the EBU Tech 3355 Television Lighting Consistency Index
and Television Luminaire Matching Factor calculations, including the
standard camera model, TLMF exposure normalisation, Tech 3355
reference-locus CCT selection, reference illuminant selection, and
reported result data.

Add the EBU Tech 3355 Appendix 2 Planckian and daylight chromaticity
tables, Appendix 3 daylight radiation vectors, and Appendix 4 test
colour sample data used by TLCI and TLMF. Document that the coloured
samples are derived from BBC R&D Report 1988/2 with small deviations
in the published table, and that the greyscale samples use unpublished
BBC R&D data.

Reuse Colour spectral, transfer-function, colourspace, and CIE 2000
colour-difference routines for the underlying mathematics. Keep only
Tech 3355-specific CCT normal-intersection and Planckian/daylight
reference synthesis local, because TLCI defines those calculations
against its Appendix data and reference-luminaire procedure.

Preserve the Tech 3355 studio-swing signal headroom through the camera
BT.709 signal path, and cap it at the 8-bit code-value limit defined
by the nominal 16-235 range.

Add public quality API exports, documentation, examples, and
reproducible validation spectra covering Planckian, daylight,
mixed-reference, phosphor-like LED, RGB LED, and TLMF matching cases.

Include reference-locus distance sign, mixed-boundary, TLMF exposure,
Tech 3355 Planckian-reference, CIE 2000, and test-colour sample
regression tests. This provides redistributable test coverage despite
the lack of official EBU reference vectors.

Fixes colour-science#974

Signed-off-by: Christoph Müllner <christophm30@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE]: Implement TLCI / TLMF calculation

2 participants