Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix the sdr to hdr transfer function. #1

Merged
merged 2 commits into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,6 @@ dmypy.json
# emacs
\#*#
.projectile

# Jetbrain
.idea/
44 changes: 1 addition & 43 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,26 +47,7 @@
* 字幕亮度峰值,默认 100 nit

如果输出字幕太亮,可以调低。反之亦然。

* `-o --output-brightness`
* 屏幕亮度峰值,默认 1000 nit

实际只会使用字幕与屏幕亮度比例,可以无视

* `-c --colourspace`
* 输出色域,可选值为 dcip3 与 bt2020,默认bt2020

如果希望颜色更艳丽一些可以调为dcip3

* `-g --gamma`
* 调整输出时使用的oetf,可选值为pq, hlg, 和hybrid。默认pq
* 该值需要吻合视频显示时使用的OETF
* pq: 大多数视频在显示时会使用PQ 作为OETF
* hlg: 少数视频,通常来自于传统电视台,可能会使用HLG
* hybrid: 实验性功能。由于在HDR视频master时昏暗的场景通常比SDR版本更暗,
所以暗色调字幕在观看时更容易出现过亮情况。hybrid模式在低亮度时使用hlg,在高亮度时使用pq
可以一定程度上缓解这一问题。(不过这做法没任何严谨性可言恩≡ω≡...)


* `-f --file`
* 字幕文件,缺失状态下会弹出文件选择窗口。

Expand Down Expand Up @@ -121,29 +102,6 @@ The script supports some simple command line options. All arguments are optional

If the processed subtitle is too bright, decrease this value, and vice versa.

* `-o --output-brightness`
* Peak brightness for the display. Default: 1000 nit

The important part is the ratio between screen and subtitle brightness.
You can probably ignore this.

* `-c --colourspace`
* Output colourspace, value should be one of dcip3 and bt2020. Default: bt2020

Use dcip3 if you prefer slightly more saturated colours.

* `-g --gamma`
* Change the OETF used for output RGB conversion. Options values are pq, hlg,
and hybrid.
* This should match the OETF expected by the display.
* pq: most content should use the PQ curve for OETF.
* hlg: some content, likely from traditional broadcasts, might use HLG
* hybrid: experimental feature. When in HDR mastering, dark scenes tends
to be darker than the same scene in SDR. This would lead make darker subtitle
appear brighter than it should be, even after processing. Hybrid mode attempt
to solve this by using HLG for darker subtitles and PQ for brighter ones.
(and yes, this is technically wrong in many many ways)

* `-f --file`
* Subtitle files. A popup will be used if this is missing.

Expand Down
105 changes: 46 additions & 59 deletions ssa_hdrify.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,20 @@
import ass as ssa
import colour
import numpy as np
from colour.models import (RGB_COLOURSPACE_BT2020, RGB_COLOURSPACE_DCI_P3,
RGB_COLOURSPACE_sRGB)

D65_ILLUMINANT = RGB_COLOURSPACE_sRGB.whitepoint
from colour import RGB_Colourspace
from colour.models import eotf_inverse_BT2100_PQ, sRGB_to_XYZ, XYZ_to_xyY, xyY_to_XYZ, XYZ_to_RGB, \
RGB_COLOURSPACE_BT2020, eotf_BT2100_PQ

COLOURSPACE_BT2100_PQ = RGB_Colourspace(
name='COLOURSPACE_BT2100',
primaries=RGB_COLOURSPACE_BT2020.primaries,
whitepoint=RGB_COLOURSPACE_BT2020.whitepoint,
matrix_RGB_to_XYZ=RGB_COLOURSPACE_BT2020.matrix_RGB_to_XYZ,
matrix_XYZ_to_RGB=RGB_COLOURSPACE_BT2020.matrix_XYZ_to_RGB,
cctf_encoding=eotf_inverse_BT2100_PQ,
cctf_decoding=eotf_BT2100_PQ,
)
"""HDR color space on display side."""


def files_picker() -> list[str]:
Expand Down Expand Up @@ -41,39 +51,44 @@ def apply_oetf(source: list[float], luma: float):
else:
# linear mix between 0.1 and 0.2
pq_mix_ratio = (np.clip(luma, 0.1, 0.2) - 0.1) / 0.1
return hlg_result * (1-pq_mix_ratio) + pq_result * pq_mix_ratio
return hlg_result * (1 - pq_mix_ratio) + pq_result * pq_mix_ratio


def sRgbToHdr(source: tuple[int, int, int]) -> tuple[int, int, int]:
"""
大致思路:先做gamma correction,然后转入XYZ。 在xyY内将Y的极值由sRGB亮度调为输出
亮度,然后转回输出色域的RGB。
Convert RGB color in SDR color space to HDR color space.

How it works:
1. Convert the RGB color to reference xyY color space to get absolute chromaticity and linear luminance response
2. Time the target brightness of SDR color space to the Y because Rec.2100 has an absolute luminance
3. Convert the xyY color back to RGB under Rec.2100/Rec.2020 color space.

Notes:
- Unlike sRGB and Rec.709 color space which have their OOTF(E) = EOTF(OETF(E)) equals or almost equals to y = x,
it's OOTF is something close to gamma 2.4. Therefore, to have matched display color for color in SDR color space
the COLOURSPACE_BT2100_PQ denotes a display color space rather than a scene color space. It wasted me quite some
time to figure that out :(
- Option to set output luminance is removed because PQ has an absolute luminance level, which means any color in
the Rec.2100 color space will be displayed the same on any accurate display regardless of the capable peak
brightness of the device if no clipping happens. Therefore, the peak brightness should always target 10000 nits
so the SDR color can be accurately projected to the sub-range of Rec.2100 color space
args:
colour -- (0-255, 0-255, 0-255)
"""
if source == (0, 0, 0):
return (0, 0, 0)

args = parse_args()
srgb_brightness = args.sub_brightness
screen_brightness = args.output_brightness
target_colourspace = RGB_COLOURSPACE_BT2020
if args.colourspace == 'dcip3':
target_colourspace = RGB_COLOURSPACE_DCI_P3

normalized_source = np.array(source) / 255
linear_source = colour.oetf_inverse(normalized_source, 'ITU-R BT.709')
xyz = colour.RGB_to_XYZ(linear_source, RGB_COLOURSPACE_sRGB.whitepoint,
D65_ILLUMINANT,
RGB_COLOURSPACE_sRGB.matrix_RGB_to_XYZ)
xyy = colour.XYZ_to_xyY(xyz)
srgb_luma = xyy[2]
xyy[2] = xyy[2] * srgb_brightness / screen_brightness
xyz = colour.xyY_to_XYZ(xyy)
output = colour.XYZ_to_RGB(xyz, D65_ILLUMINANT,
target_colourspace.whitepoint,
target_colourspace.matrix_XYZ_to_RGB)
output = apply_oetf(output, srgb_luma)
output = np.trunc(output * 255)

normalized_sdr_color = np.array(source) / 255
xyY_sdr_color = XYZ_to_xyY(sRGB_to_XYZ(normalized_sdr_color, apply_cctf_decoding=True))

xyY_hdr_color = xyY_sdr_color.copy()
target_luminance = xyY_sdr_color[2] * srgb_brightness
xyY_hdr_color[2] = target_luminance

output = XYZ_to_RGB(xyY_to_XYZ(xyY_hdr_color), colourspace=COLOURSPACE_BT2100_PQ, apply_cctf_encoding=True)

output = np.round(output * 255)

return (int(output[0]), int(output[1]), int(output[2]))


Expand Down Expand Up @@ -139,44 +154,15 @@ def parse_args():
help=("设置字幕最大亮度,纯白色字幕将被映射为该亮度。"
"(默认: %(default)s)"),
default=100)

parser.add_argument('-o',
'--output-brightness',
metavar='val',
type=int,
help=("设置输出屏幕最大亮度。"
"(默认: %(default)s)"),
default=1000)
parser.add_argument('-c',
'--colourspace',
metavar='{dcip3,bt2020}',
type=str,
help=('选择输出的色彩空间。可用值为 dcip3 和 bt2020。'
'(默认: %(default)s)'),
default='bt2020')

parser.add_argument('-f',
'--file',
metavar='path',
type=str,
help=('输入字幕文件。可重复添加。'),
action='append')

parser.add_argument('-g',
'--gamma',
metavar='{pq,hlg,hybrid}',
type=str,
help=('选择输出时使用的gamma函数。可用值为 (默认: %(default)s)\n'
'pq: 大部分视频均使用pq压制,但hybrid字幕效果可能更好\n'
'hlg: 视频为hlg时应使用hlg模式\n'
'hybrid: 实验性。在纯pq时容易导致低亮度字幕过亮,hybrid模式在低亮度时使用hlg,'
'高亮度时使用pq。\n'),
default='pq')

args = parser.parse_args()

args.output_brightness

return args


Expand All @@ -188,4 +174,5 @@ def parse_args():
for f in files:
ssaProcessor(f)

input("按回车键退出……...")
print("Press Enter to exit...")
input("按回车键退出...")
Loading