mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-06-10 03:02:09 +00:00
fix(hybrid): correct static L6 source and reset stale L5 active area
level_6() read MaxCLL/MaxFALL from the dovi_tool L1 line (dynamic per-shot content-light peak) and baked it into the static L6 field, so displays tone-map to a phantom peak and HDR brightness breaks. Read MaxCLL/MaxFALL from the static L6 block instead (mastering display parse unchanged), falling back to the HDR10 base stream only when the RPU has no L6 block. Add sanitize_l6() to clamp MaxCLL to the mastering-display peak and MaxFALL to MaxCLL (0 = "unknown" preserved); also applied to the HDR10+ -> DV path. level_5() early-returned when cropdetect found no bars on the base, leaving whatever L5 offsets the DV source carried. When DV and HDR10 geometry differ (e.g. a letterboxed DV injected into an already-cropped base) the stale offsets rode through and signalled phantom bars. Always write the detected active area, including zeros, so stale source L5 resets to the full base frame.
This commit is contained in:
@@ -6,6 +6,7 @@ import re
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from rich.padding import Padding
|
from rich.padding import Padding
|
||||||
from rich.rule import Rule
|
from rich.rule import Rule
|
||||||
@@ -341,11 +342,7 @@ class Hybrid:
|
|||||||
for crop in crop_results:
|
for crop in crop_results:
|
||||||
crop_counts[crop] = crop_counts.get(crop, 0) + 1
|
crop_counts[crop] = crop_counts.get(crop, 0) + 1
|
||||||
most_common = max(crop_counts, key=crop_counts.get)
|
most_common = max(crop_counts, key=crop_counts.get)
|
||||||
left, top, right, bottom = most_common
|
left, top, right, bottom = most_common # frame instead of leaving phantom bars from the source.
|
||||||
|
|
||||||
# If all borders are 0 there's nothing to correct
|
|
||||||
if left == 0 and top == 0 and right == 0 and bottom == 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
l5_json = {
|
l5_json = {
|
||||||
"active_area": {
|
"active_area": {
|
||||||
@@ -390,8 +387,24 @@ class Hybrid:
|
|||||||
)
|
)
|
||||||
self.rpu_file = "RPU_L5.bin"
|
self.rpu_file = "RPU_L5.bin"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def sanitize_l6(
|
||||||
|
max_mdl: Optional[int], min_mdl: Optional[int], max_cll: Optional[int], max_fall: Optional[int]
|
||||||
|
) -> tuple[Optional[int], Optional[int], Optional[int], Optional[int]]:
|
||||||
|
"""Clamp static L6 values to a valid relationship.
|
||||||
|
|
||||||
|
MaxCLL must not exceed the mastering-display peak (some sources, e.g. ATV
|
||||||
|
HDR10+, ship MaxCLL 10000 on a 1000-nit master), and MaxFALL must not exceed
|
||||||
|
MaxCLL. A value of 0 means "unknown" and is preserved as-is.
|
||||||
|
"""
|
||||||
|
if max_mdl and max_cll and max_cll > max_mdl:
|
||||||
|
max_cll = max_mdl
|
||||||
|
if max_cll and max_fall and max_fall > max_cll:
|
||||||
|
max_fall = max_cll
|
||||||
|
return max_mdl, min_mdl, max_cll, max_fall
|
||||||
|
|
||||||
def level_6(self):
|
def level_6(self):
|
||||||
"""Edit RPU Level 6 values using actual luminance data from the RPU."""
|
"""Edit RPU Level 6 values using the static L6 luminance data from the RPU."""
|
||||||
if os.path.isfile(config.directories.temp / "RPU_L6.bin"):
|
if os.path.isfile(config.directories.temp / "RPU_L6.bin"):
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -413,17 +426,33 @@ class Hybrid:
|
|||||||
max_mdl = None
|
max_mdl = None
|
||||||
min_mdl = None
|
min_mdl = None
|
||||||
|
|
||||||
|
in_l6 = False
|
||||||
for line in info_text.splitlines():
|
for line in info_text.splitlines():
|
||||||
if "RPU content light level (L1):" in line:
|
stripped = line.strip()
|
||||||
parts = line.split("MaxCLL:")[1].split(",")
|
if "L6 metadata" in stripped:
|
||||||
max_cll = int(float(parts[0].strip().split()[0]))
|
in_l6 = True
|
||||||
if len(parts) > 1 and "MaxFALL:" in parts[1]:
|
if stripped.startswith("RPU mastering display:"):
|
||||||
max_fall = int(float(parts[1].split("MaxFALL:")[1].strip().split()[0]))
|
mastering = stripped.split(":", 1)[1].strip()
|
||||||
elif "RPU mastering display:" in line:
|
|
||||||
mastering = line.split(":", 1)[1].strip()
|
|
||||||
min_lum, max_lum = mastering.split("/")[0], mastering.split("/")[1].split(" ")[0]
|
min_lum, max_lum = mastering.split("/")[0], mastering.split("/")[1].split(" ")[0]
|
||||||
min_mdl = int(float(min_lum) * 10000)
|
min_mdl = int(float(min_lum) * 10000)
|
||||||
max_mdl = int(float(max_lum))
|
max_mdl = int(float(max_lum))
|
||||||
|
elif in_l6 and "MaxCLL:" in stripped and max_cll is None:
|
||||||
|
max_cll = int(float(stripped.split("MaxCLL:")[1].split("nits")[0].strip().rstrip(",")))
|
||||||
|
if "MaxFALL:" in stripped:
|
||||||
|
max_fall = int(float(stripped.split("MaxFALL:")[1].split("nits")[0].strip().rstrip(",")))
|
||||||
|
|
||||||
|
if any(v is None for v in (max_cll, max_fall, max_mdl, min_mdl)):
|
||||||
|
base_max_mdl, base_min_mdl, base_cll, base_fall = self._probe_hdr_metadata()
|
||||||
|
if max_cll is None:
|
||||||
|
max_cll = base_cll
|
||||||
|
if max_fall is None:
|
||||||
|
max_fall = base_fall
|
||||||
|
if max_mdl is None:
|
||||||
|
max_mdl = base_max_mdl
|
||||||
|
if min_mdl is None:
|
||||||
|
min_mdl = base_min_mdl
|
||||||
|
|
||||||
|
max_mdl, min_mdl, max_cll, max_fall = self.sanitize_l6(max_mdl, min_mdl, max_cll, max_fall)
|
||||||
|
|
||||||
if any(v is None for v in (max_cll, max_fall, max_mdl, min_mdl)):
|
if any(v is None for v in (max_cll, max_fall, max_mdl, min_mdl)):
|
||||||
if self.debug_logger:
|
if self.debug_logger:
|
||||||
@@ -638,6 +667,7 @@ class Hybrid:
|
|||||||
with console.status("Converting HDR10+ metadata to Dolby Vision...", spinner="dots"):
|
with console.status("Converting HDR10+ metadata to Dolby Vision...", spinner="dots"):
|
||||||
# Extract actual HDR metadata from the source stream
|
# Extract actual HDR metadata from the source stream
|
||||||
max_mdl, min_mdl, max_cll, max_fall = self._probe_hdr_metadata()
|
max_mdl, min_mdl, max_cll, max_fall = self._probe_hdr_metadata()
|
||||||
|
max_mdl, min_mdl, max_cll, max_fall = self.sanitize_l6(max_mdl, min_mdl, max_cll, max_fall)
|
||||||
|
|
||||||
# First create the extra metadata JSON for dovi_tool
|
# First create the extra metadata JSON for dovi_tool
|
||||||
extra_metadata = {
|
extra_metadata = {
|
||||||
|
|||||||
Reference in New Issue
Block a user