Files
lua-scripts-w-api/contrib/image_time.lua
2020-06-24 21:51:40 -04:00

541 lines
17 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 gettext = dt.gettext
local img_time = {}
du.check_min_api_version("3.0.0", "image_time")
-- Tell gettext where to find the .mo file translating messages for a particular domain
gettext.bindtextdomain("image_time",dt.configuration.config_dir.."/lua/locale/")
local function _(msgid)
return gettext.dgettext("image_time", msgid)
end
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(_("unable to get information for ") .. 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(_("unable to get information for ") .. 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
-- 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,
}
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
)