Skip to content

Instantly share code, notes, and snippets.

@zachlewis
Created June 2, 2025 13:00
Show Gist options
  • Save zachlewis/bdc55517f3c3797155ed64576c1e8c68 to your computer and use it in GitHub Desktop.
Save zachlewis/bdc55517f3c3797155ed64576c1e8c68 to your computer and use it in GitHub Desktop.
MaxCLL and MaxFALL example
from functools import lru_cache
from typing import Sequence, Callable, Tuple
from fastprogress import progress_bar
import numpy as np
import OpenImageIO as oiio
@lru_cache
def compute_maxcll_maxfall(
image_sequence: Sequence[Union[str, Path, oiio.ImageBuf]],
convert_to_pq: Callable[[oiio.ImageBuf], oiio.ImageBuf] | None = None,
permit_outliers: bool = False,
dry_run: bool = False,
) -> Tuple[float, float]:
"""
Compute MaxCLL and MaxFALL using an outlier rejection method.
Based on the paper "A New Method for Measuring Maximum Content Light Level
(MaxCLL) and Maximum Frame Average Light Level (MaxFALL) for HDR Video" by
Michael D. Smith and Michael Zink, published in 2021.
See https://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=9508136
Parameters
----------
image_sequence:
List of file paths or OIIO ImageBufs.
convert_to_pq:
Arbitrary callable for pre-transforming each `ImageBuf`, i.e. when analyzing
non-PQ-encoded image sequences.
permit_outliers:
Use an outlier rejection method to compute MaxCLL and MaxFALL.
dry_run:
If True, only process the first two images in the sequence for testing.
Returns
-------
(MaxCLL, MaxFALL) in cd/m^2 for the entire sequence (float).
"""
per_frame_max,per_frame_99_99,per_frame_avg = [],[],[]
if dry_run:
image_sequence = image_sequence[0:2]
for img in progress_bar(image_sequence):
buf = oiio.ImageBuf(str(img)) if isinstance(img, (str, Path)) else img
if convert_to_pq:
buf = convert_to_pq(buf)
buf = oiio.ImageBufAlgo.ocionamedtransform(
buf, "ST-2084 - Curve", colorconfig="ocio://studio-config-latest")
stats = oiio.ImageBufAlgo.computePixelStats(buf)
if not permit_outliers:
# 99.99th percentile of all pixel values
per_frame_99_99.append(np.percentile(buf.get_pixels(oiio.FLOAT), 99.99))
else:
per_frame_max.append(np.max(stats.max))
per_frame_avg.append(np.max(stats.avg))
if not permit_outliers:
# 99.5th percentile of per-frame 99.99th percentiles
maxcll = np.percentile(per_frame_99_99, 99.5)
# 99.75th percentile of per-frame averages
maxfall = np.percentile(per_frame_avg, 99.75)
else:
maxcll = np.max(per_frame_max)
maxfall = np.max(per_frame_avg)
return float(maxcll * 100), float(maxfall * 100)
@zachlewis
Copy link
Author

(note: I haven't actually tested this in production; but it produces seemingly valid values for the datasets I've experimented with, which certainly include a handful of NaNs and infs here and there).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment