540
contrib/image_time.lua
Normal file
540
contrib/image_time.lua
Normal file
@@ -0,0 +1,540 @@
|
||||
--[[
|
||||
|
||||
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
|
||||
)
|
||||
Reference in New Issue
Block a user