Skip to content

Commit e712afa

Browse files
committed
Fix color space conversion filters
1 parent d492060 commit e712afa

2 files changed

Lines changed: 100 additions & 21 deletions

File tree

‎sharp_frames/processing/colorspace.py‎

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -300,9 +300,10 @@ def _build_hdr_to_sdr_filter(color_info: VideoColorInfo) -> str:
300300
"format=yuv420p" # Standard output format
301301
)
302302
else:
303-
# Fallback: basic colorspace conversion (won't tone map properly)
304-
# This is better than nothing but may clip highlights
305-
filter_chain = f"colorspace=all=bt709:iall={primaries_in}:fast=0"
303+
raise RuntimeError(
304+
"HDR-to-SDR conversion requires FFmpeg's zscale filter. "
305+
"Install an FFmpeg build with libzimg support."
306+
)
306307

307308
return filter_chain
308309

@@ -311,18 +312,31 @@ def _build_wide_gamut_to_srgb_filter(color_info: VideoColorInfo) -> str:
311312
"""
312313
Build wide gamut SDR (Display P3/BT.2020) to sRGB/BT.709 filter.
313314
"""
314-
# Determine input color space for the filter
315-
if color_info.color_primaries == ColorPrimaries.DISPLAY_P3:
316-
input_primaries = "smpte432"
317-
elif color_info.color_primaries == ColorPrimaries.BT2020:
318-
input_primaries = "bt2020"
319-
else:
320-
input_primaries = "bt709" # Assume BT.709 if unknown
321-
322-
# Use colorspace filter for gamut conversion (no tone mapping needed)
323-
filter_chain = f"colorspace=all=bt709:iall={input_primaries}:fast=0"
324-
325-
return filter_chain
315+
input_primaries = {
316+
ColorPrimaries.DISPLAY_P3: "smpte432",
317+
ColorPrimaries.BT2020: "bt2020",
318+
}.get(color_info.color_primaries, "bt709")
319+
input_transfer = {
320+
TransferFunction.SRGB: "iec61966-2-1",
321+
TransferFunction.BT709: "bt709",
322+
}.get(color_info.transfer_function, "bt709")
323+
default_matrix = (
324+
"bt2020nc"
325+
if color_info.color_primaries == ColorPrimaries.BT2020
326+
else "bt709"
327+
)
328+
input_matrix = {
329+
ColorMatrix.BT709: "bt709",
330+
ColorMatrix.BT2020_NCL: "bt2020nc",
331+
ColorMatrix.BT2020_CL: "bt2020c",
332+
}.get(color_info.color_matrix, default_matrix)
333+
334+
return (
335+
"colorspace=all=bt709:"
336+
f"ispace={input_matrix}:"
337+
f"iprimaries={input_primaries}:"
338+
f"itrc={input_transfer}:fast=0"
339+
)
326340

327341

328342
def get_color_info_description(color_info: VideoColorInfo) -> str:

‎tests/test_colorspace.py‎

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
"""Tests for the colorspace detection and conversion module."""
22

3-
import pytest
3+
import shutil
4+
import subprocess
45
from unittest.mock import patch, MagicMock
56

7+
import pytest
8+
69
from sharp_frames.processing.colorspace import (
710
VideoColorInfo,
811
ColorPrimaries,
@@ -198,6 +201,8 @@ def test_filter_for_display_p3(self):
198201
assert 'colorspace' in filter_str
199202
assert 'bt709' in filter_str
200203
assert 'smpte432' in filter_str
204+
assert 'iprimaries=smpte432' in filter_str
205+
assert 'iall=smpte432' not in filter_str
201206

202207
@patch('sharp_frames.processing.colorspace.is_zscale_available', return_value=True)
203208
def test_hdr_filter_with_zscale(self, mock_zscale):
@@ -219,17 +224,77 @@ def test_hdr_filter_with_zscale(self, mock_zscale):
219224

220225
@patch('sharp_frames.processing.colorspace.is_zscale_available', return_value=False)
221226
def test_hdr_filter_fallback_without_zscale(self, mock_zscale):
222-
"""HDR filter falls back to colorspace when zscale unavailable."""
227+
"""HDR conversion fails clearly when tone mapping is unavailable."""
223228
info = VideoColorInfo(
224229
color_primaries=ColorPrimaries.BT2020,
225230
transfer_function=TransferFunction.PQ,
226231
color_matrix=ColorMatrix.BT2020_NCL,
227232
is_hdr=True
228233
)
229-
filter_str = build_colorspace_filter(info)
230-
assert filter_str is not None
231-
assert 'colorspace' in filter_str
232-
assert 'zscale' not in filter_str
234+
with pytest.raises(RuntimeError, match='zscale'):
235+
build_colorspace_filter(info)
236+
237+
238+
@pytest.mark.skipif(shutil.which('ffmpeg') is None, reason='FFmpeg is not installed')
239+
class TestColorspaceFilterIntegration:
240+
"""Validate generated filters with a real FFmpeg process."""
241+
242+
@staticmethod
243+
def _run_filter(source_params, filter_string):
244+
source = (
245+
"testsrc2=size=64x64:rate=1,format=yuv420p,"
246+
f"setparams={source_params}"
247+
)
248+
result = subprocess.run(
249+
[
250+
'ffmpeg', '-hide_banner', '-loglevel', 'error',
251+
'-f', 'lavfi', '-i', source,
252+
'-frames:v', '1', '-vf', filter_string,
253+
'-f', 'null', '-'
254+
],
255+
capture_output=True,
256+
text=True,
257+
timeout=30,
258+
)
259+
assert result.returncode == 0, result.stderr
260+
261+
def test_display_p3_filter_executes(self):
262+
info = VideoColorInfo(
263+
color_primaries=ColorPrimaries.DISPLAY_P3,
264+
transfer_function=TransferFunction.BT709,
265+
color_matrix=ColorMatrix.BT709,
266+
is_hdr=False,
267+
)
268+
self._run_filter(
269+
'color_primaries=smpte432:color_trc=bt709:colorspace=bt709',
270+
build_colorspace_filter(info),
271+
)
272+
273+
def test_bt2020_sdr_filter_executes(self):
274+
info = VideoColorInfo(
275+
color_primaries=ColorPrimaries.BT2020,
276+
transfer_function=TransferFunction.BT709,
277+
color_matrix=ColorMatrix.BT2020_NCL,
278+
is_hdr=False,
279+
)
280+
self._run_filter(
281+
'color_primaries=bt2020:color_trc=bt709:colorspace=bt2020nc',
282+
build_colorspace_filter(info),
283+
)
284+
285+
def test_hdr_filter_executes(self):
286+
if not is_zscale_available():
287+
pytest.skip("FFmpeg's zscale filter is not available")
288+
info = VideoColorInfo(
289+
color_primaries=ColorPrimaries.BT2020,
290+
transfer_function=TransferFunction.PQ,
291+
color_matrix=ColorMatrix.BT2020_NCL,
292+
is_hdr=True,
293+
)
294+
self._run_filter(
295+
'color_primaries=bt2020:color_trc=smpte2084:colorspace=bt2020nc',
296+
build_colorspace_filter(info),
297+
)
233298

234299

235300
class TestGetColorInfoDescription:

0 commit comments

Comments
 (0)