Skip to content

Commit 95e2e49

Browse files
committed
Added HDR only encoding variant.
- Increase timeouts to improve stability of choosing export profiles. - Added peak nits setting.
1 parent 72ce0b0 commit 95e2e49

File tree

1 file changed

+157
-35
lines changed

1 file changed

+157
-35
lines changed

contrib/ultrahdr.lua

Lines changed: 157 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ local GUI = {
7979
executable_path_widget = {},
8080
quality_widget = {},
8181
gainmap_downsampling_widget = {},
82+
target_display_peak_nits_widget = {}
8283
},
8384
options = {},
8485
run = {}
@@ -100,6 +101,7 @@ local PS = dt.configuration.running_os == "windows" and "\\" or "/"
100101
local ENCODING_VARIANT_SDR_AND_GAINMAP = 1
101102
local ENCODING_VARIANT_SDR_AND_HDR = 2
102103
local ENCODING_VARIANT_SDR_AUTO_GAINMAP = 3
104+
local ENCODING_VARIANT_HDR_ONLY = 4
103105

104106
local SELECTION_TYPE_ONE_STACK = 1
105107
local SELECTION_TYPE_GROUP_BY_FNAME = 2
@@ -146,7 +148,11 @@ local function save_preferences()
146148
dt.preferences.write(namespace, "hdr_capacity_max", "float", GUI.optionwidgets.hdr_capacity_max.value)
147149
end
148150
dt.preferences.write(namespace, "quality", "integer", GUI.optionwidgets.quality_widget.value)
149-
dt.preferences.write(namespace, "gainmap_downsampling", "integer", GUI.optionwidgets.gainmap_downsampling_widget.value)
151+
dt.preferences.write(namespace, "gainmap_downsampling", "integer",
152+
GUI.optionwidgets.gainmap_downsampling_widget.value)
153+
dt.preferences.write(namespace, "target_display_peak_nits", "integer",
154+
(GUI.optionwidgets.target_display_peak_nits_widget.value+0.5)//1)
155+
150156
end
151157

152158
local function default_to(value, default)
@@ -179,7 +185,10 @@ local function load_preferences()
179185
GUI.optionwidgets.hdr_capacity_max.value = default_to(dt.preferences.read(namespace, "hdr_capacity_max", "float"),
180186
6.0)
181187
GUI.optionwidgets.quality_widget.value = default_to(dt.preferences.read(namespace, "quality", "integer"), 95)
182-
GUI.optionwidgets.gainmap_downsampling_widget.value = default_to(dt.preferences.read(namespace, "gainmap_downsampling", "integer"), 0)
188+
GUI.optionwidgets.target_display_peak_nits_widget.value = default_to(
189+
dt.preferences.read(namespace, "target_display_peak_nits", "integer"), 10000)
190+
GUI.optionwidgets.gainmap_downsampling_widget.value = default_to(
191+
dt.preferences.read(namespace, "gainmap_downsampling", "integer"), 0)
183192
end
184193

185194
-- 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)
192201
end
193202

194203
dt.gui.action(path, 0, "selection", "first", 1.0)
204+
dt.control.sleep(50)
195205
local limit, i = 30, 0 -- in case there is no matching config value in the first n entries of a combobox.
196206
while i < limit do
197207
i = i + 1
198208
dt.gui.action(path, 0, "selection", "next", 1.0)
199-
dt.control.sleep(10)
209+
dt.control.sleep(50)
200210
if dt.preferences.read("darktable", config_name, "integer") == new_config_value then
201211
log.msg(log.debug, string.format(_("Changed %s from %d to %d"), config_name, pref, new_config_value))
202212
return pref
@@ -224,7 +234,8 @@ local function assert_settings_correct(encoding_variant)
224234
hdr_capacity_max = GUI.optionwidgets.hdr_capacity_max.value
225235
},
226236
quality = GUI.optionwidgets.quality_widget.value,
227-
downsample = 2^GUI.optionwidgets.gainmap_downsampling_widget.value,
237+
target_display_peak_nits = (GUI.optionwidgets.target_display_peak_nits_widget.value+0.5)//1,
238+
downsample = 2 ^ GUI.optionwidgets.gainmap_downsampling_widget.value,
228239
tmpdir = dt.configuration.tmp_dir,
229240
skip_cleanup = false -- keep temporary files around, for debugging.
230241
}
@@ -263,23 +274,27 @@ end
263274

264275
local function get_stacks(images, encoding_variant, selection_type)
265276
local stacks = {}
266-
local extra_image_content_type
277+
local primary = "sdr"
278+
local extra
267279
if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP then
268-
extra_image_content_type = "gainmap"
280+
extra = "gainmap"
269281
elseif encoding_variant == ENCODING_VARIANT_SDR_AND_HDR then
270-
extra_image_content_type = "hdr"
282+
extra = "hdr"
271283
elseif encoding_variant == ENCODING_VARIANT_SDR_AUTO_GAINMAP then
272-
extra_image_content_type = nil
284+
extra = nil
285+
elseif encoding_variant == ENCODING_VARIANT_HDR_ONLY then
286+
extra = nil
287+
primary = "hdr"
273288
end
274289

275290
local tags = nil
276-
-- Group images into (sdr [,extra]) stacks
277-
-- 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
291+
-- Group images into (primary [,extra]) stacks
292+
-- 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
278293
for k, v in pairs(images) do
279294
local is_extra = false
280295
tags = dt.tags.get_tags(v)
281296
for ignore, tag in pairs(tags) do
282-
if extra_image_content_type and tag.name == extra_image_content_type then
297+
if extra and tag.name == extra then
283298
is_extra = true
284299
end
285300
end
@@ -296,27 +311,27 @@ local function get_stacks(images, encoding_variant, selection_type)
296311
if stacks[key] == nil then
297312
stacks[key] = {}
298313
end
299-
if extra_image_content_type and (is_extra or stacks[key]["sdr"]) then
314+
if extra and (is_extra or stacks[key][primary]) then
300315
-- Don't overwrite existing entries
301-
if not stacks[key][extra_image_content_type] then
302-
stacks[key][extra_image_content_type] = v
316+
if not stacks[key][extra] then
317+
stacks[key][extra] = v
303318
end
304319
elseif not is_extra then
305320
-- Don't overwrite existing entries
306-
if not stacks[key]["sdr"] then
307-
stacks[key]["sdr"] = v
321+
if not stacks[key][primary] then
322+
stacks[key][primary] = v
308323
end
309324
end
310325
end
311326
-- remove invalid stacks
312327
local count = 0
313328
for k, v in pairs(stacks) do
314-
if extra_image_content_type then
315-
if not v["sdr"] or not v[extra_image_content_type] then
329+
if extra then
330+
if not v[primary] or not v[extra] then
316331
stacks[k] = nil
317332
else
318-
local sdr_w, sdr_h = get_dimensions(v["sdr"])
319-
local extra_w, extra_h = get_dimensions(v[extra_image_content_type])
333+
local sdr_w, sdr_h = get_dimensions(v[primary])
334+
local extra_w, extra_h = get_dimensions(v[extra])
320335
if (sdr_w ~= extra_w) or (sdr_h ~= extra_h) then
321336
stacks[k] = nil
322337
end
@@ -346,6 +361,7 @@ end
346361
local function generate_ultrahdr(encoding_variant, images, settings, step, total_steps)
347362
local total_substeps
348363
local substep = 0
364+
local best_source_image
349365
local uhdr
350366
local errors = {}
351367
local remove_files = {}
@@ -408,6 +424,7 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total
408424

409425
if encoding_variant == ENCODING_VARIANT_SDR_AND_GAINMAP or encoding_variant == ENCODING_VARIANT_SDR_AUTO_GAINMAP then
410426
total_substeps = 5
427+
best_source_image = images["sdr"]
411428
-- Export/copy both SDR and gainmap to JPEGs
412429
local sdr = df.create_unique_filename(settings.tmpdir .. PS .. df.chop_filetype(images["sdr"].filename) ..
413430
".jpg")
@@ -456,9 +473,13 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total
456473
-- Merge files
457474
uhdr = df.chop_filetype(sdr) .. "_ultrahdr.jpg"
458475
table.insert(remove_files, uhdr)
459-
cmd = settings.bin.ultrahdr_app .. " -m 0 -i " .. df.sanitize_filename(sdr .. ".noexif") .. " -g " ..
460-
df.sanitize_filename(gainmap) .. " -f " .. df.sanitize_filename(metadata_file) .. " -z " ..
461-
df.sanitize_filename(uhdr)
476+
cmd = settings.bin.ultrahdr_app ..
477+
string.format(" -m 0 -i %s -g %s -L %d -f %s -z %s", df.sanitize_filename(sdr .. ".noexif"), -- -i
478+
df.sanitize_filename(gainmap), -- -g
479+
settings.target_display_peak_nits, -- -L
480+
df.sanitize_filename(metadata_file), -- -f
481+
df.sanitize_filename(uhdr) -- -z
482+
)
462483
if not execute_cmd(cmd, string.format(_("Error merging UltraHDR to %s"), uhdr)) then
463484
return cleanup(), errors
464485
end
@@ -476,6 +497,7 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total
476497
update_job_progress()
477498
elseif encoding_variant == ENCODING_VARIANT_SDR_AND_HDR then
478499
total_substeps = 6
500+
best_source_image = images["sdr"]
479501
-- https://discuss.pixls.us/t/manual-creation-of-ultrahdr-images/45004/20
480502
-- Step 1: Export HDR to JPEG-XL with DT_COLORSPACE_PQ_P3
481503
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
528550
end
529551
-- sanity check for file sizes (sometimes dt exports different size images if the files were never opened in darktable view)
530552
if file_size(sdr_raw) ~= size_in_px * 4 or file_size(hdr_raw) ~= size_in_px * 3 then
531-
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))
553+
table.insert(errors,
554+
string.format(
555+
_("Wrong raw image resolution: %s, expected %dx%d. Try opening the image in darktable mode first."),
556+
images["sdr"].filename, sdr_w, sdr_h))
532557
return cleanup(), errors
533558
end
534559
update_job_progress()
535-
cmd = settings.bin.ultrahdr_app .. " -m 0 -y " .. df.sanitize_filename(sdr_raw) .. " -p " ..
536-
df.sanitize_filename(hdr_raw) ..
537-
string.format(" -a 0 -b 3 -c 1 -C 1 -t 2 -M 0 -s 1 -q %d -Q %d -D 1 ", settings.quality,
538-
settings.quality) .. string.format(" -s %d ", settings.downsample) .. " -w " .. tostring(sdr_w - sdr_w % 2) .. " -h " .. tostring(sdr_h - sdr_h % 2) ..
539-
" -z " .. df.sanitize_filename(uhdr)
560+
cmd = settings.bin.ultrahdr_app ..
561+
string.format(
562+
" -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",
563+
df.sanitize_filename(sdr_raw), -- -y
564+
df.sanitize_filename(hdr_raw), -- -p
565+
settings.quality, -- -q
566+
settings.quality, -- -Q
567+
settings.target_display_peak_nits, -- -L
568+
settings.downsample, -- -s
569+
sdr_w - sdr_w % 2, -- w
570+
sdr_h - sdr_h % 2, -- h
571+
df.sanitize_filename(uhdr) -- z
572+
)
540573
if not execute_cmd(cmd, string.format(_("Error merging %s"), uhdr)) then
541574
return cleanup(), errors
542575
end
@@ -551,13 +584,82 @@ local function generate_ultrahdr(encoding_variant, images, settings, step, total
551584
end
552585
end
553586
update_job_progress()
587+
elseif encoding_variant == ENCODING_VARIANT_HDR_ONLY then
588+
total_substeps = 5
589+
best_source_image = images["hdr"]
590+
-- TODO: Check if exporting to JXL would be ok too.
591+
-- Step 1: Export HDR to JPEG-XL with DT_COLORSPACE_PQ_P3
592+
local hdr = df.create_unique_filename(settings.tmpdir .. PS .. df.chop_filetype(images["hdr"].filename) ..
593+
".jxl")
594+
table.insert(remove_files, hdr)
595+
ok = copy_or_export(images["hdr"], hdr, "jpegxl", DT_COLORSPACE_PQ_P3, {
596+
bpp = 10,
597+
quality = 100, -- lossless
598+
effort = 1 -- we don't care about the size, the file is temporary.
599+
})
600+
if not ok then
601+
table.insert(errors, string.format(_("Error exporting %s to %s"), images["hdr"].filename, "jxl"))
602+
return cleanup(), errors
603+
end
604+
update_job_progress()
605+
-- Step 1: Generate raw HDR image
606+
local hdr_raw = df.create_unique_filename(settings.tmpdir .. PS .. df.chop_filetype(images["hdr"].filename) ..
607+
".raw")
608+
table.insert(remove_files, hdr_raw)
609+
local hdr_w, hdr_h = get_dimensions(images["hdr"])
610+
local resize_cmd = ""
611+
if hdr_h % 2 + hdr_w % 2 > 0 then -- needs resizing to even dimensions.
612+
resize_cmd = string.format(" -vf 'crop=%d:%d:0:0' ", hdr_w - hdr_w % 2, hdr_h - hdr_h % 2)
613+
end
614+
local size_in_px = (hdr_w - hdr_w % 2) * (hdr_h - hdr_h % 2)
615+
cmd = settings.bin.ffmpeg .. " -i " .. df.sanitize_filename(hdr) .. resize_cmd ..
616+
" -pix_fmt p010le -f rawvideo " .. df.sanitize_filename(hdr_raw)
617+
if not execute_cmd(cmd, string.format(_("Error generating %s"), hdr_raw)) then
618+
return cleanup(), errors
619+
end
620+
if file_size(hdr_raw) ~= size_in_px * 3 then
621+
table.insert(errors,
622+
string.format(
623+
_("Wrong raw image resolution: %s, expected %dx%d. Try opening the image in darktable mode first."),
624+
images["hdr"].filename, hdr_w, hdr_h))
625+
return cleanup(), errors
626+
end
627+
update_job_progress()
628+
uhdr = df.chop_filetype(hdr_raw) .. "_ultrahdr.jpg"
629+
table.insert(remove_files, uhdr)
630+
cmd = settings.bin.ultrahdr_app ..
631+
string.format(
632+
" -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",
633+
df.sanitize_filename(hdr_raw), -- -p
634+
settings.quality, -- -q
635+
settings.quality, -- -Q
636+
settings.target_display_peak_nits, -- -L
637+
settings.downsample, -- s
638+
hdr_w - hdr_w % 2, -- -w
639+
hdr_h - hdr_h % 2, -- -h
640+
df.sanitize_filename(uhdr) -- -z
641+
)
642+
if not execute_cmd(cmd, string.format(_("Error merging %s"), uhdr)) then
643+
return cleanup(), errors
644+
end
645+
update_job_progress()
646+
if settings.copy_exif then
647+
-- Restricting tags to EXIF only, to make sure we won't mess up XMP tags (-all>all).
648+
-- This might hapen e.g. when the source files are Adobe gainmap HDRs.
649+
cmd = settings.bin.exiftool .. " -tagsfromfile " .. df.sanitize_filename(hdr) .. " -exif " ..
650+
df.sanitize_filename(uhdr) .. " -overwrite_original -preserve"
651+
if not execute_cmd(cmd, string.format(_("Error adding EXIF to %s"), uhdr)) then
652+
return cleanup(), errors
653+
end
654+
end
655+
update_job_progress()
554656
end
555657

556-
local output_dir = settings.use_original_dir and images["sdr"].path or settings.output
658+
local output_dir = settings.use_original_dir and best_source_image.path or settings.output
557659
local output_file = df.create_unique_filename(output_dir .. PS .. df.get_filename(uhdr))
558660
ok = df.file_move(uhdr, output_file)
559661
if not ok then
560-
table.insert(errors, string.format(_("Error generating UltraHDR for %s"), images["sdr"].filename))
662+
table.insert(errors, string.format(_("Error generating UltraHDR for %s"), best_source_image.filename))
561663
return cleanup(), errors
562664
end
563665
if settings.import_to_darktable then
@@ -593,7 +695,9 @@ local function main()
593695

594696
local stacks, stack_count = get_stacks(dt.gui.selection(), encoding_variant, selection_type)
595697
if stack_count == 0 then
596-
dt.print(string.format(_("No image stacks detected.\n\nMake sure that the image pairs have the same widths and heights."), stack_count))
698+
dt.print(string.format(_(
699+
"No image stacks detected.\n\nMake sure that the image pairs have the same widths and heights."),
700+
stack_count))
597701
return
598702
end
599703
dt.print(string.format(_("Detected %d image stack(s)"), stack_count))
@@ -737,10 +841,11 @@ This will determine the method used to generate UltraHDR.
737841
- %s: SDR image paired with a gain map image.
738842
- %s: SDR image paired with an HDR image.
739843
- %s: Each stack consists of a single SDR image. Gain maps will be copies of SDR images.
844+
- %s: Each stack consists of a single HDR image. HDR will be tone mapped to SDR.
740845
741846
By default, the first image in a stack is treated as SDR, and the second one is a gain map/HDR.
742847
You can force the image into a specific stack slot by attaching "hdr" / "gainmap" tags to it.
743-
]]), _("SDR + gain map"), _("SDR + HDR"), _("SDR only")),
848+
]]), _("SDR + gain map"), _("SDR + HDR"), _("SDR only"), _("HDR only")),
744849
selected = 0,
745850
changed_callback = function(self)
746851
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
754859
end,
755860
_("SDR + gain map"), -- ENCODING_VARIANT_SDR_AND_GAINMAP
756861
_("SDR + HDR"), -- ENCODING_VARIANT_SDR_AND_HDR
757-
_("SDR only") -- ENCODING_VARIANT_SDR_AUTO_GAINMAP
862+
_("SDR only"), -- ENCODING_VARIANT_SDR_AUTO_GAINMAP
863+
_("HDR only") -- ENCODING_VARIANT_HDR_ONLY
758864
}
759865

760866
GUI.optionwidgets.selection_type_combo = dt.new_widget("combobox") {
@@ -787,9 +893,24 @@ GUI.optionwidgets.quality_widget = dt.new_widget("slider") {
787893
end
788894
}
789895

896+
GUI.optionwidgets.target_display_peak_nits_widget = dt.new_widget("slider") {
897+
label = _('target display peak brightness (nits)'),
898+
tooltip = _('Peak brightness of target display in nits (defaults to 10000)'),
899+
hard_min = 203,
900+
hard_max = 10000,
901+
soft_min = 1000,
902+
soft_max = 10000,
903+
step = 10,
904+
digits = 0,
905+
reset_callback = function(self)
906+
self.value = 10000
907+
end
908+
}
909+
790910
GUI.optionwidgets.gainmap_downsampling_widget = dt.new_widget("slider") {
791911
label = _('gain map downsampling steps'),
792-
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)'),
912+
tooltip = _(
913+
'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)'),
793914
hard_min = 0,
794915
hard_max = 7,
795916
soft_min = 0,
@@ -807,6 +928,7 @@ GUI.optionwidgets.encoding_settings_box = dt.new_widget("box") {
807928
GUI.optionwidgets.encoding_variant_combo,
808929
GUI.optionwidgets.quality_widget,
809930
GUI.optionwidgets.gainmap_downsampling_widget,
931+
GUI.optionwidgets.target_display_peak_nits_widget,
810932
GUI.optionwidgets.metadata_box
811933
}
812934

0 commit comments

Comments
 (0)