42 Commits

Author SHA1 Message Date
wpferguson
8afd6ccee4 Merge pull request #202 from wpferguson/fix_libraries
fix libraries
2019-12-16 22:45:59 -05:00
Bill Ferguson
e427294ec1 fix library problems found during unit testing 2019-12-16 22:42:32 -05:00
wpferguson
18283ccbc6 Merge pull request #201 from darktable-org/revert-199-library_fixes
Revert "Library fixes"
2019-12-16 22:31:40 -05:00
wpferguson
b7857c8462 Revert "Library fixes" 2019-12-16 22:30:50 -05:00
wpferguson
2d231806d2 Merge pull request #199 from wpferguson/library_fixes
Library fixes
2019-12-16 21:46:14 -05:00
wpferguson
36b58333ca Merge pull request #200 from wpferguson/README_update
Update README.md
2019-12-16 21:40:26 -05:00
Bill Ferguson
fc77af598d Another update 2019-12-16 20:39:10 -05:00
Bill Ferguson
602295ae89 Updated contributed scripts 2019-12-16 20:27:20 -05:00
Bill Ferguson
1c295a88ac Fixed bugs found during unit testing with testing framework 2019-12-11 22:55:44 -05:00
wpferguson
232535faf9 Merge pull request #197 from wpferguson/update_face_recognition
Update face recognition
2019-12-09 20:19:24 -05:00
Bill Ferguson
10b5dee40f Added windows compatibility 2019-12-09 20:08:11 -05:00
Bill Ferguson
be48459092 Converted face_recognition to a module and updated it to use newer library functions. Added tolerance adjustment to settings to fine tune the face detection. 2019-12-08 20:18:07 -05:00
supertobi
cb6199c1a8 Merge pull request #100 from GHswitt/master
Storage plugin for face_recognition
2019-12-07 21:34:38 +01:00
supertobi
c9eb087ade Update README.md
The ' is not not needed in the windows cmd
2019-12-04 13:01:58 +01:00
supertobi
87e223a854 Update README.md
Added: If you don't have %LOCALAPPDATA%\darktable you have to start dartable at least once, because the directory is created at the first start of darktable.
2019-12-04 12:52:44 +01:00
wpferguson
7c7118dfad Merge pull request #193 from simon04/patch-1
README: list various third-party projects
2019-11-10 00:33:39 -05:00
Simon Legner
fdcd6c6dfe README: list various third-party projects 2019-11-04 21:42:15 +01:00
Bill Ferguson
205cbcf05d Checkpoint save 2019-07-03 12:41:58 -04:00
wpferguson
c9ccbb2750 Merge pull request #189 from wpferguson/remove_plugin_manager_symlink
Removed symlink for plugin_manager development
2019-06-19 11:25:32 -04:00
Bill Ferguson
eacfc4b623 Removed symlink for plugin_manager development 2019-06-19 11:23:11 -04:00
supertobi
d3dafd4ce1 Merge pull request #179 from BzKevin/BzKevin-enfuseAdvanced-refactor
Refactored enfuseAdvanced
2019-05-07 15:56:06 +02:00
supertobi
8004b96c41 Merge branch 'master' into BzKevin-enfuseAdvanced-refactor 2019-05-07 15:54:12 +02:00
supertobi
7cc58d9fac Merge pull request #164 from BzKevin/BzKevin-HDRMerge
Add HDRMerge
2019-05-07 13:00:40 +02:00
BzKevin
f82ae6c091 Change 'Create Unique Filename' to default 2019-05-06 21:35:41 -04:00
BzKevin
fcad7a5733 Makte Translatable + others
Made text translatable except for line 499 which kept giving me problems
   Making L499 translatable would throw the following error   lua:499: attempt to call a nil value (upvalue '_')
   Troubleshooting steps taken and results:
        1) Changed the translation function from _() to translate() - this worked, no errors but ugly
        2) Reverted the changes from 1, then changed all "for _, var in table do" to be "for ignore, var in table do" - this did not help

Moved a chunk of code due to anomalous behavior, I suspect previous code was attempting to change the value in the stack widget before it was fully created. Adding in a print statement before trying to change the stack.active would cause things to work fine. Moving it behind the register call also helped.
2019-03-22 12:01:56 -04:00
BzKevin
7ffdf1631c Make text translatable 2019-03-22 09:05:11 -04:00
BzKevin
a5ad5e67f3 Remove executable preferences
The executable choosers have been moved to a box which is now element 4 in the GUI stack. Once the user configures these and presses update they are confirmed good or bad and the GUI changed to normal view if all works out.
2019-03-12 13:04:21 -04:00
BzKevin
4b3b1f7786 Fix bug when updating exe preference
Move the preference.write() call inside the for loop
2019-03-12 12:42:37 -04:00
BzKevin
844e89a652 Use file chooser to set executable path preference
previous commit failed to actually utilize the newly created file chooser to set the path preference
2019-03-12 12:25:37 -04:00
BzKevin
d88af9ffd9 Minor tweaks to last commit 2019-03-12 11:20:04 -04:00
BzKevin
7a3a9ba559 Removed preferences for exe
The GUI is now made of a stack which has two boxes in it. One box contains all the normal GUI elements, the other contains a file chooser button and a normal button to call an update routine. This box is only displayed if an issue is found. I decided to track the install status via a preference boolean, rather than calling df_check_if_bin_exists at startup every time. I do this because on my windows PC I was starting to get slow startups with too many scripts installed and many many flashing CMD windows at startup as well, which was a nuisance.
2019-03-12 11:19:22 -04:00
BzKevin
5d3910da49 Tab to Space 2019-03-05 09:14:28 -05:00
BzKevin
f740d34f1f Tab to Space 2019-03-05 09:12:54 -05:00
BzKevin
58ca70c511 Tab to Space 2019-03-05 09:11:28 -05:00
BzKevin
ee59540648 Refactored
I was having issue wit this script periodically. The original version of this was real a hack-job of Holger Klemms original. This new version utilizes tables in a manner that I hope will make the code easier to follow and understand, but also allows for certain functions to be re-used throughout (and potentially in different scripts as well). In general:

Each program (align image stack, enfuse, exiftool) how has a table that contains information about that program, this includes a sub-table that contains argument data for the program.
There is a GUI table which contains all gui elements. Those elements are split into sub-tables within GUI based on what aspect of the scrpt behavior they relate to (AIS, ENF, Target File, Presets, etc...)
These two structures (and the similarities between them) have allows me to create loops which populate the arguments from the GUI and vice-versa, this is all done through an intermediary preference, the "active" preference. 
Whenever a change to a GUI element occurs the associated member of hte active preference is updated. then when it becomes time to build an argument string for a program command the active preference is read from and the value contained within is used. This works fine for all GUI elements that have a "changed" or "clicked" callback, as they can remain up-to-date real time. Sliders and Entry boxes do not have this, so at a certain point withing the script the GUI values are read directly for these elements and written to the associated active preference field.

As you will see about 50% of this code is simply building all the GUI elements and making sure that the initialization value for that element is valid (via the InRange() function). The actual heart of the program and it's functions really only takes up about 300 lines (starting at 180 and going through 476). That is broken down into multiple fairly concise functions. Hopefully this makes maintenance, feature enhancement, and troubleshooting much easier in the future.

I also made a few UI tweaks. This script use to create such a massive list of GUI elements that it was a nuisance when actually using it. They are now grouped into three containers for AIS options, Enfuse options, and target options. Only one of these options containers is displayed at a time. Additionally, I removed all the registered preferences I had before, so now I only register 3 options to enable user to point towards the program binaries.
2019-03-04 13:13:49 -05:00
BzKevin
85c242ff0e Comments added, minor updates
I have added comments throughout, mainly to the functions
I improved the performance of the CleanSpaces() function
Improved communication to user via dt.print()
2019-03-04 09:45:05 -05:00
BzKevin
7bc4846c0a Refactored
After experiencing some issues after upgrading to dt2.6 I decided it would be worthwhile to refactor the code for more reliable performance. Specifically the script would crash dt when attempting to apply a style on import and sometimes just fail to run. I have refactored this in a manor that should make it more logical for future users to add/maintain as applicable. 

Refactored script has only been tested on Windows 10 w/ dt2.6. Hopefullt all cross-platform aspects have been maintained.
2019-03-03 15:48:08 -05:00
BzKevin
5fd3a56a31 Remove extraneous widgets in preferences
I now understand how preferences work better and have removed the extraneous widgets which were doing nothing
2019-02-15 14:24:51 -05:00
BzKevin
2cb3397d75 Update HDRMerge
Now only checks for binary proper install once the functionality of the program is actually utilized. This avoids to many external command calls at dt startup just to check if certain things are installed.
2019-02-15 13:22:49 -05:00
BzKevin
2a2fbd18cd Add HDRMerge 2019-02-13 12:21:57 -05:00
GHswitt
8e6552ee9c Added ignore tag and unknown tag, verbose output 2017-12-11 17:43:53 +01:00
GHswitt
024e016df2 Storage plugin for face_recognition 2017-12-11 14:27:36 +01:00
7 changed files with 1995 additions and 985 deletions

View File

@@ -32,20 +32,25 @@ These scripts are contributed by users. They are meant to have an "owner", i.e.
Name|Standalone|OS |Purpose
----|:--------:|:---:|-------
AutoGrouper|Yes|LMW|Group images together by time
autostyle|Yes|LMW|Automatically apply styles on import
clear_GPS|Yes|LMW|Reset GPS information for selected images
CollectHelper|Yes|LMW|Add buttons to selected images module to manipulate the collection
copy_attach_detach_tags|Yes|LMW|Copy and paste tags from/to images
cr2hdr|Yes|L|Process image created with Magic Lantern Dual ISO
enfuseAdvanced|No|LMW|Merge multiple images into Dynamic Range Increase (DRI) or Depth From Focus (DFF) images
face_recognition|No|LM|Identify and tag images using facial recognition
fujifilm_ratings|No|LM|Support importing Fujifilm ratings
geoJSON_export|No|L|Create a geo JSON script with thumbnails for use in ...
geoToolbox|No|LMW|A toolbox of geo functions
gimp|No|LMW|Open an image in GIMP for editing and return the result
gpx_export|No|LMW|Export a GPX track file from selected images GPS data
HDRMerge|No|LMW|Combind the selected images into an HDR DNG and return the result
hugin|No|LMW|Combine selected images into a panorama and return the result
image_stack|No|LMW|Combine a stack of images to remove noise or transient objects
kml_export|No|L|Export photos with a KML file for usage in Google Earth
LabelsToTags|Yes|LMW|Apply tags based on color labels and ratings
OpenInExplorer|No|LMW|Open the selected images in the system file manager
passport_guide|Yes|LMW|Add passport cropping guide to darkroom crop tool
pdf_slideshow|No|LM|Export images to a PDF slideshow
quicktag|Yes|LMW|Create shortcuts for quickly applying tags
@@ -53,6 +58,7 @@ rate_group|Yes|LMW|Apply or remove a star rating from grouped images
rename-tags|Yes|LMW|Change a tag name
select_untagged|Yes|LMW|Enable selection of untagged images
slideshowMusic|No|L|Play music during a slideshow
video_ffmpeg|No|LMW|Export video from darktable
video_mencoder|No|L|Export video from darktable
### Example Scripts
@@ -82,6 +88,21 @@ get_lib_manpages|No|LM|Retrieve the library documentation and output it in man p
get_libdoc|No|LMW|Retrieve the library documentation and output it as text
script_manager|No|LMW|Manage (install, update, enable, disable) the lua scripts
### Related third-party projects
The following third-party projects are listed for information only. Think of this collection as an `awesome-darktable-lua-scripts` list. Use at your own risk!
* [trougnouf/dtMediaWiki](https://github.com/trougnouf/dtMediaWiki) – Wikimedia Commons export
* [wpferguson/extra-dt-lua-scripts](https://github.com/wpferguson/extra-dt-lua-scripts)
* [xxv/darktable-git-annex](https://github.com/xxv/darktable-git-annex) – git-annex integration
* [theres/dt_backup](https://github.com/theres/dt_backup) – automatic backup on exit
* [Sitwon/dt_fujifilm_ratings](https://github.com/Sitwon/dt_fujifilm_ratings) – Fujifilm Ratings import
* [progo/leica-q-autocrop](https://github.com/progo/leica-q-autocrop) – crop in 35 mm or 50 mm equivalent
* [BzKevin/OpenInPS-Darktable-PlugIn](https://github.com/BzKevin/OpenInPS-Darktable-PlugIn) – open in Adobe Photoshop
* [johnnyrun/darktable_lua_gimp](https://github.com/johnnyrun/darktable_lua_gimp) – GIMP export
* [arru/darktable-scripts](https://github.com/arru/darktable-scripts)
* [nbremond77/darktable](https://github.com/nbremond77/darktable/tree/master/scripts)
## Download and Install
The recommended method of installation is using git to clone the repository. This ensures that all dependencies on other scripts
@@ -116,6 +137,8 @@ Open a command prompt.
cd %LOCALAPPDATA%\darktable
git clone https://github.com/darktable-org/lua-scripts.git lua
If you don't have %LOCALAPPDATA%\darktable you have to start dartable at least once, because the directory is created at the first start of darktable.
## Enabling
When darktable starts it looks for a file name `~/.config/darktable/luarc` (`%LOCALAPPDATA%\darktable\luarc` for windows) and reads it to see which scripts to include. The file is a plain text file with entries of the form `require "<directory>/<name>"` where directory is the directory containing the scripts, from the above list, and name is the name from the above list. To include GIMP the line would be `require "contrib/gimp"`.
@@ -127,8 +150,8 @@ or `echo 'require "contrib/hugin"' >> ~/.config/darktable/luarc` to add an entry
On windows from a command prompt:
`echo 'require "contrib/gimp"' > %LOCALAPPDATA%\darktable\luarc` to create the file with a gimp entry\
or `echo 'require "contrib/hugin"' >> %LOCALAPPDATA%\darktable\luarc` to add an entry for hugin.
`echo require "contrib/gimp" > %LOCALAPPDATA%\darktable\luarc` to create the file with a gimp entry\
or `echo require "contrib/hugin" >> %LOCALAPPDATA%\darktable\luarc` to add an entry for hugin.
## Disabling

424
contrib/HDRMerge.lua Normal file
View File

@@ -0,0 +1,424 @@
--[[HDRMerge plugin for darktable
copyright (c) 2018 Kevin Ertel
darktable 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.
darktable 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 darktable. If not, see <http://www.gnu.org/licenses/>.
]]
--[[About this Plugin
This plugin adds the module 'HDRMerge' to darktable's lighttable view
----REQUIRED SOFTWARE----
HDRMerge ver. 4.5 or greater
----USAGE----
Install: (see here for more detail: https://github.com/darktable-org/lua-scripts )
1) Copy this file in to your 'lua/contrib' folder where all other scripts reside.
2) Require this file in your luarc file, as with any other dt plug-in
On the initial startup go to darktable settings > lua options and set your executable paths and other preferences, then restart darktable
Select bracketed images and press the Run HDRMerge button. The resulting DNG will be auto-imported into darktable.
Additional tags or style can be applied on auto import as well, if you desire.
Base Options:
Select your desired BPS (bits per sample and Embedded Preview Size.
Batch Options:
Select if you want to run in batch mode or not
Select the gap, in seconds, between images for auto grouping in batch mode
See HDRMerge manual for further detail: http://jcelaya.github.io/hdrmerge/documentation/2014/07/11/user-manual.html
Auto-import Options:
Select a style, whether you want tags to be copied from the original, and any additional tags you desire added when the new image is auto-imported
]]
local dt = require 'darktable'
local df = require 'lib/dtutils.file'
local dsys = require 'lib/dtutils.system'
local mod = 'module_HDRMerge'
local os_path_seperator = '/'
if dt.configuration.running_os == 'windows' then os_path_seperator = '\\' end
-- Tell gettext where to find the .mo file translating messages for a particular domain
local gettext = dt.gettext
gettext.bindtextdomain('HDRMerge',dt.configuration.config_dir..'/lua/locale/')
local function _(msgid)
return gettext.dgettext('HDRMerge', msgid)
end
local temp
local HDRM = { --HDRMerge Program Table
name = 'HDRMerge',
bin = '',
first_run = true,
install_error = false,
arg_string = '',
images_string = '',
args = {
bps ={text = '-b ', style = 'integer'},
size ={text = '-p ', style = 'string'},
batch ={text = '-B ', style = 'bool'},
gap ={text = '-g ', style = 'integer'}
}
}
local GUI = { --GUI Elements Table
HDR = {
bps ={},
size ={},
batch ={},
gap ={}
},
Target = {
style ={},
copy_tags ={},
add_tags ={}
},
run = {},
stack = {},
options = {},
exes = {
HDRMerge = {},
update = {},
}
}
--Detect User Styles--
local styles = dt.styles
local styles_count = 1 -- 'none' = 1
for _,i in pairs(dt.styles) do
if type(i) == 'userdata' then styles_count = styles_count + 1 end
end
local function InRange(test, low, high) --tests if test value is within range of low and high (inclusive)
if test >= low and test <= high then
return true
else
return false
end
end
local function GetFileName(full_path) --Parses a full path (path/filename_identifier.extension) into individual parts
--[[Input: Folder1/Folder2/Folder3/Img_0001.CR2
Returns:
path: Folder1/Folder2/Folder3/
filename: Img_0001
identifier: 0001
extension: .CR2
EX:
path_1, file_1, id_1, ext_1 = GetFileName(full_path_1)
]]
local path = string.match(full_path, '.*[\\/]')
local filename = string.gsub(string.match(full_path, '[%w-_]*%.') , '%.' , '' )
local identifier = string.match(filename, '%d*$')
local extension = string.match(full_path, '%.%w*')
return path, filename, identifier, extension
end
local function CleanSpaces(text) --removes spaces from the front and back of passed in text
text = string.gsub(text,'^%s*','')
text = string.gsub(text,'%s*$','')
return text
end
local function BuildExecuteCmd(prog_table) --creates a program command using elements of the passed in program table
local result = CleanSpaces(prog_table.bin)..' '..CleanSpaces(prog_table.arg_string)..' '..CleanSpaces(prog_table.images_string)
return result
end
local function PreCall(prog_tbl) --looks to see if this is the first call, if so checks to see if program is installed properly
for _,prog in pairs(prog_tbl) do
if prog.first_run then
prog.bin = df.check_if_bin_exists(prog.name)
if not prog.bin then
prog.install_error = true
dt.preferences.write(mod, 'bin_exists', 'bool', false)
else
prog.bin = CleanSpaces(prog.bin)
end
prog.first_run = false
end
end
if not dt.preferences.read(mod, 'bin_exists', 'bool') then
GUI.stack.active = 2
dt.print(_('please update you binary locatoin'))
end
end
local function ExeUpdate(prog_tbl)
dt.preferences.write(mod, 'bin_exists', 'bool', true)
for _,prog in pairs(prog_tbl) do
dt.preferences.write('executable_paths', prog.name, 'string', GUI.exes[prog.name].value)
prog.bin = df.check_if_bin_exists(prog.name)
if not prog.bin then
prog.install_error = true
dt.preferences.write(mod, 'bin_exists', 'bool', false)
else
prog.bin = CleanSpaces(prog.bin)
end
prog.first_run = false
end
if dt.preferences.read(mod, 'bin_exists', 'bool') then
GUI.stack.active = 1
dt.print(_('update successful'))
else
dt.print(_('update unsuccessful, please try again'))
end
end
local function UpdateActivePreference() --sliders & entry boxes do not have a click/changed callback, so their values must be saved to the active preference
temp = GUI.HDR.gap.value
dt.preferences.write(mod, 'active_gap', 'integer', temp)
temp = GUI.Target.add_tags.text
dt.preferences.write(mod, 'active_add_tags', 'string', temp)
end
local function main()
PreCall({HDRM}) --check if furst run then check if install OK
if HDRM.install_error then
dt.print_error(_('HDRMerge install issue'))
dt.print(_('HDRMerge install issue, please ensure the binary path is proper'))
return
end
images = dt.gui.selection() --get selected images
if #images < 2 then --ensure enough images selected
dt.print(_('not enough images selected, select at least 2 images to merge'))
return
end
UpdateActivePreference() --save current gui elements to active preference so those values will be pre-loaded at next startup
--create image string and output path
HDRM.images_string = ''
local out_path = ''
local smallest_id = math.huge
local smallest_name = ''
local largest_id = 0
local source_raw = {}
for _,image in pairs(images) do --loop to concat the images string, also track the image indexes for use in creating the final image name (eg; IMG_1034-1037.dng)
local curr_image = image.path..os_path_seperator..image.filename
HDRM.images_string = HDRM.images_string..df.sanitize_filename(curr_image)..' '
out_path = image.path
_, source_name, source_id = GetFileName(image.filename)
source_id = tonumber(source_id)
if source_id < smallest_id then
smallest_id = source_id
smallest_name = source_name
source_raw = image
end
if source_id > largest_id then largest_id = source_id end
end
out_path = out_path..os_path_seperator..smallest_name..'-'..largest_id..'.dng'
out_path = df.create_unique_filename(out_path)
--create argument string
HDRM.arg_string = HDRM.args.bps.text..GUI.HDR.bps.value..' '..HDRM.args.size.text..GUI.HDR.size.value..' '
if GUI.HDR.batch.value then
HDRM.arg_string = HDRM.arg_string..HDRM.args.batch.text..HDRM.args.gap.text..math.floor(GUI.HDR.gap.value)..' -a'
else
HDRM.arg_string = HDRM.arg_string..'-o '..df.sanitize_filename(out_path)
end
-- create run command and execute
local run_cmd = BuildExecuteCmd(HDRM)
resp = dsys.external_command(run_cmd)
if resp == 0 and not GUI.HDR.batch.value then
local imported = dt.database.import(out_path) -- import the new file
if GUI.Target.style.selected > 1 then -- apply selected style
local set_style = styles[GUI.Target.style.selected - 1]
dt.styles.apply(set_style , imported)
end
if GUI.Target.copy_tags.value then -- copy tags from the original file (ignore 'darktable' generated tags)
local all_tags = dt.tags.get_tags(source_raw)
for _,tag in pairs(all_tags) do
if string.match(tag.name, 'darktable|') == nil then dt.tags.attach(tag, imported) end
end
end
local set_tag = GUI.Target.add_tags.text
if set_tag ~= nil then -- add additional user-specified tags
for tag in string.gmatch(set_tag, '[^,]+') do
tag = CleanSpaces(tag)
tag = dt.tags.create(tag)
dt.tags.attach(tag, imported)
end
end
dt.print(_('HDRMerge completed successfully'))
else
dt.print_error(_('HDRMerge failed'))
dt.print(_('HDRMerge failed'))
end
end
-- GUI Elements --
local lbl_hdr = dt.new_widget('section_label'){
label = _('HDRMerge options')
}
temp = dt.preferences.read(mod, 'active_bps_ind', 'integer')
if not InRange(temp, 1, 3) then temp = 3 end
GUI.HDR.bps = dt.new_widget('combobox'){
label = _('bits per sample'),
tooltip =_('number of bits per sample in the output image'),
selected = temp,
'16','24','32',
changed_callback = function(self)
dt.preferences.write(mod, 'active_bps', 'integer', self.value)
dt.preferences.write(mod, 'active_bps_ind', 'integer', self.selected)
end,
reset_callback = function(self)
self.selected = 3
dt.preferences.write(mod, 'active_bps', 'integer', self.value)
dt.preferences.write(mod, 'active_bps_ind', 'integer', self.selected)
end
}
temp = dt.preferences.read(mod, 'active_size_ind', 'integer')
if not InRange(temp, 1, 3) then temp = 2 end
GUI.HDR.size = dt.new_widget('combobox'){
label = _('embedded preview size'),
tooltip =_('size of the embedded preview in output image'),
selected = temp,
_('none'),_('half'),_('full'),
changed_callback = function(self)
dt.preferences.write(mod, 'active_size', 'string', self.value)
dt.preferences.write(mod, 'active_size_ind', 'integer', self.selected)
end,
reset_callback = function(self)
self.selected = 2
dt.preferences.write(mod, 'active_size', 'string', self.value)
dt.preferences.write(mod, 'active_size_ind', 'integer', self.selected)
end
}
GUI.HDR.batch = dt.new_widget('check_button'){
label = _('batch mode'),
value = dt.preferences.read(mod, 'active_batch', 'bool'),
tooltip = _('enable batch mode operation \nNOTE: resultant files will NOT be auto-imported'),
clicked_callback = function(self)
dt.preferences.write(mod, 'active_batch', 'bool', self.value)
GUI.HDR.gap.sensitive = self.value
end,
reset_callback = function(self) self.value = false end
}
temp = dt.preferences.read(mod, 'active_gap', 'integer')
if not InRange(temp, 1, 3600) then temp = 3 end
GUI.HDR.gap = dt.new_widget('slider'){
label = _('batch gap [sec.]'),
tooltip = _('gap, in seconds, between batch mode groups'),
soft_min = 1,
soft_max = 30,
hard_min = 1,
hard_max = 3600,
step = 1,
digits = 0,
value = temp,
sensitive = GUI.HDR.batch.value,
reset_callback = function(self)
self.value = 3
end
}
local lbl_import = dt.new_widget('section_label'){
label = _('import options')
}
GUI.Target.style = dt.new_widget('combobox'){
label = _('apply style on import'),
tooltip = _('Apply selected style on auto-import to newly created image'),
selected = 1,
_('none'),
changed_callback = function(self)
dt.preferences.write(mod, 'active_style', 'string', self.value)
dt.preferences.write(mod, 'active_style_ind', 'integer', self.selected)
end,
reset_callback = function(self)
self.selected = 1
dt.preferences.write(mod, 'active_style', 'string', self.value)
dt.preferences.write(mod, 'active_style_ind', 'integer', self.selected)
end
}
for k=1, (styles_count-1) do
GUI.Target.style[k+1] = styles[k].name
end
temp = dt.preferences.read(mod, 'active_style_ind', 'integer')
if not InRange(temp, 1, styles_count) then temp = 1 end
GUI.Target.style.selected = temp
GUI.Target.copy_tags = dt.new_widget('check_button'){
label = _('copy tags'),
value = dt.preferences.read(mod, 'active_copy_tags', 'bool'),
tooltip = _('copy tags from first source image'),
clicked_callback = function(self) dt.preferences.write(mod, 'active_copy_tags', 'bool', self.value) end,
reset_callback = function(self) self.value = true end
}
temp = dt.preferences.read(mod, 'active_add_tags', 'string')
if temp == '' then temp = nil end
GUI.Target.add_tags = dt.new_widget('entry'){
tooltip = _('Additional tags to be added on import. Seperate with commas, all spaces will be removed'),
text = temp,
placeholder = _('Enter tags, seperated by commas'),
editable = true
}
GUI.run = dt.new_widget('button'){
label = _('merge'),
tooltip =_('run HDRMerge with the above specified settings'),
clicked_callback = function() main() end
}
GUI.exes.HDRMerge = dt.new_widget('file_chooser_button'){
title = _('Select HDRmerge executable'),
value = df.get_executable_path_preference(HDRM.name),
is_directory = false
}
GUI.exes.update = dt.new_widget('button'){
label = _('update'),
tooltip =_('update the binary path with current value'),
clicked_callback = function() ExeUpdate({HDRM}) end
}
GUI.options = dt.new_widget('box'){
orientation = 'vertical',
lbl_hdr,
GUI.HDR.bps,
GUI.HDR.size,
GUI.HDR.batch,
GUI.HDR.gap,
lbl_import,
GUI.Target.style,
GUI.Target.copy_tags,
GUI.Target.add_tags,
GUI.run
}
local exes_box = dt.new_widget('box'){
orientation = 'vertical',
GUI.exes.HDRMerge,
GUI.exes.update
}
GUI.stack = dt.new_widget('stack'){
GUI.options,
exes_box
}
if dt.preferences.read(mod, 'bin_exists', 'bool') then
GUI.stack.active = 1
else
GUI.stack.active = 2
end
dt.register_lib( -- register HDRMerge module
'HDRMerge_Lib', -- Module name
_('HDRMerge'), -- name
true, -- expandable
true, -- resetable
{[dt.gui.views.lighttable] = {'DT_UI_CONTAINER_PANEL_RIGHT_CENTER', 99}}, -- containers
dt.new_widget('box'){
orientation = 'vertical',
GUI.stack
}
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,436 @@
--[[
Face recognition for darktable
Copyright (c) 2017 Sebastian Witt
darktable 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.
darktable 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 darktable. If not, see <http://www.gnu.org/licenses/>.
]]
--[[
face_recognition
Add a new storage option to send images to face_recognition.
Images are exported to darktable tmp dir first.
A directory with known faces must exist, the image name are the
tag names which will be used.
Multiple images for one face can exist, add a number to it, the
number will be removed from the tag, for example:
People|IknowYou1.jpg
People|IknowYou2.jpg
People|Another.jpg
People|Youtoo.jpg
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
* https://github.com/ageitgey/face_recognition
* https://github.com/darktable-org/lua-scripts/tree/master/lib
USAGE
* require this file from your main luarc config file.
This plugin will add a new storage option and calls face_recognition after export.
]]
local dt = require "darktable"
local du = require "lib/dtutils"
local df = require "lib/dtutils.file"
local dtsys = require "lib/dtutils.system"
local gettext = dt.gettext
-- constants
local MODULE = "face_recognition"
local PS = dt.configuration.running_os == "windows" and '\\' or '/'
local OUTPUT = dt.configuration.tmp_dir .. PS .. "facerecognition.txt"
-- namespace
local fc = {}
-- ensure we meet the minimum api
du.check_min_api_version("5.0.0", "face_recognition")
-- Tell gettext where to find the .mo file translating messages for a particular domain
gettext.bindtextdomain("face_recognition", dt.configuration.config_dir.."/lua/locale/")
local function _(msgid)
return gettext.dgettext("face_recognition", msgid)
end
-- preferences
if not dt.preferences.read(MODULE, "initialized", "bool") then
dt.preferences.write(MODULE, "unknown_tag", "string", "unknown_person")
dt.preferences.write(MODULE, "ignore_tags", "string", "")
dt.preferences.write(MODULE, "tolerance", "float", 0.6)
dt.preferences.write(MODULE, "num_cores", "integer", 0)
dt.preferences.write(MODULE, "known_image_path", "directory", dt.configuration.config_dir .. "/face_recognition")
dt.preferences.write(MODULE, "export_format", "integer", 1)
dt.preferences.write(MODULE, "max_width", "integer", 1000)
dt.preferences.write(MODULE, "max_height", "integer", 1000)
dt.preferences.write(MODULE, "initialized", "bool", true)
end
local function build_image_table(images)
local image_table = {}
local file_extension = ""
local tmp_dir = dt.configuration.tmp_dir .. PS
local ff = fc.export_format.value
local cnt = 0
-- check for plugin-data and direct_edit and build image table accordingly
if string.match(ff, "JPEG") then
file_extension = ".jpg"
elseif string.match(ff, "PNG") then
file_extension = ".png"
elseif string.match(ff, "TIFF") then
file_extension = ".tif"
end
for _,img in ipairs(images) do
image_table[img] = tmp_dir .. df.get_basename(img.filename) .. file_extension
cnt = cnt + 1
end
return image_table, cnt
end
local function stop_job(job)
job.valid = false
end
local function do_export(img_tbl)
local exporter = nil
local upsize = false
local upscale = false
local ff = fc.export_format.value
local height = dt.preferences.read(MODULE, "max_height", "integer")
local width = dt.preferences.read(MODULE, "max_width", "integer")
local images = 0
for k,v in pairs(img_tbl) do
images = images + 1
end
-- get the export format parameters
if string.match(ff, "JPEG") then
exporter = dt.new_format("jpeg")
exporter.quality = 80
elseif string.match(ff, "PNG") then
exporter = dt.new_format("png")
exporter.bpp = 8
elseif string.match(ff, "TIFF") then
exporter = dt.new_format("tiff")
exporter.bpp = 8
end
exporter.max_height = height
exporter.max_width = width
-- export the images
local job = dt.gui.create_job(_("export images"), true, stop_job)
local exp_cnt = 0
local percent_step = 1.0 / images
job.percent = 0.0
for img,export in pairs(img_tbl) do
exp_cnt = exp_cnt + 1
dt.print(string.format(_("Exporting image %i of %i images"), exp_cnt, images))
exporter:write_image(img, export, upsize)
job.percent = job.percent + percent_step
end
job.valid = false
-- return success, or not
return true
end
local function save_preferences()
dt.preferences.write(MODULE, "unknown_tag", "string", fc.unknown_tag.text)
dt.preferences.write(MODULE, "ignore_tags", "string", fc.ignore_tags.text)
dt.preferences.write(MODULE, "max_width", "integer", tonumber(fc.width.text))
dt.preferences.write(MODULE, "max_height", "integer", tonumber(fc.height.text))
dt.preferences.write(MODULE, "num_cores", "integer", fc.num_cores.value)
local val = fc.tolerance.value
val = string.gsub(tostring(val), ",", ".")
dt.preferences.write(MODULE, "tolerance", "float", tonumber(val))
end
-- Check if image has ignored tag attached
local function ignoreByTag (image, ignoreTags)
local tags = image:get_tags ()
local ignoreImage = false
-- For each image tag
for _,t in ipairs (tags) do
-- Check if it contains a ignore tag
for _,it in ipairs (ignoreTags) do
if string.find (t.name, it, 1, true) then
-- The image has ignored tag attached
ignoreImage = true
dt.print_log ("Face recognition: Ignored tag: " .. it .. " found in " .. image.id .. ":" .. t.name)
end
end
end
return ignoreImage
end
local function cleanup(img_list)
for _, img in ipairs(img_list) do
os.remove(img)
end
os.remove(OUTPUT)
end
local function face_recognition ()
local bin_path = df.check_if_bin_exists("face_recognition")
if not bin_path then
dt.print(_("Face recognition not found"))
return
end
save_preferences()
-- Get preferences
local knownPath = dt.preferences.read(MODULE, "known_image_path", "directory")
local nrCores = dt.preferences.read(MODULE, "num_cores", "integer")
local ignoreTagString = dt.preferences.read(MODULE, "ignore_tags", "string")
local unknownTag = dt.preferences.read(MODULE, "unknown_tag", "string")
-- face_recognition uses -1 for all cores, we use 0 in preferences
if nrCores < 1 then
nrCores = -1
end
-- Split ignore tags (if any)
ignoreTags = {}
for tag in string.gmatch(ignoreTagString, '([^,]+)') do
table.insert (ignoreTags, tag)
dt.print_log ("Face recognition: Ignore tag: " .. tag)
end
-- list of exported images
local image_table, cnt = build_image_table(dt.gui.action_images)
if cnt > 0 then
local success = do_export(image_table)
if success then
-- do the face recognition
local img_list = {}
for img,v in pairs(image_table) do
table.insert (img_list, v)
end
-- Get path of exported images
local path = df.get_path (img_list[1])
dt.print_log ("Face recognition: Path to unknown images: " .. path)
local tolerance = dt.preferences.read(MODULE, "tolerance", "float")
local command = bin_path .. " --cpus " .. nrCores .. " --tolerance " .. tolerance .. " " .. knownPath .. " " .. path .. " > " .. OUTPUT
dt.print_log("Face recognition: Running command: " .. command)
dt.print(_("Starting face recognition..."))
dtsys.external_command(command)
-- Open output file
local f = io.open(OUTPUT, "rb")
if not f then
dt.print(_("Face recognition failed"))
else
dt.print(_("Face recognition finished"))
f:close ()
end
-- Read output
dt.print(_("processing results..."))
local result = {}
for line in io.lines(OUTPUT) do
local file, tag = string.match (line, "(.*),(.*)$")
tag = string.gsub (tag, "%d*$", "")
dt.print_log ("File:"..file .." Tag:".. tag)
if result[file] ~= nil then
table.insert (result[file], tag)
else
result[file] = {tag}
end
end
-- Attach tags
for file,tags in pairs(result) do
-- Find image in table
for img,file2 in pairs(image_table) do
if file == file2 then
for _,t in ipairs (tags) do
-- Check if image is ignored
if ignoreByTag (img, ignoreTags) then
dt.print_log("Face recognition: Ignoring image with ID " .. img.id)
else
-- Check of unrecognized unknown_person
if t == "unknown_person" then
t = unknownTag
end
dt.print_log ("ImgId:" .. img.id .. " Tag:".. t)
-- Create tag if it does not exists
local tag = dt.tags.create (t)
img:attach_tag (tag)
end
end
end
end
end
dt.print(_("face recognition complete"))
cleanup(img_list)
else
dt.print(_("image export failed"))
return
end
else
dt.print(_("no images selected"))
return
end
end
-- build the interface
fc.unknown_tag = dt.new_widget("entry"){
text = dt.preferences.read(MODULE, "unknown_tag", "string"),
tooltip = _("tag to be used for unknown person"),
editable = true,
}
fc.ignore_tags = dt.new_widget("entry"){
text = dt.preferences.read(MODULE, "ignore_tags", "string"),
tooltip = _("tags of images to ignore"),
editable = true,
}
fc.tolerance = dt.new_widget("slider"){
label = _("tolerance"),
tooltip = ("detection tolerance - 0.6 default - lower if too many faces detected"),
soft_min = 0.0,
hard_min = 0.0,
soft_max = 1.0,
soft_min = 1.0,
step = 0.1,
digits = 1,
value = 0.0,
}
fc.num_cores = dt.new_widget("slider"){
label = _("processor cores"),
tooltip = _("number of processor cores to use, 0 for all"),
soft_min = 0,
soft_max = 16,
hard_min = 0,
hard_max = 64,
step = 1,
digits = 0,
value = dt.preferences.read(MODULE, "num_cores", "integer"),
}
fc.known_image_path = dt.new_widget("file_chooser_button"){
title = _("known image directory"),
tooltip = _("face data directory"),
value = dt.preferences.read(MODULE, "known_image_path", "directory"),
is_directory = true,
changed_callback = function(this)
dt.preferences.write(MODULE, "known_image_path", "directory", this.value)
end
}
fc.export_format = dt.new_widget("combobox"){
label = _("export image format"),
tooltip = _("format for exported images"),
selected = dt.preferences.read(MODULE, "export_format", "integer"),
changed_callback = function(this)
dt.preferences.write(MODULE, "export_format", "integer", this.selected)
end,
"JPEG", "PNG", "TIFF",
}
fc.width = dt.new_widget("entry"){
text = tostring(dt.preferences.read(MODULE, "max_width", "integer")),
tooltip = _("maximum exported image width"),
editable = true,
}
fc.height = dt.new_widget("entry"){
text = tostring(dt.preferences.read(MODULE, "max_height", "integer")),
tooltip = _("maximum exported image height"),
editable = true,
}
fc.execute = dt.new_widget("button"){
label = "detect faces",
clicked_callback = function(this)
face_recognition()
end
}
local widgets = {
dt.new_widget("label"){ label = _("unknown person tag")},
fc.unknown_tag,
dt.new_widget("label"){ label = _("togs of images to ignore")},
fc.ignore_tags,
dt.new_widget("label"){ label = _("face data directory")},
fc.known_image_path,
}
if dt.configuration.running_os == "windows" or dt.configuration.running_os == "macos" then
table.insert(widgets, df.executable_path_widget({"face_recognition"}))
end
table.insert(widgets, dt.new_widget("section_label"){ label = _("processing options")})
table.insert(widgets, fc.tolerance)
table.insert(widgets, fc.num_cores)
table.insert(widgets, fc.export_format)
table.insert(widgets, dt.new_widget("box"){
orientation = "horizontal",
dt.new_widget("label"){ label = _("width ")},
fc.width,
})
table.insert(widgets, dt.new_widget("box"){
orientation = "horizontal",
dt.new_widget("label"){ label = _("height ")},
fc.height,
})
table.insert(widgets, fc.execute)
fc.widget = dt.new_widget("box"){
orientation = vertical,
table.unpack(widgets)
}
--fc.tolerance.value = dt.preferences.read(MODULE, "tolerance", "float")
-- Register
--dt.register_storage("module_face_recognition", _("Face recognition"), show_status, face_recognition)
dt.register_lib(
"face_recognition", -- Module name
_("face recognition"), -- Visible name
true, -- expandable
false, -- resetable
{[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 300}}, -- containers
fc.widget,
nil,-- view_enter
nil -- view_leave
)
fc.tolerance.value = dt.preferences.read(MODULE, "tolerance", "float")
--
-- vim: shiftwidth=2 expandtab tabstop=2 cindent syntax=lua

View File

@@ -48,7 +48,7 @@ dtutils_file.libdoc.functions["check_if_bin_exists"] = {
quoted and returned. If no preference is specified and the operating system is
linux then the which command is used to check for a binary in the path. If found
that path is returned. If no binary is found, false is returned.]],
Return_Value = [[result - string - the path of the binary, false if not found]],
Return_Value = [[result - string - the sanitized path of the binary, false if not found]],
Limitations = [[]],
Example = [[]],
See_Also = [[]],
@@ -70,9 +70,9 @@ function dtutils_file.check_if_bin_exists(bin)
if string.len(path) > 0 then
if dtutils_file.check_if_file_exists(path) then
if (string.match(path, ".exe$") or string.match(path, ".EXE$")) and dt.configuration.running_os ~= "windows" then
result = "wine " .. "\"" .. path .. "\""
result = dtutils_file.sanitize_filename("wine " .. path)
else
result = "\"" .. path .. "\""
result = dtutils_file.sanitize_filename(path)
end
end
elseif dt.configuration.running_os == "linux" then
@@ -80,7 +80,7 @@ function dtutils_file.check_if_bin_exists(bin)
local output = p:read("*a")
p:close()
if string.len(output) > 0 then
result = output:sub(1,-2)
result = dtutils_file.sanitize_filename(output:sub(1,-2))
end
end
return result
@@ -107,9 +107,14 @@ dtutils_file.libdoc.functions["split_filepath"] = {
function dtutils_file.split_filepath(str)
-- strip out single quotes from quoted pathnames
str = string.gsub(str, "'", "")
str = string.gsub(str, '"', '')
local result = {}
-- Thank you Tobias Jakobs for the awesome regular expression, which I tweaked a little
result["path"], result["filename"], result["basename"], result["filetype"] = string.match(str, "(.-)(([^\\/]-)%.?([^%.\\/]*))$")
if result["basename"] == "" and result["filetype"]:len() > 1 then
result["basename"] = result["filetype"]
result["filetype"] = ""
end
return result
end
@@ -222,15 +227,21 @@ dtutils_file.libdoc.functions["check_if_file_exists"] = {
}
function dtutils_file.check_if_file_exists(filepath)
local result
local result = false
if (dt.configuration.running_os == 'windows') then
filepath = string.gsub(filepath, '[\\/]+', '\\')
result = os.execute('if exist "'..filepath..'" (cmd /c exit 0) else (cmd /c exit 1)')
if not result then
result = false
local p = io.popen("if exist " .. filepath .. " (echo 'yes') else (echo 'no')")
local ans = p:read("*all")
p:close()
if string.match(ans, "yes") then
result = true
end
-- result = os.execute('if exist "'..filepath..'" (cmd /c exit 0) else (cmd /c exit 1)')
-- if not result then
-- result = false
-- end
elseif (dt.configuration.running_os == "linux") then
result = os.execute('test -e ' .. "\"" .. filepath .. "\"")
result = os.execute('test -e ' .. dtutils_file.sanitize_filename(filepath))
if not result then
result = false
end
@@ -266,7 +277,11 @@ dtutils_file.libdoc.functions["chop_filetype"] = {
function dtutils_file.chop_filetype(path)
local length = dtutils_file.get_filetype(path):len() + 2
return string.sub(path, 1, -length)
if length > 2 then
return string.sub(path, 1, -length)
else
return path
end
end
dtutils_file.libdoc.functions["file_copy"] = {
@@ -373,7 +388,7 @@ dtutils_file.libdoc.functions["filename_increment"] = {
"01" is added to the basename. If the filename already has an increment, then
1 is added to it and the filename returned.]],
Return_Value = [[result - string - the incremented filename]],
Limitations = [[]],
Limitations = [[The filename will be incremented to 99]],
Example = [[]],
See_Also = [[]],
Reference = [[]],
@@ -396,6 +411,9 @@ function dtutils_file.filename_increment(filepath)
if string.len(increment) > 2 then
-- we got the filename so set the increment to 01
increment = "01"
elseif increment == "99" then
dt.print_error("not incrementing, filename has already been incremented 99 times.")
return filepath
else
increment = string.format("%02d", tonumber(increment) + 1)
basename = string.gsub(basename, "_(%d-)$", "")
@@ -433,8 +451,9 @@ function dtutils_file.create_unique_filename(filepath)
while dtutils_file.check_if_file_exists(filepath) do
filepath = dtutils_file.filename_increment(filepath)
-- limit to 99 more exports of the original export
if string.match(dtutils_file.get_basename(filepath), "_(d-)$") == "99" then
break
local increment = string.match(dtutils_file.get_basename(filepath), "_(%d-)$")
if increment == "99" then
break
end
end
return filepath
@@ -576,6 +595,7 @@ dtutils_file.libdoc.functions["mkdir"] = {
License = [[]],
Copyright = [[]],
}
function dtutils_file.mkdir(path)
if not dtutils_file.check_if_file_exists(path) then
local mkdir_cmd = dt.configuration.running_os == "windows" and "mkdir" or "mkdir -p"

View File

@@ -167,7 +167,7 @@ dtutils_string.libdoc.functions["urlencode"] = {
function dtutils_string.urlencode(str)
if (str) then
str = string.gsub (str, "\n", "\r\n")
str = string.gsub (str, "([^%w ])", function () return string.format ("%%%02X", string.byte()) end)
str = string.gsub (str, "([^%w ])", function (c) return string.format ("%%%02X", string.byte(c)) end)
str = string.gsub (str, " ", "+")
end
return str
@@ -194,14 +194,41 @@ dtutils_string.libdoc.functions["sanitize"] = {
function dtutils_string.sanitize(str)
local result = ""
local os_quote = dt.configuration.running_os == "windows" and '"' or "'"
if dt.configuration.running_os == "windows" then
result = '"' .. str .. '"'
else
result = "'" .. str .. "'"
if dtutils_string.is_not_sanitized(str) then
result = os_quote .. str .. os_quote
end
return result
end
dtutils_string.libdoc.functions["is_not_sanitized"] = {
Name = [[is_not_sanitized]],
Synopsis = [[Check if a string has been sanitized]],
Usage = [[local ds = require "lib/dtutils.string"
local result = ds.is_not_sanitized(str)
str - string - the string that needs to be made safe]],
Description = [[is_not_sanitized checks a string to see if it
has been made safe use passing as an argument in a system command.]],
Return_Value = [[result - boolean - true if the string is not sanitized otherwise false]],
Limitations = [[]],
Example = [[]],
See_Also = [[]],
Reference = [[]],
License = [[]],
Copyright = [[]],
}
function dtutils_string.is_not_sanitized(str)
local os_quote = dt.configuration.running_os == "windows" and '"' or "'"
if string.match(str, os_quote .. ".*" .. os_quote) then
return false
else
return true
end
end
return dtutils_string

View File

@@ -1 +0,0 @@
/home/bill/src/plugins/lua-scripts/tools/plugin_manager.lua