Added HDR only encoding variant.
- Increase timeouts to improve stability of choosing export profiles. - Added peak nits setting.
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user