From b08d7f27d2e8b2359b5096b167be0d865b58f150 Mon Sep 17 00:00:00 2001 From: Dan Torop Date: Tue, 5 Jan 2021 22:14:40 -0500 Subject: [PATCH 1/5] fujifilm_dynamic_range : first draft Read RawExposureBias and adjust "EXIF Exposure Bias" based upon this. --- contrib/fujifilm_dynamic_range.lua | 73 ++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 contrib/fujifilm_dynamic_range.lua diff --git a/contrib/fujifilm_dynamic_range.lua b/contrib/fujifilm_dynamic_range.lua new file mode 100644 index 0000000..5ce8fc0 --- /dev/null +++ b/contrib/fujifilm_dynamic_range.lua @@ -0,0 +1,73 @@ +--[[ fujifilm_dynamic_range-0.1 + +Compensate for Fujifilm raw files made using "dynamic range". + +Copyright (C) 2020 Dan Torop + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +]] + +--[[About this Plugin +Support for adjusting darktable exposure by Fujifilm raw exposure +bias. This corrects for a DR100/DR200/DR400 "dynamic range" setting. + +Dependencies: +- exiftool (https://www.sno.phy.queensu.ca/~phil/exiftool/) + +Based upon fujifilm_ratings by Ben Mendis + +--]] + +local dt = require "darktable" +local du = require "lib/dtutils" +local df = require "lib/dtutils.file" +local gettext = dt.gettext + +du.check_min_api_version("4.0.0", "fujifilm_dynamic_range") + +gettext.bindtextdomain("fujifilm_dynamic_range", dt.configuration.config_dir.."/lua/locale/") + +local function _(msgid) + return gettext.dgettext("fujifilm_dynamic_range", msgid) +end + +local function detect_dynamic_range(event, image) + -- exiftool knows about the RawExposureBias tag, unlike exiv2, but it is also 10x slower + if not df.check_if_bin_exists("exiftool") then + dt.print_error(_("exiftool not found")) + return + end + local RAF_filename = df.sanitize_filename(tostring(image)) + local command = "exiftool -RawExposureBias " .. RAF_filename + dt.print_error(command) + output = io.popen(command) + local raf_result = output:read("*all") + output:close() + if string.len(raf_result) > 0 then + raf_result = string.gsub(raf_result, "^Raw Exposure Bias.-([%d%.%-]+)", "%1") + if image.exif_exposure_bias ~= image.exif_exposure_bias then + -- is NAN (this is unlikely as RAFs should have ExposureBiasValue set) + image.exif_exposure_bias = 0 + end + -- this should be auto-applied if plugins/darkroom/workflow is scene-referred + -- FIXME: scene-referred workflow pushes exposure up 0.5 EV, but DR100 pushes up 0.7 EV -- should reduce this by 0.5 EV? + image.exif_exposure_bias = image.exif_exposure_bias + tonumber(raf_result) + dt.print_error(_("Using RAF exposure bias: ") .. tostring(raf_result)) + end +end + +dt.register_event("post-import-image", detect_dynamic_range) + +print(_("fujifilm_dynamic_range loaded.")) From 952b8b2eb23476c3d12133600ae14ab9fd7c67ab Mon Sep 17 00:00:00 2001 From: Dan Torop Date: Wed, 6 Jan 2021 10:11:59 -0500 Subject: [PATCH 2/5] fujifilm-dynamic-range: better exiftool parsing & comments Get tab-delimited output from exiftool without rounding. Add comments to explain the choice of tags being read and tool to read them. --- contrib/fujifilm_dynamic_range.lua | 35 ++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/contrib/fujifilm_dynamic_range.lua b/contrib/fujifilm_dynamic_range.lua index 5ce8fc0..d5dee9b 100644 --- a/contrib/fujifilm_dynamic_range.lua +++ b/contrib/fujifilm_dynamic_range.lua @@ -28,6 +28,33 @@ Dependencies: Based upon fujifilm_ratings by Ben Mendis +The relevant tag is RawExposureBias (0x9650). This appears to +represent the shift in EV for the chosen DR setting (whether manual or +automatic). Note that even at 100DR ("standard") there is an EV shift: + +100 DR -> -0.72 EV +200 DR -> -1.72 EV +400 DR -> -2.72 EV + +The ideal would be to use exiv2 to read this tag, as this is the same +code which darktable import uses. Unfortunately, exiv2 as of v0.27.3 +can't read this tag. As it is encoded as a 4-byte ratio of two signed +shorts -- a novel data type -- it will require some attention to fix +this. + +There is an exiv2-readable DevelopmentDynamicRange tag which maps to +RawExposureBias as above. DevelopmentDynamicRange is only present +when tag DynamicRangeSetting (0x1402) is Manual/Raw (0x0001). When it +is Auto (0x0000), the equivalent data is tag AutoDynamicRange +(0x140b). But exiv2 currently can't read that tag either. + +Hence for now this code uses exiftool to read RawExposureBias, as a +more general solution. As exiftool is approx. 10x slower than exiv2 +(Perl vs. C++), this may slow large imports. + +These tags have been checked on a Fujifilm X100S and X100V. Other +cameras may behave in other ways. + --]] local dt = require "darktable" @@ -44,25 +71,25 @@ local function _(msgid) end local function detect_dynamic_range(event, image) - -- exiftool knows about the RawExposureBias tag, unlike exiv2, but it is also 10x slower if not df.check_if_bin_exists("exiftool") then dt.print_error(_("exiftool not found")) return end local RAF_filename = df.sanitize_filename(tostring(image)) - local command = "exiftool -RawExposureBias " .. RAF_filename + -- without -n flag, exiftool will round to the nearest tenth + local command = "exiftool -RawExposureBias -n -t " .. RAF_filename dt.print_error(command) output = io.popen(command) local raf_result = output:read("*all") output:close() if string.len(raf_result) > 0 then - raf_result = string.gsub(raf_result, "^Raw Exposure Bias.-([%d%.%-]+)", "%1") + raf_result = string.match(raf_result, "\t(.*)") if image.exif_exposure_bias ~= image.exif_exposure_bias then -- is NAN (this is unlikely as RAFs should have ExposureBiasValue set) image.exif_exposure_bias = 0 end -- this should be auto-applied if plugins/darkroom/workflow is scene-referred - -- FIXME: scene-referred workflow pushes exposure up 0.5 EV, but DR100 pushes up 0.7 EV -- should reduce this by 0.5 EV? + -- note that scene-referred workflow exposure preset also pushes exposure up by 0.5 EV image.exif_exposure_bias = image.exif_exposure_bias + tonumber(raf_result) dt.print_error(_("Using RAF exposure bias: ") .. tostring(raf_result)) end From 6341a0a45939f8bd770e410ebd97408aa6026615 Mon Sep 17 00:00:00 2001 From: Dan Torop Date: Wed, 6 Jan 2021 10:25:18 -0500 Subject: [PATCH 3/5] fujifilm-dynamic-range: better logging and error handling Use dt.print_log() for log messages. Print error output if no results or results not parseable from exiftool. Also update the date, it's 2021. --- contrib/fujifilm_dynamic_range.lua | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/contrib/fujifilm_dynamic_range.lua b/contrib/fujifilm_dynamic_range.lua index d5dee9b..0f61cd5 100644 --- a/contrib/fujifilm_dynamic_range.lua +++ b/contrib/fujifilm_dynamic_range.lua @@ -2,7 +2,7 @@ Compensate for Fujifilm raw files made using "dynamic range". -Copyright (C) 2020 Dan Torop +Copyright (C) 2021 Dan Torop This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -78,23 +78,29 @@ local function detect_dynamic_range(event, image) local RAF_filename = df.sanitize_filename(tostring(image)) -- without -n flag, exiftool will round to the nearest tenth local command = "exiftool -RawExposureBias -n -t " .. RAF_filename - dt.print_error(command) + dt.print_log(command) output = io.popen(command) local raf_result = output:read("*all") output:close() if string.len(raf_result) > 0 then raf_result = string.match(raf_result, "\t(.*)") - if image.exif_exposure_bias ~= image.exif_exposure_bias then - -- is NAN (this is unlikely as RAFs should have ExposureBiasValue set) - image.exif_exposure_bias = 0 + if raf_result then + if image.exif_exposure_bias ~= image.exif_exposure_bias then + -- is NAN (this is unlikely as RAFs should have ExposureBiasValue set) + image.exif_exposure_bias = 0 + end + -- this should be auto-applied if plugins/darkroom/workflow is scene-referred + -- note that scene-referred workflow exposure preset also pushes exposure up by 0.5 EV + image.exif_exposure_bias = image.exif_exposure_bias + tonumber(raf_result) + dt.print_log(_("Using RAF exposure bias: ") .. tostring(raf_result)) + else + dt.print_error(_("Could not parse exiftool output.")) end - -- this should be auto-applied if plugins/darkroom/workflow is scene-referred - -- note that scene-referred workflow exposure preset also pushes exposure up by 0.5 EV - image.exif_exposure_bias = image.exif_exposure_bias + tonumber(raf_result) - dt.print_error(_("Using RAF exposure bias: ") .. tostring(raf_result)) + else + dt.print_error(_("No output returned by exiftool.")) end end dt.register_event("post-import-image", detect_dynamic_range) -print(_("fujifilm_dynamic_range loaded.")) +dt.print_log(_("fujifilm_dynamic_range loaded.")) From a9fd0718b072d646011aee6216c605c4085f4ffe Mon Sep 17 00:00:00 2001 From: Dan Torop Date: Wed, 6 Jan 2021 11:21:05 -0500 Subject: [PATCH 4/5] fujifilm-dynamic-range: skip non-Fuji non-raws and clean-ups Don't bother to pull EXIF data unless the imported image is a Fujifilm raw. Cleanup conditionals for error handling by immediately returning on failure, avoiding deep nested conditionals. Cleanup log/error output with clearer messages. --- contrib/fujifilm_dynamic_range.lua | 45 ++++++++++++++++++------------ 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/contrib/fujifilm_dynamic_range.lua b/contrib/fujifilm_dynamic_range.lua index 0f61cd5..8c080d5 100644 --- a/contrib/fujifilm_dynamic_range.lua +++ b/contrib/fujifilm_dynamic_range.lua @@ -71,8 +71,17 @@ local function _(msgid) end local function detect_dynamic_range(event, image) + if image.exif_maker ~= "FUJIFILM" then + dt.print_log(_("[fujifilm_dynamic_range] ignoring non-Fujifilm image")) + return + end + -- it would be nice to check image.is_raw but this appears to not yet be set + if not string.match(image.filename, "%.RAF$") then + dt.print_log(_("[fujifilm_dynamic_range] ignoring non-raw image")) + return + end if not df.check_if_bin_exists("exiftool") then - dt.print_error(_("exiftool not found")) + dt.print_error(_("[fujifilm_dynamic_range] exiftool not found")) return end local RAF_filename = df.sanitize_filename(tostring(image)) @@ -82,25 +91,25 @@ local function detect_dynamic_range(event, image) output = io.popen(command) local raf_result = output:read("*all") output:close() - if string.len(raf_result) > 0 then - raf_result = string.match(raf_result, "\t(.*)") - if raf_result then - if image.exif_exposure_bias ~= image.exif_exposure_bias then - -- is NAN (this is unlikely as RAFs should have ExposureBiasValue set) - image.exif_exposure_bias = 0 - end - -- this should be auto-applied if plugins/darkroom/workflow is scene-referred - -- note that scene-referred workflow exposure preset also pushes exposure up by 0.5 EV - image.exif_exposure_bias = image.exif_exposure_bias + tonumber(raf_result) - dt.print_log(_("Using RAF exposure bias: ") .. tostring(raf_result)) - else - dt.print_error(_("Could not parse exiftool output.")) - end - else - dt.print_error(_("No output returned by exiftool.")) + if #raf_result == 0 then + dt.print_error(_("[fujifilm_dynamic_range] no output returned by exiftool")) + return end + raf_result = string.match(raf_result, "\t(.*)") + if not raf_result then + dt.print_error(_("[fujifilm_dynamic_range] could not parse exiftool output")) + return + end + if image.exif_exposure_bias ~= image.exif_exposure_bias then + -- is NAN (this is unlikely as RAFs should have ExposureBiasValue set) + image.exif_exposure_bias = 0 + end + -- this should be auto-applied if plugins/darkroom/workflow is scene-referred + -- note that scene-referred workflow exposure preset also pushes exposure up by 0.5 EV + image.exif_exposure_bias = image.exif_exposure_bias + tonumber(raf_result) + dt.print_log(_("[fujifilm_dynamic_range] raw exposure bias ") .. tostring(raf_result)) end dt.register_event("post-import-image", detect_dynamic_range) -dt.print_log(_("fujifilm_dynamic_range loaded.")) +dt.print_log(_("[fujifilm_dynamic_range] loaded")) From 1d1eb6a7688bf6bc0d9c18ea3239797785f4a306 Mon Sep 17 00:00:00 2001 From: Dan Torop Date: Thu, 7 Jan 2021 22:55:55 -0500 Subject: [PATCH 5/5] fujifilm-dynamic-range: remove translations, use full exiftool path Suggestions as per @wpferguson: "We came to a decision a while ago to not translate debug messages(i.e. print_log() and print_error()) because the author might not be able to read the output if it was a language they weren't familiar with. So the only thing we translate are labels, tooltips, and dt.print() messages." "df.check_if_bin_exists() returns the full path to the executable. Using the returned value would let the script run on windows and macos too." --- contrib/fujifilm_dynamic_range.lua | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/contrib/fujifilm_dynamic_range.lua b/contrib/fujifilm_dynamic_range.lua index 8c080d5..fb773fa 100644 --- a/contrib/fujifilm_dynamic_range.lua +++ b/contrib/fujifilm_dynamic_range.lua @@ -60,44 +60,38 @@ cameras may behave in other ways. local dt = require "darktable" local du = require "lib/dtutils" local df = require "lib/dtutils.file" -local gettext = dt.gettext du.check_min_api_version("4.0.0", "fujifilm_dynamic_range") -gettext.bindtextdomain("fujifilm_dynamic_range", dt.configuration.config_dir.."/lua/locale/") - -local function _(msgid) - return gettext.dgettext("fujifilm_dynamic_range", msgid) -end - local function detect_dynamic_range(event, image) if image.exif_maker ~= "FUJIFILM" then - dt.print_log(_("[fujifilm_dynamic_range] ignoring non-Fujifilm image")) + dt.print_log("[fujifilm_dynamic_range] ignoring non-Fujifilm image") return end -- it would be nice to check image.is_raw but this appears to not yet be set if not string.match(image.filename, "%.RAF$") then - dt.print_log(_("[fujifilm_dynamic_range] ignoring non-raw image")) + dt.print_log("[fujifilm_dynamic_range] ignoring non-raw image") return end - if not df.check_if_bin_exists("exiftool") then - dt.print_error(_("[fujifilm_dynamic_range] exiftool not found")) + local command = df.check_if_bin_exists("exiftool") + if not command then + dt.print_error("[fujifilm_dynamic_range] exiftool not found") return end local RAF_filename = df.sanitize_filename(tostring(image)) -- without -n flag, exiftool will round to the nearest tenth - local command = "exiftool -RawExposureBias -n -t " .. RAF_filename + command = command .. " -RawExposureBias -n -t " .. RAF_filename dt.print_log(command) output = io.popen(command) local raf_result = output:read("*all") output:close() if #raf_result == 0 then - dt.print_error(_("[fujifilm_dynamic_range] no output returned by exiftool")) + dt.print_error("[fujifilm_dynamic_range] no output returned by exiftool") return end raf_result = string.match(raf_result, "\t(.*)") if not raf_result then - dt.print_error(_("[fujifilm_dynamic_range] could not parse exiftool output")) + dt.print_error("[fujifilm_dynamic_range] could not parse exiftool output") return end if image.exif_exposure_bias ~= image.exif_exposure_bias then @@ -107,9 +101,9 @@ local function detect_dynamic_range(event, image) -- this should be auto-applied if plugins/darkroom/workflow is scene-referred -- note that scene-referred workflow exposure preset also pushes exposure up by 0.5 EV image.exif_exposure_bias = image.exif_exposure_bias + tonumber(raf_result) - dt.print_log(_("[fujifilm_dynamic_range] raw exposure bias ") .. tostring(raf_result)) + dt.print_log("[fujifilm_dynamic_range] raw exposure bias " .. tostring(raf_result)) end dt.register_event("post-import-image", detect_dynamic_range) -dt.print_log(_("[fujifilm_dynamic_range] loaded")) +dt.print_log("[fujifilm_dynamic_range] loaded")