Commit d357ac9c authored by Mickaël Desfrênes's avatar Mickaël Desfrênes
Browse files

Fix excessive brightness in HDR-to-SDR tone mapping

Use BT.709 luminance coefficients during highlight desaturation to avoid brightening neutral RGB highlights. Check expected brightness across HLS variants and thumbnails, and bump Jama to 2026.10.16.1.
parent 75e172f4
Loading
Loading
Loading
Loading
+20 −8
Changes for src/hls/test_conversion.py: 20 added lines, 8 removed lines.
Original line number Diff line number Diff line
@@ -56,7 +56,12 @@ class HLSConversionTestCase(SimpleTestCase):
        return sum(pixels) / len(pixels)

    def convert_sample(
        self, transfer: str, *, audio: bool = False, missing_color_tags: bool = False
        self,
        transfer: str,
        *,
        audio: bool = False,
        missing_color_tags: bool = False,
        expected_luma: float | None = None,
    ) -> None:
        with TemporaryDirectory() as temp_dir:
            source_path = Path(temp_dir, "source.mp4")
@@ -137,16 +142,20 @@ class HLSConversionTestCase(SimpleTestCase):
                    side_data = str(metadata["frames"])
                    self.assertNotIn("Mastering display metadata", side_data)
                    self.assertNotIn("Content light level metadata", side_data)
                    difference = abs(self.center_luma(segment) - source_luma)
                    if hdr:
                    output_luma = self.center_luma(segment)
                    difference = abs(output_luma - source_luma)
                    if expected_luma is not None:
                        self.assertAlmostEqual(output_luma, expected_luma, delta=3)
                    elif hdr:
                        self.assertGreater(difference, 5)
                    else:
                        self.assertLessEqual(difference, 2)

            thumbnail_difference = abs(
                self.center_luma(destination / "thumbnail.jpg") - source_luma
            )
            if hdr:
            thumbnail_luma = self.center_luma(destination / "thumbnail.jpg")
            thumbnail_difference = abs(thumbnail_luma - source_luma)
            if expected_luma is not None:
                self.assertAlmostEqual(thumbnail_luma, expected_luma, delta=3)
            elif hdr:
                self.assertGreater(thumbnail_difference, 5)
            else:
                self.assertLessEqual(thumbnail_difference, 2)
@@ -156,7 +165,10 @@ class HLSConversionTestCase(SimpleTestCase):
        self.convert_sample("smpte2084", audio=True)

    def test_hlg_hdr_is_tone_mapped_without_audio(self):
        self.convert_sample("arib-std-b67")
        # This grey is about 200 nits in HLG and maps to roughly 51% linear SDR
        # with Hable's 1000-nit peak, or 194 with BT.1886 display gamma (2.4).
        # Desaturation must preserve neutral brightness.
        self.convert_sample("arib-std-b67", expected_luma=194)

    def test_hdr_with_missing_primaries_and_matrix_uses_bt2020(self):
        self.convert_sample("smpte2084", missing_color_tags=True)
+1 −1
Changes for src/jama/__init__.py: 1 added line, 1 removed line.
Original line number Diff line number Diff line
__version__ = "2026.10.16.0"
__version__ = "2026.10.16.1"
+3 −1
Changes for src/resources/helpers.py: 3 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -245,9 +245,11 @@ def _hls_hdr_to_sdr_filter(video_stream: dict) -> str:
        input_tags.append("colorspace=bt2020nc")
    input_filter = f"setparams={':'.join(input_tags)}," if input_tags else ""

    # Tonemap uses this tag for luminance even on RGB frames; RGB's (1, 1, 1)
    # weights would brighten neutral highlights during desaturation.
    return (
        f"{input_filter}zscale=transferin={transfer}:transfer=linear:npl=100,"
        "format=gbrpf32le,zscale=primaries=bt709,"
        "format=gbrpf32le,zscale=primaries=bt709,setparams=colorspace=bt709,"
        "tonemap=tonemap=hable:desat=2,"
        "zscale=transfer=bt709:matrix=bt709:range=limited:dither=error_diffusion,"
        "format=yuv420p,"