From 33f91af1af429500686025114fe4d49e9ab744a1 Mon Sep 17 00:00:00 2001 From: Tobias Scheck Date: Sat, 6 Jun 2020 12:39:26 +0200 Subject: [PATCH] add photils plugin --- README.md | 1 + contrib/photils.lua | 416 ++++++++++++++++++++++++++++ locale/de_DE/LC_MESSAGES/photils.po | 83 ++++++ 3 files changed, 500 insertions(+) create mode 100644 contrib/photils.lua create mode 100644 locale/de_DE/LC_MESSAGES/photils.po 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..102bd4d --- /dev/null +++ b/contrib/photils.lua @@ -0,0 +1,416 @@ +--[[ 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 which tries to predict keywords based on the selected image. + This plugin uses photils-cli to handle this task. Photils-cli is an application + which passes the image through a neural network and generates a feature vector. + This feature vector will be used to find similar images in a self-hosted online + database and the corresponding tags. The usage of the feature vector has this advantage + that at no time your selected image will leave the PC. Rather a representation + of the image is sent. + + ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT + * photils-cli - https://github.com/scheckmedia/photils-cli + + 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 = {}, + 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 + + 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] + GUI.tag_box[tag_index].value = has_key(PHOTILS.selected_tags, tag) + + if tag then + 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 + 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] + 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 + PHOTILS.selected_tags[tag_button.label] = tag_button.label + 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} +} + +-- dt.control.dispatch(PHOTILS.image_changed) +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..f1480a1 --- /dev/null +++ b/locale/de_DE/LC_MESSAGES/photils.po @@ -0,0 +1,83 @@ +# 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" + +#: ../../../contrib/photils.lua:97 +msgid "Get Tags" +msgstr "Hole Stichwörter" + +#: ../../../contrib/photils.lua:128 +msgid "requires a restart to be applied" +msgstr "erfordert die Neustart um angewendet zu werden" + +#: ../../../contrib/photils.lua:155 +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!" + +#: ../../../contrib/photils.lua:161 +#, lua-format +msgid " Page %s of %s " +msgstr " Seite %s von %s " + +#: ../../../contrib/photils.lua:204 +msgid "Tags successfully attached to image" +msgstr "Stichwörter erfolgreich an Bild angefügt" + +#: ../../../contrib/photils.lua:215 +#, lua-format +msgid "Error writing to `%s`" +msgstr "Fehler beim Schreiben in `%s`" + +#: ../../../contrib/photils.lua:229 +#, lua-format +msgid "%s failed, see terminal output for details" +msgstr "%s fehlgeschlagen, siehe Terminal-Ausgabe für Details" + +#: ../../../contrib/photils.lua:243 +#, lua-format +msgid "%s found %d tags for your image" +msgstr "%s gefunden %d-Stichwörter für Ihr Bild" + +#: ../../../contrib/photils.lua:257 +msgid "No image selected." +msgstr "Kein Bild ausgewählt." + +#: ../../../contrib/photils.lua:262 +msgid "This plugin can only handle a single image." +msgstr "Dieses Plugin kann nur ein einziges Bild verarbeiten." + +#: ../../../contrib/photils.lua:293 +#, lua-format +msgid "Attach %d Tags" +msgstr "%d-Stichwörter anhängen" + +#: ../../../contrib/photils.lua:326 ../../../contrib/photils.lua:327 +msgid "photils-cli not found" +msgstr "photils-cli nicht gefunden" + +#: ../../../contrib/photils.lua:329 +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."