596 lines
18 KiB
Lua
596 lines
18 KiB
Lua
--[[
|
|
|
|
image_time.lua - synchronize image time for images shot with different cameras
|
|
|
|
Copyright (C) 2019, 2020 Bill Ferguson <wpferguson@gmail.com>.
|
|
|
|
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 3 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, see <http://www.gnu.org/licenses/>.
|
|
]]
|
|
--[[
|
|
image_time - non-destructively modify the image time
|
|
|
|
DESCRIPTION
|
|
|
|
image_time non destructively adjusts image times by modifying the
|
|
database image exif_datetime_taken field. There are 4 modes: adjust time,
|
|
set time, synchronize time, and reset time.
|
|
|
|
ADJUST TIME
|
|
|
|
adjust time mode lets you chose an offset in terms of years, months,
|
|
days, hours, minutes, and seconds. The adjustment can be added or
|
|
subtracted.
|
|
|
|
WARNING: When adding and subtracting months the result will usually
|
|
be what is expected unless the time being adjusted is at the end of
|
|
the month. This is because a month is a variable amount of time that
|
|
can be 28, 29, 30 or 31 days depending on the month. Example: It's
|
|
March 31st and I subtract a month which not sets the time to February
|
|
31st. When that gets set to a valid time, then the date changes to
|
|
March 3rd.
|
|
|
|
SET TIME
|
|
|
|
set time mode allows you to pick a date and time and set the image
|
|
time accordingly. Fields may be left out. This is useful when
|
|
importing scanned images that don't have an embedded date.
|
|
|
|
SYNCHRONIZE TIME
|
|
|
|
I recently purchased a 7DmkII to replace my aging 7D. My 7D was still
|
|
serviceable, so I bought a remote control and figured I'd try shooting
|
|
events from 2 different perspectives. I didn't think to synchonize the
|
|
time between the 2 cameras, so when I loaded the images and sorted by
|
|
time it was a disaster. I hacked a script together with hard coded values
|
|
to adjust the exif_datetime_taken value in the database for the 7D images
|
|
so that everything sorted properly. I've tried shooting with 2 cameras
|
|
several times since that first attempt. I've gotten better at getting the
|
|
camera times close, but still haven't managed to get them to sync. So I
|
|
decided to think the problem through and write a proper script to take
|
|
care of the problem.
|
|
|
|
RESET TIME
|
|
|
|
Select the images and click reset.
|
|
|
|
USAGE
|
|
|
|
ADJUST TIME
|
|
|
|
Change the year, month, day, hour, minute, second dropdowns to the amount
|
|
of change desired. Select add or subtract. Select the images. Click
|
|
adjust.
|
|
|
|
SET TIME
|
|
|
|
Set the time fields to the desired time. Select the images to change. Click
|
|
set.
|
|
|
|
SYNCHRONIZE TIME
|
|
|
|
Select 2 images, one from each camera, of the same moment in time. Click
|
|
the Calculate button to calculate the time difference. The difference is
|
|
displayed in the difference entry. You can manually adjust it by changing
|
|
the value if necessary.
|
|
|
|
Select the images that need their time adjusted. Determine which way to adjust
|
|
adjust the time (add or subtract) and select the appropriate choice.
|
|
|
|
If the image times get messed up and you just want to start over, select reset time
|
|
from the mode and reset the image times.
|
|
|
|
RESET TIME
|
|
|
|
Select the images and click reset.
|
|
|
|
ADDITIONAL SOFTWARE REQUIRED
|
|
* exiv2
|
|
|
|
BUGS, COMMENTS, SUGGESTIONS
|
|
* Send to Bill Ferguson, wpferguson@gmail.com
|
|
|
|
CHANGES
|
|
|
|
]]
|
|
local dt = require "darktable"
|
|
local du = require "lib/dtutils"
|
|
local df = require "lib/dtutils.file"
|
|
local ds = require "lib/dtutils.string"
|
|
local dtsys = require "lib/dtutils.system"
|
|
local gettext = dt.gettext.gettext
|
|
|
|
local img_time = {}
|
|
img_time.module_installed = false
|
|
img_time.event_registered = false
|
|
|
|
du.check_min_api_version("7.0.0", "image_time")
|
|
|
|
dt.gettext.bindtextdomain("image_time", dt.configuration.config_dir .."/lua/locale/")
|
|
|
|
local function _(msgid)
|
|
return gettext(msgid)
|
|
end
|
|
|
|
-- return data structure for script_manager
|
|
|
|
local script_data = {}
|
|
|
|
script_data.metadata = {
|
|
name = "image_time",
|
|
purpose = _("synchronize image time for images shot with different cameras or adjust or set image time"),
|
|
author = "Bill Ferguson <wpferguson@gmail.com>",
|
|
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/image_time"
|
|
}
|
|
|
|
script_data.destroy = nil -- function to destory the script
|
|
script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil
|
|
script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again
|
|
script_data.show = nil -- only required for libs since the destroy_method only hides them
|
|
|
|
|
|
|
|
local PS = dt.configuration.runnin_os == "windows" and "\\" or "/"
|
|
local ERROR = -1
|
|
|
|
-- function to convert from exif time to system time
|
|
local function exiftime2systime(exiftime)
|
|
local yr,mo,dy,h,m,s = string.match(exiftime, "(%d-):(%d-):(%d-) (%d-):(%d-):(%d+)")
|
|
return(os.time{year=yr, month=mo, day=dy, hour=h, min=m, sec=s})
|
|
end
|
|
|
|
-- function to convert from systime to exif time
|
|
local function systime2exiftime(systime)
|
|
local t = os.date("*t", systime)
|
|
return(string.format("%4d:%02d:%02d %02d:%02d:%02d", t.year, t.month, t.day, t.hour, t.min, t.sec))
|
|
end
|
|
|
|
local function vars2exiftime(year, month, day, hour, min, sec)
|
|
local y = tonumber(year) and string.format("%4d", year) or " "
|
|
local mo = tonumber(month) and string.format("%02d", month) or " "
|
|
local d = tonumber(day) and string.format("%02d", day) or " "
|
|
local h = tonumber(hour) and string.format("%02d", hour) or " "
|
|
local m = tonumber(min) and string.format("%02d", min) or " "
|
|
local s = tonumber(sec) and string.format("%02d", sec) or " "
|
|
return(y .. ":" .. mo .. ":" .. d .. " " .. h .. ":" .. m .. ":" .. s)
|
|
end
|
|
|
|
local function exiftime2vars(exiftime)
|
|
return string.match(exiftime, "(%d+):(%d+):(%d+) (%d+):(%d+):(%d+)")
|
|
end
|
|
|
|
local function calc_time_difference(image1, image2)
|
|
return math.abs(exiftime2systime(image1.exif_datetime_taken) - exiftime2systime(image2.exif_datetime_taken))
|
|
end
|
|
|
|
local function adjust_image_time(image, difference)
|
|
image.exif_datetime_taken = systime2exiftime(exiftime2systime(image.exif_datetime_taken) + difference)
|
|
return
|
|
end
|
|
|
|
local function calculate_difference(images)
|
|
if #images == 2 then
|
|
img_time.diff_entry.text = calc_time_difference(images[1], images[2])
|
|
img_time.btn.sensitive = true
|
|
else
|
|
dt.print(_("ERROR: 2 images must be selected"))
|
|
end
|
|
end
|
|
|
|
local function synchronize_times(images, difference)
|
|
for _, image in ipairs(images) do
|
|
adjust_image_time(image, difference)
|
|
end
|
|
end
|
|
|
|
local function synchronize_time(images)
|
|
local sign = 1
|
|
if img_time.sdir.value == "subtract" then
|
|
sign = -1
|
|
end
|
|
synchronize_times(images, tonumber(img_time.diff_entry.text) * sign)
|
|
end
|
|
|
|
local function add_time(images)
|
|
synchronize_times(images, tonumber(img_time.diff_entry.text))
|
|
end
|
|
|
|
local function year_month2months(year, month)
|
|
year_months = tonumber(year) and year * 12 or 0
|
|
local months = tonumber(month) and tonumber(month) or 0
|
|
|
|
return year_months + months
|
|
end
|
|
|
|
local function months2year_month(months)
|
|
dt.print_log("months is " .. months)
|
|
local year = math.floor(months / 12)
|
|
local month = months - (year * 12)
|
|
|
|
return year, month
|
|
end
|
|
|
|
local function get_image_taken_time(image)
|
|
-- get original image time
|
|
local datetime = nil
|
|
|
|
local exiv2 = df.check_if_bin_exists("exiv2")
|
|
if exiv2 then
|
|
p = io.popen(exiv2 .. " -K Exif.Image.DateTime " .. image.path .. PS .. image.filename)
|
|
if p then
|
|
for line in p:lines() do
|
|
if string.match(line, "Exif.Image.DateTime") then
|
|
datetime = string.match(line, "(%d-:%d-:%d- %d-:%d-:%d+)")
|
|
end
|
|
end
|
|
p:close()
|
|
end
|
|
else
|
|
dt.print(_("unable to detect exiv2"))
|
|
datetime = ERROR
|
|
end
|
|
return datetime
|
|
end
|
|
|
|
local function _get_windows_image_file_creation_time(image)
|
|
local datetime = nil
|
|
local p = io.popen("dir " .. image.path .. PS .. image.filename)
|
|
if p then
|
|
for line in p:lines() do
|
|
if string.match(line, ds.sanitize_lua(image.filename)) then
|
|
local mo, day, yr, hr, min, apm = string.match(line, "(%d+)/(%d+)/(%d+) (%d-):(%d+) (%S+)")
|
|
if apm == "PM" then
|
|
hr = hr + 12
|
|
end
|
|
datetime = vars2exiftime(yr, mo, day, hr, min, 0)
|
|
end
|
|
end
|
|
p:close()
|
|
else
|
|
dt.print(string.format(_("unable to get information for $s"), image.filename))
|
|
datetime = ERROR
|
|
end
|
|
return datetime
|
|
end
|
|
|
|
local function _get_nix_image_file_creation_time(image)
|
|
local datetime = nil
|
|
local p = io.popen("ls -lL --time-style=full-iso " .. image.path .. PS .. image.filename)
|
|
if p then
|
|
for line in p:lines() do
|
|
if string.match(line, ds.sanitize_lua(image.filename)) then
|
|
datetime = vars2exiftime(string.match(line, "(%d+)%-(%d-)%-(%d-) (%d-):(%d-):(%d+)."))
|
|
end
|
|
end
|
|
p:close()
|
|
else
|
|
dt.print(string.format(_("unable to get information for %s"), image.filename))
|
|
datetime = ERROR
|
|
end
|
|
return datetime
|
|
end
|
|
|
|
local function get_image_file_creation_time(image)
|
|
-- no exif time in the image file so get the creation time
|
|
local datetime = nil
|
|
if dt.configuration.running_os == "windows" then
|
|
datetime = _get_windows_image_file_creation_time(image)
|
|
else
|
|
datetime = _get_nix_image_file_creation_time(image)
|
|
end
|
|
return datetime
|
|
end
|
|
|
|
local function get_original_image_time(image)
|
|
local image_time = image.exif_datetime_taken
|
|
local reset_time = nil
|
|
|
|
reset_time = get_image_taken_time(image)
|
|
|
|
if reset_time then
|
|
if reset_time == ERROR then
|
|
return image_time
|
|
else
|
|
return reset_time
|
|
end
|
|
else
|
|
reset_time = get_image_file_creation_time(image)
|
|
|
|
if reset_time then
|
|
if reset_time == ERROR then
|
|
return image_time
|
|
else
|
|
return reset_time
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function reset_time(images)
|
|
if #images > 0 then
|
|
for _, image in ipairs(images) do
|
|
image.exif_datetime_taken = get_original_image_time(image)
|
|
end
|
|
else
|
|
dt.print_error("reset time: no images selected")
|
|
dt.print(_("please select the images that need their time reset"))
|
|
end
|
|
end
|
|
|
|
local function adjust_time(images)
|
|
local SEC_MIN = 60
|
|
local SEC_HR = SEC_MIN * 60
|
|
local SEC_DY = SEC_HR * 24
|
|
|
|
local offset = nil
|
|
local sign = 1
|
|
|
|
if #images < 1 then
|
|
dt.print(_("please select some images and try again"))
|
|
return
|
|
end
|
|
|
|
if img_time.adir.value == _("subtract") then
|
|
sign = -1
|
|
end
|
|
|
|
for _, image in ipairs(images) do
|
|
local y, mo, d, h, m, s = exiftime2vars(image.exif_datetime_taken)
|
|
local image_months = year_month2months(y, mo)
|
|
local months_diff = year_month2months(img_time.ayr.value, img_time.amo.value)
|
|
y, mo = months2year_month(image_months + (months_diff * sign))
|
|
local exif_new = vars2exiftime(y, mo, d, h, m, s)
|
|
offset = img_time.ady.value * SEC_DY
|
|
offset = offset + img_time.ahr.value * SEC_HR
|
|
offset = offset + img_time.amn.value * SEC_MIN
|
|
offset = offset + img_time.asc.value
|
|
offset = offset * sign
|
|
image.exif_datetime_taken = systime2exiftime(exiftime2systime(exif_new) + offset)
|
|
end
|
|
end
|
|
|
|
local function set_time(images)
|
|
if #images < 1 then
|
|
dt.print(_("please select some images and try again"))
|
|
return
|
|
end
|
|
|
|
local y = img_time.syr.value
|
|
local mo = img_time.smo.value
|
|
local d = img_time.sdy.value
|
|
local h = img_time.shr.value
|
|
local m = img_time.smn.value
|
|
local s = img_time.ssc.value
|
|
|
|
for _, image in ipairs(images) do
|
|
image.exif_datetime_taken = vars2exiftime(y, mo, d, h, m, s)
|
|
end
|
|
end
|
|
|
|
local function seq(first, last)
|
|
local result = {}
|
|
|
|
local num = first
|
|
|
|
while num <= last do
|
|
table.insert(result, num)
|
|
num = num + 1
|
|
end
|
|
|
|
return table.unpack(result)
|
|
end
|
|
|
|
local function reset_widgets()
|
|
dt.print_log("took the reset function")
|
|
img_time.ayr.selected = 1
|
|
img_time.amo.selected = 1
|
|
img_time.ady.selected = 1
|
|
img_time.ahr.selected = 1
|
|
img_time.ayr.selected = 1
|
|
img_time.amn.selected = 1
|
|
img_time.asc.selected = 1
|
|
img_time.adir.selected = 1
|
|
img_time.syr.selected = #img_time.syr
|
|
img_time.smo.selected = 1
|
|
img_time.sdy.selected = 1
|
|
img_time.shr.selected = 1
|
|
img_time.smn.selected = 1
|
|
img_time.ssc.selected = 1
|
|
img_time.adir.selected = 1
|
|
end
|
|
|
|
local function install_module()
|
|
if not img_time.module_installed then
|
|
dt.register_lib(
|
|
"image_time", -- Module name
|
|
_("image time"), -- Visible name
|
|
true, -- expandable
|
|
true, -- resetable
|
|
{[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}}, -- containers
|
|
img_time.widget,
|
|
nil,-- view_enter
|
|
nil -- view_leave
|
|
)
|
|
img_time.module_installed = true
|
|
end
|
|
end
|
|
|
|
local function destroy()
|
|
dt.gui.libs["image_time"].visible = false
|
|
end
|
|
|
|
local function restart()
|
|
dt.gui.libs["image_time"].visible = true
|
|
end
|
|
|
|
-- widgets
|
|
|
|
img_time.widgets = {
|
|
-- name, type, tooltip, placeholder,
|
|
{"ayr", "combobox", _("years"), _("years to adjust by, 0 - ?"), {seq(0,20)}, 1},
|
|
{"amo", "combobox", _("months"), _("months to adjust by, 0-12"), {seq(0,12)}, 1},
|
|
{"ady", "combobox", _("days"), _("days to adjust by, 0-31"), {seq(0,31)}, 1},
|
|
{"ahr", "combobox", _("hours"), _("hours to adjust by, 0-23"), {seq(0,23)}, 1},
|
|
{"amn", "combobox", _("minutes"), _("minutes to adjust by, 0-59"), {seq(0,59)}, 1},
|
|
{"asc", "combobox", _("seconds"), _("seconds to adjust by, 0-59"), {seq(0,59)}, 1},
|
|
{"adir", "combobox", _("add/subtract"), _("add or subtract time"), {_("add"), _("subtract")}, 1},
|
|
{"syr", "combobox", _("year"), _("year to set, 1900 - now"), {" ", seq(1900,os.date("*t", os.time()).year)}, 1},
|
|
{"smo", "combobox", _("month"), _("month to set, 1-12"), {" ", seq(1,12)}, 1},
|
|
{"sdy", "combobox", _("day"), _("day to set, 1-31"), {" ", seq(1,31)}, 1},
|
|
{"shr", "combobox", _("hour"), _("hour to set, 0-23"), {" ", seq(0,23)}, 1},
|
|
{"smn", "combobox", _("minute"), _("minutes to set, 0-59"), {" ", seq(0, 59)}, 1},
|
|
{"ssc", "combobox", _("seconds"), _("seconds to set, 0-59"), {" ", seq(0,59)}, 1},
|
|
{"sdir", "combobox", _("add/subtract"), _("add or subtract time"), {_("add"), _("subtract")}, 1},
|
|
}
|
|
|
|
for _, widget in ipairs(img_time.widgets) do
|
|
img_time[widget[1]] = dt.new_widget(widget[2]){
|
|
label = widget[3],
|
|
tooltip = widget[4],
|
|
selected = widget[6],
|
|
table.unpack(widget[5])
|
|
}
|
|
end
|
|
|
|
img_time.syr.selected = #img_time.syr
|
|
|
|
img_time.diff_entry = dt.new_widget("entry"){
|
|
tooltip = _("time difference between images in seconds"),
|
|
placeholder = _("select 2 images and use the calculate button"),
|
|
text = "",
|
|
}
|
|
|
|
img_time.calc_btn = dt.new_widget("button"){
|
|
label = _("calculate"),
|
|
tooltip = _("calculate time difference between 2 images"),
|
|
clicked_callback = function()
|
|
calculate_difference(dt.gui.action_images)
|
|
end
|
|
}
|
|
|
|
img_time.btn = dt.new_widget("button"){
|
|
label = _("synchronize image times"),
|
|
tooltip = _("apply the time difference from selected images"),
|
|
sensitive = false,
|
|
clicked_callback = function()
|
|
synchronize_time(dt.gui.action_images)
|
|
end
|
|
}
|
|
|
|
img_time.stack = dt.new_widget("stack"){
|
|
dt.new_widget("box"){
|
|
orientation = "vertical",
|
|
dt.new_widget("label"){label = _("adjust time")},
|
|
dt.new_widget("section_label"){label = _("days, months, years")},
|
|
img_time.ady,
|
|
img_time.amo,
|
|
img_time.ayr,
|
|
dt.new_widget("section_label"){label = _("hours, minutes, seconds")},
|
|
img_time.ahr,
|
|
img_time.amn,
|
|
img_time.asc,
|
|
dt.new_widget("section_label"){label = _("adjustment direction")},
|
|
img_time.adir,
|
|
dt.new_widget("button"){
|
|
label = _("adjust"),
|
|
clicked_callback = function()
|
|
adjust_time(dt.gui.action_images)
|
|
end
|
|
}
|
|
},
|
|
dt.new_widget("box"){
|
|
orientation = "vertical",
|
|
dt.new_widget("label"){label = _("set time")},
|
|
dt.new_widget("section_label"){label = _("date: ")},
|
|
img_time.sdy,
|
|
img_time.smo,
|
|
img_time.syr,
|
|
dt.new_widget("section_label"){label = _("time:")},
|
|
img_time.shr,
|
|
img_time.smn,
|
|
img_time.ssc,
|
|
dt.new_widget("button"){
|
|
label = _("set"),
|
|
clicked_callback = function()
|
|
set_time(dt.gui.action_images)
|
|
end
|
|
}
|
|
},
|
|
dt.new_widget("box"){
|
|
orientation = "vertical",
|
|
dt.new_widget("label"){label = _("synchronize image time")},
|
|
dt.new_widget("section_label"){label = _("calculate difference between images")},
|
|
img_time.diff_entry,
|
|
img_time.calc_btn,
|
|
dt.new_widget("section_label"){label = _("apply difference")},
|
|
img_time.sdir,
|
|
img_time.btn,
|
|
},
|
|
dt.new_widget("box"){
|
|
orientation = "vertical",
|
|
dt.new_widget("label"){label = _("reset to original time")},
|
|
dt.new_widget("separator"){},
|
|
dt.new_widget("button"){
|
|
label = _("reset"),
|
|
clicked_callback = function()
|
|
reset_time(dt.gui.action_images)
|
|
end
|
|
}
|
|
},
|
|
}
|
|
|
|
img_time.mode = dt.new_widget("combobox"){
|
|
label = _("mode"),
|
|
tooltip = _("select mode"),
|
|
selected = 1,
|
|
changed_callback = function(this)
|
|
img_time.stack.active = this.selected
|
|
end,
|
|
_("adjust time"),
|
|
_("set time"),
|
|
_("synchronize time"),
|
|
_("reset time")
|
|
}
|
|
|
|
img_time.widget = dt.new_widget("box"){
|
|
orientation = "vertical",
|
|
reset_callback = function(this)
|
|
reset_widgets()
|
|
end,
|
|
img_time.mode,
|
|
img_time.stack,
|
|
}
|
|
|
|
if dt.gui.current_view().id == "lighttable" then
|
|
install_module()
|
|
else
|
|
if not img_time.event_registered then
|
|
dt.register_event(
|
|
"image_time", "view-changed",
|
|
function(event, old_view, new_view)
|
|
if new_view.name == "lighttable" and old_view.name == "darkroom" then
|
|
install_module()
|
|
end
|
|
end
|
|
)
|
|
img_time.event_registered = true
|
|
end
|
|
end
|
|
|
|
script_data.destroy = destroy
|
|
script_data.restart = restart
|
|
script_data.destroy_method = "hide"
|
|
script_data.show = restart
|
|
|
|
return script_data
|