diff --git a/README.md b/README.md index 49551e5..2381dbc 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ 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 +[photils](https://github.com/scheckmedia/photils-dt)|No|LM|Automatic tag suggestions for your images quicktag|Yes|LMW|Create shortcuts for quickly applying tags rate_group|Yes|LMW|Apply or remove a star rating from grouped images rename-tags|Yes|LMW|Change a tag name diff --git a/contrib/photils.lua b/contrib/photils.lua new file mode 100644 index 0000000..85a2275 --- /dev/null +++ b/contrib/photils.lua @@ -0,0 +1,444 @@ +--[[ photils Auto Tagging plugin + copyright (c) 2020 Tobias Scheck + + 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 . +--]] + +--[[ + A darktable plugin that tries to predict keywords based on the selected image. + This plugin uses photils-cli to handle this task. Photils-cli is an application + that passes the image through a neural network, classifies it, and extracts the + suggested tags. Everything happens offline without the need that your data are + sent over the internet. + + ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT + * photils-cli - https://github.com/scheckmedia/photils-cli at the moment only + available for Linux and MacOS + + USAGE + * require this script from your main lua file + To do this add this line to the file .config/darktable/luarc: + require "contrib/photils" + * Select an image + * Press "get tags" + * Select the tags you want from a list of suggestions + * Press "Attach .. Tags" to add the selected tags to your image +--]] + +local dt = require "darktable" +local du = require "lib/dtutils" +local df = require "lib/dtutils.file" +local dtsys = require "lib/dtutils.system" + +local MODULE_NAME = "photils" +du.check_min_api_version("5.0.0", MODULE_NAME) + +local PS = dt.configuration.running_os == "windows" and "\\" or "/" +local gettext = dt.gettext +gettext.bindtextdomain(MODULE_NAME, + dt.configuration.config_dir .. PS .. "lua" .. PS .. "locale" .. PS) + +local exporter = dt.new_format("jpeg") +exporter.quality = 80 +exporter.max_height = 224 +exporter.max_width = 224 + +-- helper functions + +local function _(msgid) + return gettext.dgettext(MODULE_NAME, msgid) +end + +local function num_keys(tbl) + local num = 0 + for _ in pairs(tbl) do num = num + 1 end + return num +end + +local function has_key(tbl, value) + for k, _ in pairs(tbl) do + if k == value then + return true + end + end + + return false +end + +local photils_installed = df.check_if_bin_exists("photils-cli") + +--[[ + local state object + + maybe per_page is a preference variable but I think 10 + is a good value for the ui +]] +local PHOTILS = { + tags = {}, + confidences = {}, + page = 1, + per_page = 10, + selected_tags = {}, + in_pagination = false, + tagged_image = "" +} + +local GUI = { + container = dt.new_widget("box") { + orientation = "vertical", + sensitive = true, + dt.new_widget("button") { + label = _("get tags"), + sensitive = photils_installed, + clicked_callback = function() PHOTILS.on_tags_clicked() end + }, + reset_callback = function() PHOTILS.on_reset(true) end + }, + stack = dt.new_widget("stack"), + prev_button = dt.new_widget("button") { + label = "<", + sensitive = false, + clicked_callback = function() + PHOTILS.page = PHOTILS.page - 1 + PHOTILS.paginate() + end + }, + next_button = dt.new_widget("button") { + label = ">", + sensitive = false, + clicked_callback = function() + PHOTILS.page = PHOTILS.page + 1 + PHOTILS.paginate() + end + }, + tag_box = dt.new_widget("box") {orientation = "vertical"}, + tag_view = dt.new_widget("box") {orientation = "vertical"}, + page_label = dt.new_widget("label") {label = ""}, + error_view = dt.new_widget("box") {orientation = "vertical"}, + warning_label = dt.new_widget("label") { + label = "" + }, + restart_required_label = dt.new_widget("label") { + label = _("requires a restart to be applied") + }, + attach_button = dt.new_widget("button") { + label = "", + sensitive = false, + clicked_callback = function() PHOTILS.attach_tags() end + }, + confidence_slider = dt.new_widget("slider") { + step = 1, + digits = 0, + value = 90, + hard_max = 100, + hard_min = 0, + soft_max = 100, + soft_min = 0, + label = _("min confidence value") + }, + warning = dt.new_widget("label") +} + +function PHOTILS.image_changed() + local current_image = tostring(dt.gui.selection()[1]) + if current_image ~= PHOTILS.tagged_image then + if PHOTILS.tagged_image ~= "" then + PHOTILS.tagged_image_has_changed() + end + + PHOTILS.tagged_image = tostring(current_image) + end +end + +function PHOTILS.tagged_image_has_changed() + GUI.warning.label = _("The suggested tags were not generated\n for the currently selected image!") +end + +function PHOTILS.paginate() + PHOTILS.in_pagination = true + local num_pages = math.ceil(#PHOTILS.tags / PHOTILS.per_page) + GUI.page_label.label = string.format(_(" page %s of %s "), PHOTILS.page, + num_pages) + + if PHOTILS.page <= 1 then + PHOTILS.page = 1 + GUI.prev_button.sensitive = false + else + GUI.prev_button.sensitive = true + end + + if PHOTILS.page > num_pages - 1 then + PHOTILS.page = num_pages + GUI.next_button.sensitive = false + else + GUI.next_button.sensitive = true + end + + --[[ + calculates the start positon in the tag array based on the current page + and takes N tags from that array to show these in darktable + e.g. page 1 goes from 1 to 10, page 2 from 11 to 20 a.s.o. + the paginaton approach is related to a problem with the dynamic addition + of mutliple widgets https://github.com/darktable-org/darktable/issues/4934#event-3318100463 + ]]-- + local offset = ((PHOTILS.page - 1) * PHOTILS.per_page) + 1 + local tag_index = 1 + for i = offset, offset + PHOTILS.per_page - 1, 1 do + local tag = PHOTILS.tags[i] + local conf = PHOTILS.confidences[i] + + GUI.tag_box[tag_index].value = has_key(PHOTILS.selected_tags, tag) + + if tag then + if dt.preferences.read(MODULE_NAME, "show_confidence", "bool") then + tag = tag .. string.format(" (%.3f)", conf) + end + + GUI.tag_box[tag_index].label = tag + GUI.tag_box[tag_index].sensitive = true + else + GUI.tag_box[tag_index].label = "" + GUI.tag_box[tag_index].sensitive = false + end + tag_index = tag_index + 1 + end + + PHOTILS.in_pagination = false +end + +function PHOTILS.attach_tags() + local image = dt.gui.selection()[1] + for tag, _ in pairs(PHOTILS.selected_tags) do + local dt_tag = dt.tags.create(tag) + dt.tags.attach(dt_tag, image) + end + + dt.print(_("Tags successfully attached to image")) +end + +function PHOTILS.get_tmp_file() + local tmp_file = os.tmpname() + if dt.configuration.running_os == "windows" then + tmp_file = dt.configuration.tmp_dir .. tmp_file -- windows os.tmpname() defaults to root directory + end + + local f = io.open(tmp_file, "w") + if not f then + dt.print_log(string.format(_("Error writing to `%s`"), tmp_file)) + os.remove(tmp_file) + return nil + end + + return tmp_file +end + +function PHOTILS.get_tags(image, with_export) + local tmp_file = PHOTILS.get_tmp_file() + local in_arg = df.sanitize_filename(tostring(image)) + local out_arg = df.sanitize_filename(tmp_file) + local executable = photils_installed + + if dt.configuration.running_os == "macos" then + executable = executable .. "/Contents/MacOS/photils-cli" + end + + if with_export then + dt.print_log("use export to for prediction") + local export_file = PHOTILS.get_tmp_file() + exporter:write_image(image, export_file) + in_arg = df.sanitize_filename(tostring(export_file)) + end + + local command = executable .. " -c " .. " -i " .. in_arg .. " -o " .. out_arg + + local ret = dtsys.external_command(command) + if ret > 0 then + dt.print_error(string.format("command %s returned error code %d", command, ret)) + os.remove(tmp_file) + + -- try to export the image and run tagging + if not with_export then + return PHOTILS.get_tags(image, true) + end + + return false + end + + for i = #PHOTILS.tags, 1, -1 do + PHOTILS.tags[i] = nil + PHOTILS.confidences[i] = nil + end + + for tag in io.lines(tmp_file) do + local splitted = du.split(tag, ":") + if 100 * tonumber(splitted[2]) >= GUI.confidence_slider.value then + PHOTILS.tags[#PHOTILS.tags + 1] = splitted[1] + PHOTILS.confidences[#PHOTILS.confidences+1] = splitted[2] + end + end + + dt.print(string.format(_("%s found %d tags for your image"), MODULE_NAME, + #PHOTILS.tags)) + os.remove(tmp_file) + + return true +end + +function PHOTILS.on_tags_clicked() + PHOTILS.page = 1 + GUI.warning.label = "" + + PHOTILS.on_reset(false) + + local images = dt.gui.selection() + + if #images == 0 then + dt.print(_("No image selected.")) + dt.control.sleep(2000) + else + if #images > 1 then + dt.print(_("This plugin can only handle a single image.")) + dt.gui.selection({images[1]}) + dt.control.sleep(2000) + end + + if not PHOTILS.get_tags(images[1], true) then + local msg = string.format(_("%s failed, see terminal output for details"), MODULE_NAME) + GUI.warning_label.label = msg + GUI.stack.active = GUI.error_view + dt.print(msg) + return + end + + if #PHOTILS.tags == 0 then + local msg = string.format(_("no tags where found"), MODULE_NAME) + GUI.warning_label.label = msg + GUI.stack.active = GUI.error_view + return + end + + GUI.stack.active = GUI.tag_view + PHOTILS.paginate() + end +end + +function PHOTILS.tag_selected(tag_button) + if PHOTILS.in_pagination then return end + + if tag_button.value then + local tag = tag_button.label + if dt.preferences.read(MODULE_NAME, "show_confidence", "bool") then + local idx = string.find(tag, "%(") - 2 + tag = string.sub(tag, 0, idx) + end + + PHOTILS.selected_tags[tag] = tag + else + PHOTILS.selected_tags[tag_button.label] = nil + end + + local num_selected = num_keys(PHOTILS.selected_tags) + if num_selected == 0 then + GUI.attach_button.label = "" + GUI.attach_button.sensitive = false + else + GUI.attach_button.label = string.format(_("attach %d tags"), + num_selected) + GUI.attach_button.sensitive = true + end +end + +function PHOTILS.on_reset(with_view) + if with_view then GUI.stack.active = 1 end + + for k, _ in pairs(PHOTILS.selected_tags) do + PHOTILS.selected_tags[k] = nil + end + + for _, v in ipairs(GUI.tag_box) do + v.value = false + end + + GUI.attach_button.label = "" + GUI.attach_button.sensitive = false +end + +-- add a fix number of buttons +for _ = 1, PHOTILS.per_page, 1 do + local btn_tag = dt.new_widget("check_button") { + label = "", + sensitive = false, + clicked_callback = PHOTILS.tag_selected + } + + table.insert(GUI.tag_box, btn_tag) +end + +if not photils_installed then + GUI.warning_label.label = _("photils-cli not found") + dt.print_log(_("photils-cli not found")) +else + GUI.warning_label.label = _("Select an image, click \"get tags\" and get \nsuggestions for tags.") +end + +GUI.pagination = dt.new_widget("box") { + orientation = "horizontal", + GUI.prev_button, + GUI.page_label, + GUI.next_button +} + + +table.insert(GUI.error_view, GUI.warning_label) +if not photils_installed then + table.insert(GUI.error_view, df.executable_path_widget({"photils-cli"})) + table.insert(GUI.error_view, GUI.restart_required_label) +end +table.insert(GUI.stack, GUI.error_view) +table.insert(GUI.stack, GUI.tag_view) + +table.insert(GUI.tag_view, GUI.pagination) +table.insert(GUI.tag_view, GUI.tag_box) +table.insert(GUI.tag_view, GUI.attach_button) +table.insert(GUI.tag_view, GUI.warning) + +table.insert(GUI.container, GUI.confidence_slider) +table.insert(GUI.container, GUI.stack) + +GUI.stack.active = 1 + +local plugin_display_views = { + [dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}, + [dt.gui.views.darkroom] = {"DT_UI_CONTAINER_PANEL_LEFT_CENTER", 100} +} + + +-- uses photils: prefix because script settings are all together and not seperated by script +dt.preferences.register(MODULE_NAME, + "show_confidence", + "bool", + _("photils: show confidence value"), + _("if enabled, the confidence value for each tag is displayed"), + true) + +dt.register_event("mouse-over-image-changed",PHOTILS.image_changed) +dt.register_lib(MODULE_NAME, + "photils autotagger", + true, + true, + plugin_display_views, + GUI.container, + nil, + nil +) diff --git a/locale/de_DE/LC_MESSAGES/photils.po b/locale/de_DE/LC_MESSAGES/photils.po new file mode 100644 index 0000000..b81dbc7 --- /dev/null +++ b/locale/de_DE/LC_MESSAGES/photils.po @@ -0,0 +1,73 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: Lua photils\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-05-12 13:12+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "get tags" +msgstr "hole stichwörter" + +msgid "requires a restart to be applied" +msgstr "erfordert einen Neustart um angewendet zu werden" + +msgid "min confidence value" +msgstr "min vertrauenswert" + +msgid "" +"The suggested tags were not generated\n" +" for the currently selected image!" +msgstr "" +"Die vorgeschlagenen Stichwörter wurden für das\n" +"aktuell ausgewählte Bild nicht generiert!" + +#, lua-format +msgid " page %s of %s " +msgstr " seite %s von %s " + +msgid "Tags successfully attached to image" +msgstr "Stichwörter erfolgreich an Bild angefügt" + +#, lua-format +msgid "Error writing to `%s`" +msgstr "Fehler beim Schreiben in `%s`" + +#, lua-format +msgid "%s failed, see terminal output for details" +msgstr "%s fehlgeschlagen, siehe Terminal-Ausgabe für Details" + +#, lua-format +msgid "%s found %d tags for your image" +msgstr "%s gefunden %d-Stichwörter für Ihr Bild" + +msgid "No image selected." +msgstr "Kein Bild ausgewählt." + +msgid "This plugin can only handle a single image." +msgstr "Dieses Plugin kann nur ein einziges Bild verarbeiten." + +#, lua-format +msgid "attach %d tags" +msgstr "%d stichwörter anhängen" + +msgid "photils-cli not found" +msgstr "photils-cli nicht gefunden" + +msgid "" +"Select an image, click \"Get Tags\" and get \n" +"suggestions for tags." +msgstr "" +"Wählen Sie ein Bild aus, drücken Sie \"Hole Stichwörter\"\n" +" und erhalten Sie Vorschläge für Stichwörter."