Added HDR only encoding variant.

- Increase timeouts to improve stability of choosing export profiles.
- Added peak nits setting.
This commit is contained in:
Krzysztof Kotowicz
2024-11-25 17:14:30 +01:00
parent 72ce0b0f93
commit 95e2e4924e

View File

@@ -79,6 +79,7 @@ local GUI = {
executable_path_widget = {},
quality_widget = {},
gainmap_downsampling_widget = {},
target_display_peak_nits_widget = {}
},
options = {},
run = {}
@@ -100,6 +101,7 @@ local PS = dt.configuration.running_os == "windows" and "\\" or "/"
local ENCODING_VARIANT_SDR_AND_GAINMAP = 1
local ENCODING_VARIANT_SDR_AND_HDR = 2
local ENCODING_VARIANT_SDR_AUTO_GAINMAP = 3
local ENCODING_VARIANT_HDR_ONLY = 4
local SELECTION_TYPE_ONE_STACK = 1
local SELECTION_TYPE_GROUP_BY_FNAME = 2
@@ -146,7 +148,11 @@ local function save_preferences()
dt.preferences.write(namespace, "hdr_capacity_max", "float", GUI.optionwidgets.hdr_capacity_max.value)
end
dt.preferences.write(namespace, "quality", "integer", GUI.optionwidgets.quality_widget.value)
dt.preferences.write(namespace, "gainmap_downsampling", "integer", GUI.optionwidgets.gainmap_downsampling_widget.value)
dt.preferences.write(namespace, "gainmap_downsampling", "integer",
GUI.optionwidgets.gainmap_downsampling_widget.value)
dt.preferences.write(namespace, "target_display_peak_nits", "integer",
(GUI.optionwidgets.target_display_peak_nits_widget.value+0.5)//1)
end
local function default_to(value, default)
@@ -179,7 +185,10 @@ local function load_preferences()
GUI.optionwidgets.hdr_capacity_max.value = default_to(dt.preferences.read(namespace, "hdr_capacity_max", "float"),
6.0)
GUI.optionwidgets.quality_widget.value = default_to(dt.preferences.read(namespace, "quality", "integer"), 95)
GUI.optionwidgets.gainmap_downsampling_widget.value = default_to(dt.preferences.read(namespace, "gainmap_downsampling", "integer"), 0)
GUI.optionwidgets.target_display_peak_nits_widget.value = default_to(
dt.preferences.read(namespace, "target_display_peak_nits", "integer"), 10000)
GUI.optionwidgets.gainmap_downsampling_widget.value = default_to(
dt.preferences.read(namespace, "gainmap_downsampling", "integer"), 0)
end
-- Changes the combobox selection blindly until a paired config value is set.
@@ -192,11 +201,12 @@ local function set_combobox(path, instance, config_name, new_config_value)
end
dt.gui.action(path, 0, "selection", "first", 1.0)
dt.control.sleep(50)
local limit, i = 30, 0 -- in case there is no matching config value in the first n entries of a combobox.
while i < limit do
i = i + 1
dt.gui.action(path, 0, "selection", "next", 1.0)
dt.control.sleep(10)
dt.control.sleep(50)
if dt.preferences.read("darktable", config_name, "integer") == new_config_value then
log.msg(log.debug, string.format(_("Changed %s from %d to %d"), config_name, pref, new_config_value))
return pref
@@ -224,7 +234,8 @@ local function assert_settings_correct(encoding_variant)
hdr_capacity_max = GUI.optionwidgets.hdr_capacity_max.value
},
quality = GUI.optionwidgets.quality_widget.value,
downsample = 2^GUI.optionwidgets.gainmap_downsampling_widget.value,
target_display_peak_nits = (GUI.optionwidgets.target_display_peak_nits_widget.value+0.5)//1,
downsample = 2 ^ GUI.optionwidgets.gainmap_downsampling_widget.value,
tmpdir = dt.configuration.tmp_dir,
skip_cleanup = false -- keep temporary files around, for debugging.
}
@@ -263,23 +274,27 @@ end
local function get_stacks(images, encoding_variant, selection_type)
local stacks = {}
local extra_image_content_type
local primary = "sdr"
local extra
if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP then
extra_image_content_type = "gainmap"
extra = "gainmap"
elseif encoding_variant == ENCODING_VARIANT_SDR_AND_HDR then
extra_image_content_type = "hdr"
extra = "hdr"
elseif encoding_variant == ENCODING_VARIANT_SDR_AUTO_GAINMAP then
extra_image_content_type = nil
extra = nil
elseif encoding_variant == ENCODING_VARIANT_HDR_ONLY then
extra = nil
primary = "hdr"
end
local tags = nil
-- Group images into (sdr [,extra]) stacks
-- Assume that the first encountered image from each stack is an sdr one, unless it has a tag matching the expected extra_image_type, or has the expected extension
-- Group images into (primary [,extra]) stacks
-- Assume that the first encountered image from each stack is a primary one, unless it has a tag matching the expected extra_image_type, or has the expected extension
for k, v in pairs(images) do
local is_extra = false
tags = dt.tags.get_tags(v)
for ignore, tag in pairs(tags) do
if extra_image_content_type and tag.name == extra_image_content_type then
if extra and tag.name == extra then
is_extra = true
end
end
@@ -296,27 +311,27 @@ local function get_stacks(images, encoding_variant, selection_type)
if stacks[key] == nil then
stacks[key] = {}
end
if extra_image_content_type and (is_extra or stacks[key]["sdr"]) then
if extra and (is_extra or stacks[key][primary]) then
-- Don't overwrite existing entries
if not stacks[key][extra_image_content_type] then
stacks[key][extra_image_content_type] = v
if not stacks[key][extra] then
stacks[key][extra] = v
end
elseif not is_extra then
-- Don't overwrite existing entries
if not stacks[key]["sdr"] then
stacks[key]["sdr"] = v
if not stacks[key][primary] then
stacks[key][primary] = v
end
end
end
-- remove invalid stacks
local count = 0
for k, v in pairs(stacks) do
if extra_image_content_type then
if not v["sdr"] or not v[extra_image_content_type] then
if extra then
if not v[primary] or not v[extra] then
stacks[k] = nil
else
local sdr_w, sdr_h = get_dimensions(v["sdr"])
local extra_w, extra_h = get_dimensions(v[extra_image_content_type])
local sdr_w, sdr_h = get_dimensions(v[primary])
local extra_w, extra_h = get_dimensions(v[extra])
if (sdr_w ~= extra_w) or (sdr_h ~= extra_h) then
stacks[k] = nil
end
@@ -346,6 +361,7 @@ end
local function generate_ultrahdr(encoding_variant, images, settings, step, total_steps)
local total_substeps
local substep = 0
local best_source_image
local uhdr
local errors = {}
local remove_files = {}
@@ -408,6 +424,7 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total
if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP or encoding_variant == ENCODING_VARIANT_SDR_AUTO_GAINMAP then
total_substeps = 5
best_source_image = images["sdr"]
-- Export/copy both SDR and gainmap to JPEGs
local sdr = df.create_unique_filename(settings.tmpdir .. PS .. df.chop_filetype(images["sdr"].filename) ..
".jpg")
@@ -456,9 +473,13 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total
-- Merge files
uhdr = df.chop_filetype(sdr) .. "_ultrahdr.jpg"
table.insert(remove_files, uhdr)
cmd = settings.bin.ultrahdr_app .. " -m 0 -i " .. df.sanitize_filename(sdr .. ".noexif") .. " -g " ..
df.sanitize_filename(gainmap) .. " -f " .. df.sanitize_filename(metadata_file) .. " -z " ..
df.sanitize_filename(uhdr)
cmd = settings.bin.ultrahdr_app ..
string.format(" -m 0 -i %s -g %s -L %d -f %s -z %s", df.sanitize_filename(sdr .. ".noexif"), -- -i
df.sanitize_filename(gainmap), -- -g
settings.target_display_peak_nits, -- -L
df.sanitize_filename(metadata_file), -- -f
df.sanitize_filename(uhdr) -- -z
)
if not execute_cmd(cmd, string.format(_("Error merging UltraHDR to %s"), uhdr)) then
return cleanup(), errors
end
@@ -476,6 +497,7 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total
update_job_progress()
elseif encoding_variant == ENCODING_VARIANT_SDR_AND_HDR then
total_substeps = 6
best_source_image = images["sdr"]
-- https://discuss.pixls.us/t/manual-creation-of-ultrahdr-images/45004/20
-- Step 1: Export HDR to JPEG-XL with DT_COLORSPACE_PQ_P3
local hdr = df.create_unique_filename(settings.tmpdir .. PS .. df.chop_filetype(images["hdr"].filename) ..
@@ -528,15 +550,26 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total
end
-- sanity check for file sizes (sometimes dt exports different size images if the files were never opened in darktable view)
if file_size(sdr_raw) ~= size_in_px * 4 or file_size(hdr_raw) ~= size_in_px * 3 then
table.insert(errors, string.format(_("Wrong raw image resolution: %s, expected %dx%d. Try opening the image in darktable mode first."), images["sdr"].filename, sdr_w, sdr_h))
table.insert(errors,
string.format(
_("Wrong raw image resolution: %s, expected %dx%d. Try opening the image in darktable mode first."),
images["sdr"].filename, sdr_w, sdr_h))
return cleanup(), errors
end
update_job_progress()
cmd = settings.bin.ultrahdr_app .. " -m 0 -y " .. df.sanitize_filename(sdr_raw) .. " -p " ..
df.sanitize_filename(hdr_raw) ..
string.format(" -a 0 -b 3 -c 1 -C 1 -t 2 -M 0 -s 1 -q %d -Q %d -D 1 ", settings.quality,
settings.quality) .. string.format(" -s %d ", settings.downsample) .. " -w " .. tostring(sdr_w - sdr_w % 2) .. " -h " .. tostring(sdr_h - sdr_h % 2) ..
" -z " .. df.sanitize_filename(uhdr)
cmd = settings.bin.ultrahdr_app ..
string.format(
" -m 0 -y %s -p %s -a 0 -b 3 -c 1 -C 1 -t 2 -M 0 -q %d -Q %d -L %d -D 1 -s %d -w %d -h %d -z %s",
df.sanitize_filename(sdr_raw), -- -y
df.sanitize_filename(hdr_raw), -- -p
settings.quality, -- -q
settings.quality, -- -Q
settings.target_display_peak_nits, -- -L
settings.downsample, -- -s
sdr_w - sdr_w % 2, -- w
sdr_h - sdr_h % 2, -- h
df.sanitize_filename(uhdr) -- z
)
if not execute_cmd(cmd, string.format(_("Error merging %s"), uhdr)) then
return cleanup(), errors
end
@@ -551,13 +584,82 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total
end
end
update_job_progress()
elseif encoding_variant == ENCODING_VARIANT_HDR_ONLY then
total_substeps = 5
best_source_image = images["hdr"]
-- TODO: Check if exporting to JXL would be ok too.
-- Step 1: Export HDR to JPEG-XL with DT_COLORSPACE_PQ_P3
local hdr = df.create_unique_filename(settings.tmpdir .. PS .. df.chop_filetype(images["hdr"].filename) ..
".jxl")
table.insert(remove_files, hdr)
ok = copy_or_export(images["hdr"], hdr, "jpegxl", DT_COLORSPACE_PQ_P3, {
bpp = 10,
quality = 100, -- lossless
effort = 1 -- we don't care about the size, the file is temporary.
})
if not ok then
table.insert(errors, string.format(_("Error exporting %s to %s"), images["hdr"].filename, "jxl"))
return cleanup(), errors
end
update_job_progress()
-- Step 1: Generate raw HDR image
local hdr_raw = df.create_unique_filename(settings.tmpdir .. PS .. df.chop_filetype(images["hdr"].filename) ..
".raw")
table.insert(remove_files, hdr_raw)
local hdr_w, hdr_h = get_dimensions(images["hdr"])
local resize_cmd = ""
if hdr_h % 2 + hdr_w % 2 > 0 then -- needs resizing to even dimensions.
resize_cmd = string.format(" -vf 'crop=%d:%d:0:0' ", hdr_w - hdr_w % 2, hdr_h - hdr_h % 2)
end
local size_in_px = (hdr_w - hdr_w % 2) * (hdr_h - hdr_h % 2)
cmd = settings.bin.ffmpeg .. " -i " .. df.sanitize_filename(hdr) .. resize_cmd ..
" -pix_fmt p010le -f rawvideo " .. df.sanitize_filename(hdr_raw)
if not execute_cmd(cmd, string.format(_("Error generating %s"), hdr_raw)) then
return cleanup(), errors
end
if file_size(hdr_raw) ~= size_in_px * 3 then
table.insert(errors,
string.format(
_("Wrong raw image resolution: %s, expected %dx%d. Try opening the image in darktable mode first."),
images["hdr"].filename, hdr_w, hdr_h))
return cleanup(), errors
end
update_job_progress()
uhdr = df.chop_filetype(hdr_raw) .. "_ultrahdr.jpg"
table.insert(remove_files, uhdr)
cmd = settings.bin.ultrahdr_app ..
string.format(
" -m 0 -p %s -a 0 -b 3 -c 1 -C 1 -t 2 -M 0 -q %d -Q %d -D 1 -L %d -s %d -w %d -h %d -z %s",
df.sanitize_filename(hdr_raw), -- -p
settings.quality, -- -q
settings.quality, -- -Q
settings.target_display_peak_nits, -- -L
settings.downsample, -- s
hdr_w - hdr_w % 2, -- -w
hdr_h - hdr_h % 2, -- -h
df.sanitize_filename(uhdr) -- -z
)
if not execute_cmd(cmd, string.format(_("Error merging %s"), uhdr)) then
return cleanup(), errors
end
update_job_progress()
if settings.copy_exif then
-- Restricting tags to EXIF only, to make sure we won't mess up XMP tags (-all>all).
-- This might hapen e.g. when the source files are Adobe gainmap HDRs.
cmd = settings.bin.exiftool .. " -tagsfromfile " .. df.sanitize_filename(hdr) .. " -exif " ..
df.sanitize_filename(uhdr) .. " -overwrite_original -preserve"
if not execute_cmd(cmd, string.format(_("Error adding EXIF to %s"), uhdr)) then
return cleanup(), errors
end
end
update_job_progress()
end
local output_dir = settings.use_original_dir and images["sdr"].path or settings.output
local output_dir = settings.use_original_dir and best_source_image.path or settings.output
local output_file = df.create_unique_filename(output_dir .. PS .. df.get_filename(uhdr))
ok = df.file_move(uhdr, output_file)
if not ok then
table.insert(errors, string.format(_("Error generating UltraHDR for %s"), images["sdr"].filename))
table.insert(errors, string.format(_("Error generating UltraHDR for %s"), best_source_image.filename))
return cleanup(), errors
end
if settings.import_to_darktable then
@@ -593,7 +695,9 @@ local function main()
local stacks, stack_count = get_stacks(dt.gui.selection(), encoding_variant, selection_type)
if stack_count == 0 then
dt.print(string.format(_("No image stacks detected.\n\nMake sure that the image pairs have the same widths and heights."), stack_count))
dt.print(string.format(_(
"No image stacks detected.\n\nMake sure that the image pairs have the same widths and heights."),
stack_count))
return
end
dt.print(string.format(_("Detected %d image stack(s)"), stack_count))
@@ -737,10 +841,11 @@ This will determine the method used to generate UltraHDR.
- %s: SDR image paired with a gain map image.
- %s: SDR image paired with an HDR image.
- %s: Each stack consists of a single SDR image. Gain maps will be copies of SDR images.
- %s: Each stack consists of a single HDR image. HDR will be tone mapped to SDR.
By default, the first image in a stack is treated as SDR, and the second one is a gain map/HDR.
You can force the image into a specific stack slot by attaching "hdr" / "gainmap" tags to it.
]]), _("SDR + gain map"), _("SDR + HDR"), _("SDR only")),
]]), _("SDR + gain map"), _("SDR + HDR"), _("SDR only"), _("HDR only")),
selected = 0,
changed_callback = function(self)
GUI.run.sensitive = self.selected and self.selected > 0
@@ -754,7 +859,8 @@ You can force the image into a specific stack slot by attaching "hdr" / "gainmap
end,
_("SDR + gain map"), -- ENCODING_VARIANT_SDR_AND_GAINMAP
_("SDR + HDR"), -- ENCODING_VARIANT_SDR_AND_HDR
_("SDR only") -- ENCODING_VARIANT_SDR_AUTO_GAINMAP
_("SDR only"), -- ENCODING_VARIANT_SDR_AUTO_GAINMAP
_("HDR only") -- ENCODING_VARIANT_HDR_ONLY
}
GUI.optionwidgets.selection_type_combo = dt.new_widget("combobox") {
@@ -787,9 +893,24 @@ GUI.optionwidgets.quality_widget = dt.new_widget("slider") {
end
}
GUI.optionwidgets.target_display_peak_nits_widget = dt.new_widget("slider") {
label = _('target display peak brightness (nits)'),
tooltip = _('Peak brightness of target display in nits (defaults to 10000)'),
hard_min = 203,
hard_max = 10000,
soft_min = 1000,
soft_max = 10000,
step = 10,
digits = 0,
reset_callback = function(self)
self.value = 10000
end
}
GUI.optionwidgets.gainmap_downsampling_widget = dt.new_widget("slider") {
label = _('gain map downsampling steps'),
tooltip = _('Exponent (2^x) of the gain map downsampling factor.\nDownsampling reduces the gain map resolution.\n\n0 = don\'t downsample the gain map, 7 = maximum downsampling (128x)'),
tooltip = _(
'Exponent (2^x) of the gain map downsampling factor.\nDownsampling reduces the gain map resolution.\n\n0 = don\'t downsample the gain map, 7 = maximum downsampling (128x)'),
hard_min = 0,
hard_max = 7,
soft_min = 0,
@@ -807,6 +928,7 @@ GUI.optionwidgets.encoding_settings_box = dt.new_widget("box") {
GUI.optionwidgets.encoding_variant_combo,
GUI.optionwidgets.quality_widget,
GUI.optionwidgets.gainmap_downsampling_widget,
GUI.optionwidgets.target_display_peak_nits_widget,
GUI.optionwidgets.metadata_box
}