Last active
June 9, 2021 17:12
-
-
Save brucejackson/77d5068cba1484bce5d537d660bb64c4 to your computer and use it in GitHub Desktop.
Gramps - PhotoTaggingGramplet.py - Modified to display person regions embedded in photos
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# Gramps - a GTK+/GNOME based genealogy program | |
# | |
# Copyright (C) 2013 Artem Glebov <artem.glebov@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 2 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, write to the Free Software | |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | |
# | |
# $Id: $ | |
#------------------------------------------------------------------------- | |
# | |
# Standard python modules | |
# | |
#------------------------------------------------------------------------- | |
import sys | |
import os | |
import pickle | |
import logging | |
LOG = logging.getLogger(".PhotoTaggingGramplet") | |
#------------------------------------------------------------------------- | |
# | |
# GTK/Gnome modules | |
# | |
#------------------------------------------------------------------------- | |
from gi.repository import Gtk | |
from gi.repository import Gdk | |
from gi.repository import GdkPixbuf | |
from gi.repository import GObject | |
from gi.repository import GExiv2 | |
#------------------------------------------------------------------------- | |
# | |
# gramps modules | |
# | |
#------------------------------------------------------------------------- | |
from gramps.gen.utils.db import get_birth_or_fallback, get_death_or_fallback | |
from gramps.gen.utils.file import media_path_full | |
from gramps.gen.errors import WindowActiveError | |
from gramps.gen.config import config | |
from gramps.gen.db import DbTxn | |
from gramps.gen.display.name import displayer as name_displayer | |
from gramps.gen.plug import Gramplet, MenuOptions | |
from gramps.gen.lib import MediaRef, Person | |
from gramps.gen.lib.date import Today | |
from gramps.gui.editors.editperson import EditPerson | |
from gramps.gui.selectors import SelectorFactory | |
from gramps.gen.plug.menu import BooleanOption, NumberOption | |
from gramps.gui.plug import PluginWindows | |
from gramps.gui.widgets import SelectionWidget, Region | |
from gramps.gui.ddtargets import DdTargets | |
from gramps.gui.display import display_url | |
from gramps.gen.const import GRAMPS_LOCALE as glocale | |
try: | |
_ = glocale.get_addon_translator(__file__).sgettext | |
except ValueError: | |
_ = glocale.translation.sgettext | |
#------------------------------------------------------------------------- | |
# | |
# face detection module | |
# | |
#------------------------------------------------------------------------- | |
sys.path.append(os.path.abspath(os.path.dirname(__file__))) | |
import facedetection | |
#------------------------------------------------------------------------- | |
# | |
# configuration | |
# | |
#------------------------------------------------------------------------- | |
GRAMPLET_CONFIG_NAME = "phototagginggramplet" | |
CONFIG = config.register_manager(GRAMPLET_CONFIG_NAME) | |
CONFIG.register("detection.box_size", (50, 50)) | |
CONFIG.register("detection.inside_existing_boxes", False) | |
CONFIG.register("detection.sensitivity", 10) | |
CONFIG.register("selection.replace_without_asking", False) | |
CONFIG.load() | |
CONFIG.save() | |
MIN_FACE_SIZE = CONFIG.get("detection.box_size") | |
REPLACE_WITHOUT_ASKING = CONFIG.get("selection.replace_without_asking") | |
DETECT_INSIDE_EXISTING_BOXES = CONFIG.get("detection.inside_existing_boxes") | |
SENSITIVITY = CONFIG.get("detection.sensitivity") | |
# translators: you can link here to localized wiki page (if exists) | |
WIKI_URL = _("https://www.gramps-project.org/wiki/index.php/" | |
"Photo_Tagging_Gramplet") | |
def save_config(): | |
CONFIG.set("detection.box_size", MIN_FACE_SIZE) | |
CONFIG.set("detection.inside_existing_boxes", DETECT_INSIDE_EXISTING_BOXES) | |
CONFIG.set("detection.sensitivity", SENSITIVITY) | |
CONFIG.set("selection.replace_without_asking", REPLACE_WITHOUT_ASKING) | |
CONFIG.save() | |
#------------------------------------------------------------------------- | |
# | |
# Gramplet Options | |
# | |
#------------------------------------------------------------------------- | |
THUMBNAIL_IMAGE_SIZE = (50, 50) | |
class PhotoTaggingOptions(MenuOptions): | |
def __init__(self): | |
MenuOptions.__init__(self) | |
def add_menu_options(self, menu): | |
category_name = _("Selection") | |
self.replace_without_asking = BooleanOption( | |
_("Replace existing references to the person " | |
"being assigned without asking"), | |
REPLACE_WITHOUT_ASKING) | |
menu.add_option(category_name, "replace_without_asking", | |
self.replace_without_asking) | |
category_name = _("Face detection") | |
width, height = MIN_FACE_SIZE | |
sensitivity = SENSITIVITY | |
self.min_face_width = NumberOption(_("Minimum face width (px)"), | |
width, 1, 1000, 1) | |
self.min_face_height = NumberOption(_("Minimum face height (px)"), | |
height, 1, 1000, 1) | |
self.detect_inside_existing_boxes = BooleanOption( | |
_("Detect faces inside existing boxes"), | |
DETECT_INSIDE_EXISTING_BOXES) | |
self.sensitivity = NumberOption(_("Sensitivity (1 min .. 20 max)"), | |
sensitivity, 1, 20, 1) | |
menu.add_option(category_name, "min_face_width", self.min_face_width) | |
menu.add_option(category_name, "min_face_height", self.min_face_height) | |
menu.add_option(category_name, "sensitivity", self.sensitivity) | |
menu.add_option(category_name, "detect_inside_existing_boxes", | |
self.detect_inside_existing_boxes) | |
def update_settings(self): | |
global REPLACE_WITHOUT_ASKING | |
global DETECT_INSIDE_EXISTING_BOXES | |
global MIN_FACE_SIZE | |
global SENSITIVITY | |
REPLACE_WITHOUT_ASKING = self.replace_without_asking.get_value() | |
DETECT_INSIDE_EXISTING_BOXES = self.detect_inside_existing_boxes.get_value() | |
width = self.min_face_width.get_value() | |
height = self.min_face_height.get_value() | |
MIN_FACE_SIZE = (width, height) | |
SENSITIVITY = self.sensitivity.get_value() | |
save_config() | |
#------------------------------------------------------------------------- | |
# | |
# Settings Dialog | |
# | |
#------------------------------------------------------------------------- | |
class SettingsDialog(PluginWindows.ToolManagedWindowBase): | |
def __init__(self, dbstate, uistate, title, options): | |
self.dbstate = dbstate | |
self.uistate = uistate | |
self.title = title | |
self.options = options | |
PluginWindows.ToolManagedWindowBase.__init__(self, dbstate, uistate, | |
None, "SettingsDialog") | |
self.ok.set_label(_("_OK")) | |
def get_title(self): | |
return self.title | |
def on_ok_clicked(self, obj): | |
self.options.update_settings() | |
self.close() | |
#------------------------------------------------------------------------- | |
# | |
# Photo Tagging Gramplet | |
# | |
#------------------------------------------------------------------------- | |
class PhotoTaggingGramplet(Gramplet): | |
def init(self): | |
self.regions = [] | |
self.age_precision = config.get('preferences.age-display-precision') | |
self.build_context_menu() | |
self.gui.WIDGET = self.build_gui() | |
self.gui.get_container_widget().remove(self.gui.textview) | |
self.gui.get_container_widget().add(self.gui.WIDGET) | |
self.top.show_all() | |
def on_save(self): | |
CONFIG.save() | |
# ====================================================== | |
# building the GUI | |
# ====================================================== | |
def build_gui(self): | |
""" | |
Build the GUI interface. | |
""" | |
self.top = Gtk.VBox() | |
hpaned = Gtk.HPaned() | |
button_panel = Gtk.HBox() | |
self.button_index = Gtk.ToolButton(stock_id=Gtk.STOCK_INDEX) | |
self.button_add = Gtk.ToolButton(stock_id=Gtk.STOCK_ADD) | |
self.button_del = Gtk.ToolButton(stock_id=Gtk.STOCK_REMOVE) | |
self.button_clear = Gtk.ToolButton(stock_id=Gtk.STOCK_CLEAR) | |
self.button_edit = Gtk.ToolButton(stock_id=Gtk.STOCK_EDIT) | |
self.button_zoom_in = Gtk.ToolButton(stock_id=Gtk.STOCK_ZOOM_IN) | |
self.button_zoom_out = Gtk.ToolButton(stock_id=Gtk.STOCK_ZOOM_OUT) | |
# set custom icon for face detect button | |
self.button_detect = Gtk.ToolButton() | |
theme = Gtk.IconTheme.get_default() | |
face_detect_icon = theme.lookup_icon('gramps-face-detection', 24, | |
Gtk.IconLookupFlags.FORCE_SVG) | |
if face_detect_icon is not None: | |
self.button_detect.set_icon_name('gramps-face-detection') | |
else: | |
img = Gtk.Image() | |
path, filename = os.path.split(__file__) | |
face_detect_icon = os.path.join(path, 'gramps-face-detection.svg') | |
img.set_from_file(face_detect_icon) | |
self.button_detect.set_icon_widget(img) | |
self.button_settings = Gtk.ToolButton(stock_id=Gtk.STOCK_PREFERENCES) | |
self.button_help = Gtk.ToolButton(stock_id=Gtk.STOCK_HELP) | |
self.button_index.connect("clicked", self.sel_person_clicked) | |
self.button_add.connect("clicked", self.add_person_clicked) | |
self.button_del.connect("clicked", self.clear_ref_clicked) | |
self.button_clear.connect("clicked", self.del_region_clicked) | |
self.button_edit.connect("clicked", self.edit_person_clicked) | |
self.button_zoom_in.connect("clicked", self.zoom_in_clicked) | |
self.button_zoom_out.connect("clicked", self.zoom_out_clicked) | |
self.button_detect.connect("clicked", self.detect_faces_clicked) | |
self.button_settings.connect("clicked", self.settings_clicked) | |
self.button_help.connect("clicked", self.on_help_clicked) | |
button_panel.pack_start(self.button_index, | |
expand=False, fill=False, padding=5) | |
button_panel.pack_start(self.button_add, | |
expand=False, fill=False, padding=5) | |
button_panel.pack_start(self.button_del, | |
expand=False, fill=False, padding=5) | |
button_panel.pack_start(self.button_clear, | |
expand=False, fill=False, padding=5) | |
button_panel.pack_start(self.button_edit, | |
expand=False, fill=False, padding=5) | |
button_panel.pack_start(self.button_zoom_in, | |
expand=False, fill=False, padding=5) | |
button_panel.pack_start(self.button_zoom_out, | |
expand=False, fill=False, padding=5) | |
button_panel.pack_start(self.button_detect, | |
expand=False, fill=False, padding=5) | |
button_panel.pack_start(self.button_settings, | |
expand=False, fill=False, padding=5) | |
button_panel.pack_start(self.button_help, | |
expand=False, fill=False, padding=5) | |
self.button_index.set_tooltip_text(_("Select Person")) | |
self.button_add.set_tooltip_text(_("Add Person")) | |
self.button_del.set_tooltip_text(_("Clear Reference")) | |
self.button_clear.set_tooltip_text(_("Remove Selection")) | |
self.button_edit.set_tooltip_text(_("Edit referenced Person")) | |
self.button_zoom_in.set_tooltip_text(_("Zoom In")) | |
self.button_zoom_out.set_tooltip_text(_("Zoom Out")) | |
if facedetection.computer_vision_available: | |
text = _("Detect faces") | |
else: | |
text = _("Detect faces (OpenCV module required)") | |
self.button_detect.set_tooltip_text(text) | |
self.button_settings.set_tooltip_text(_("Settings")) | |
self.button_help.set_tooltip_text(_("Help")) | |
self.top.pack_start(button_panel, expand=False, fill=True, padding=5) | |
self.selection_widget = SelectionWidget() | |
self.selection_widget.set_size_request(200, -1) | |
self.selection_widget.connect("region-modified", self.region_modified) | |
self.selection_widget.connect("region-created", self.region_created) | |
self.selection_widget.connect("region-selected", self.region_selected) | |
self.selection_widget.connect("selection-cleared", | |
self.selection_cleared) | |
self.selection_widget.connect("right-button-clicked", | |
self.right_button_clicked) | |
self.selection_widget.connect("zoomed-in", self.zoomed) | |
self.selection_widget.connect("zoomed-out", self.zoomed) | |
# Can drop a PERSON here: | |
tglist = Gtk.TargetList.new([]) | |
tglist.add(DdTargets.PERSON_LINK.atom_drag_type, | |
DdTargets.PERSON_LINK.target_flags, | |
DdTargets.PERSON_LINK.app_id) | |
# Can drop a LIST of HANDLES here: | |
tglist.add(DdTargets.HANDLE_LIST.atom_drag_type, | |
DdTargets.HANDLE_LIST.target_flags, | |
DdTargets.HANDLE_LIST.app_id) | |
# Drag and Drop for selection widget: | |
self.selection_widget.event_box.drag_dest_set( | |
Gtk.DestDefaults.MOTION | | |
Gtk.DestDefaults.DROP, | |
[], | |
Gdk.DragAction.COPY) | |
self.selection_widget.event_box.drag_dest_set_target_list(tglist) | |
self.selection_widget.event_box.connect( | |
'drag_data_received', | |
lambda *args: self.drag_data_received(*args, on_image=True)) | |
# End Drag and Drop for selection widget | |
hpaned.pack1(self.selection_widget, resize=True, shrink=False) | |
self.treestore = Gtk.TreeStore(int, GdkPixbuf.Pixbuf, str, str, str) | |
self.treeview = Gtk.TreeView(model=self.treestore) | |
self.treeview.set_size_request(400, -1) | |
self.treeview.connect("cursor-changed", self.cursor_changed) | |
self.treeview.connect("row-activated", self.row_activated) | |
self.treeview.connect("button-press-event", self.row_mouse_click) | |
column1 = Gtk.TreeViewColumn(title='') | |
column2 = Gtk.TreeViewColumn(title=_('Preview')) | |
column3 = Gtk.TreeViewColumn(title=_('Person')) | |
column4 = Gtk.TreeViewColumn(title=_('Age')) | |
column5 = Gtk.TreeViewColumn(title=_('XMP Region Name')) | |
self.treeview.append_column(column1) | |
self.treeview.append_column(column2) | |
self.treeview.append_column(column3) | |
self.treeview.append_column(column4) | |
self.treeview.append_column(column5) | |
cell1 = Gtk.CellRendererText() | |
cell2 = Gtk.CellRendererPixbuf() | |
cell3 = Gtk.CellRendererText() | |
cell4 = Gtk.CellRendererText() | |
cell5 = Gtk.CellRendererText() | |
column1.pack_start(cell1, expand=True) | |
column1.add_attribute(cell1, 'text', 0) | |
column2.pack_start(cell2, expand=True) | |
column2.add_attribute(cell2, 'pixbuf', 1) | |
column3.pack_start(cell3, expand=True) | |
column3.add_attribute(cell3, 'text', 2) | |
column3.set_resizable(True) | |
column3.set_reorderable(True) | |
column3.set_min_width(20) | |
column4.pack_start(cell4, expand=True) | |
column4.add_attribute(cell4, 'text', 3) | |
column4.set_resizable(True) | |
column4.set_reorderable(True) | |
column4.set_min_width(20) | |
column5.pack_start(cell5, expand=False) | |
column5.add_attribute(cell5, 'text', 4) | |
column5.set_resizable(True) | |
column5.set_reorderable(True) | |
column5.set_min_width(20) | |
self.treeview.set_search_column(0) | |
column1.set_sort_column_id(0) | |
column3.set_sort_column_id(2) | |
# Drag and Drop for tree view: | |
self.treeview.drag_dest_set(Gtk.DestDefaults.MOTION | | |
Gtk.DestDefaults.DROP, | |
[], | |
Gdk.DragAction.COPY) | |
self.treeview.drag_dest_set_target_list(tglist) | |
self.treeview.connect('drag_data_received', | |
self.drag_data_received) | |
# End Drag and Drop for tree_view | |
scrolled_window2 = Gtk.ScrolledWindow() | |
scrolled_window2.add(self.treeview) | |
scrolled_window2.set_size_request(400, -1) | |
scrolled_window2.set_policy(Gtk.PolicyType.AUTOMATIC, | |
Gtk.PolicyType.AUTOMATIC) | |
hpaned.pack2(scrolled_window2, resize=False, shrink=False) | |
self.top.pack_start(hpaned, True, True, 5) | |
self.enable_buttons() | |
return self.top | |
def on_help_clicked(self, widget): | |
""" | |
Display the relevant portion of Gramps manual. | |
""" | |
display_url(WIKI_URL) | |
def drag_data_received(self, widget, context, x, y, | |
sel_data, info, time, on_image=None): | |
""" | |
Receive a dropped person onto the treeview. | |
""" | |
if sel_data: | |
pickled_data = sel_data.get_data() | |
if not pickled_data: | |
return | |
data = pickle.loads(pickled_data) | |
# Perhaps allow multiple person drops | |
# Sometimes, more than one person could be in a selected area | |
people = [] | |
# Just get the first one for now:, if a list: | |
if sel_data.get_data_type() == DdTargets.HANDLE_LIST.atom_drag_type: | |
if data[0][0] == "Person": | |
handle = data[0][1] | |
person = self.dbstate.db.get_person_from_handle(handle) | |
if person: | |
people.append(person) | |
elif data[0][0] == "Event": | |
# get first, primary person of event: | |
event_handle = data[0][1] | |
event = self.dbstate.db.get_event_from_handle(event_handle) | |
for obj_class, handle in event.get_referenced_handles(): | |
if obj_class == "Person": | |
person = self.dbstate.db.get_person_from_handle(handle) | |
if person: | |
people.append(person) | |
break | |
elif sel_data.get_data_type() == DdTargets.PERSON_LINK.atom_drag_type: | |
(drag_type, idval, handle, val) = data | |
person = self.dbstate.db.get_person_from_handle(handle) | |
if person: | |
people.append(person) | |
else: # other formats work like this: | |
handle = None | |
try: | |
(drag_type, idval, handle, val) = data | |
except: | |
pass | |
if handle: | |
person = self.dbstate.db.get_person_from_handle(handle) | |
if person: | |
people.append(person) | |
else: | |
LOG.warn("Can't handle this type of drop: '%s'" | |
% sel_data.get_data_type()) | |
return | |
for person in people: | |
if on_image: # drop on image | |
region = self.selection_widget.find_region(x, y) | |
current = self.selection_widget.get_current() | |
if region and (current is None or current == region): | |
self.ask_and_set_person(region, person) | |
self.selection_widget.clear_selection() | |
self.refresh() | |
self.enable_buttons() | |
else: # drop on list | |
drop_info = self.treeview.get_dest_row_at_pos(x, y) | |
if drop_info: | |
#path, position = drop_info | |
#self.treeview.set_cursor(path) | |
self.set_current_person(person) | |
self.selection_widget.clear_selection() | |
self.refresh() | |
self.enable_buttons() | |
def build_context_menu(self): | |
self.context_menu = Gtk.Menu() | |
self.context_menu.set_reserve_toggle_size(False) | |
self.context_button_active = Gtk.MenuItem.new_with_mnemonic( | |
_("Set as active person")) | |
self.context_button_active.connect("activate", self.set_active_person) | |
self.context_button_select = Gtk.MenuItem.new_with_mnemonic(_("_Select")) | |
self.context_button_select.connect("activate", self.sel_person_clicked) | |
self.context_button_add = Gtk.MenuItem.new_with_mnemonic(_("_Add")) | |
self.context_button_add.connect("activate", self.add_person_clicked) | |
self.context_button_clear = Gtk.MenuItem.new_with_mnemonic(_("_Clear")) | |
self.context_button_clear.connect("activate", self.clear_ref_clicked) | |
self.context_button_remove = Gtk.MenuItem.new_with_mnemonic(_("_Remove")) | |
self.context_button_remove.connect("activate", self.del_region_clicked) | |
self.context_menu.append(self.context_button_active) | |
self.context_menu.append(self.context_button_select) | |
self.context_menu.append(self.context_button_add) | |
self.context_menu.append(self.context_button_clear) | |
self.context_menu.append(self.context_button_remove) | |
self.additional_items = [] | |
def refresh(self): | |
self.selection_widget.refresh() | |
self.refresh_list() | |
self.refresh_selection() | |
# ====================================================== | |
# gramplet event handlers | |
# ====================================================== | |
def db_changed(self): | |
self.connect(self.dbstate.db, 'media-update', self.update) | |
self.connect_signal('Media', self.update) | |
def main(self): | |
media = self.get_current_object() | |
self.top.hide() | |
if media and media.mime.startswith("image"): | |
self.load_image(media) | |
else: | |
self.selection_widget.show_missing() | |
self.refresh_list() | |
self.enable_buttons() | |
self.top.show() | |
# ====================================================== | |
# loading the image | |
# ====================================================== | |
def load_image(self, media): | |
self.regions = [] | |
self.xmp_regions = [] | |
image_path = media_path_full(self.dbstate.db, media.get_path()) | |
self.selection_widget.loaded = False | |
self.selection_widget.load_image(image_path) | |
if self.selection_widget.loaded: | |
self.retrieve_backrefs() | |
self.get_xmp_regions(image_path) | |
self.regions = self.regions + self.xmp_regions | |
self.selection_widget.set_regions(self.regions) | |
def retrieve_backrefs(self): | |
""" | |
Finds the media references pointing to the current image | |
""" | |
backrefs = self.dbstate.db.find_backlink_handles(self.get_current_handle()) | |
for (reftype, ref) in backrefs: | |
if reftype == "Person": | |
person = self.dbstate.db.get_person_from_handle(ref) | |
gallery = person.get_media_list() | |
for mediaref in gallery: | |
referenced_handles = mediaref.get_referenced_handles() | |
for referens_item in referenced_handles: | |
handle_type, handle = referens_item | |
if handle_type == "Media" and handle == self.get_current_handle(): | |
rect = mediaref.get_rectangle() | |
if rect is None: | |
rect = (0, 0, 100, 100) | |
coords = self.selection_widget.proportional_to_real_rect(rect) | |
region = Region(*coords) | |
region.person = person | |
region.xmp_person = "" | |
region.mediaref = mediaref | |
self.regions.append(region) | |
def get_xmp_regions(self, image_path): | |
""" | |
Get named regions from Xmp metadata. | |
""" | |
try: | |
metadata = GExiv2.Metadata(image_path) | |
except: | |
return | |
region_tag = 'Xmp.mwg-rs.Regions/mwg-rs:RegionList[%s]/' | |
region_name = region_tag + 'mwg-rs:Name' | |
region_type = region_tag + 'mwg-rs:Type' | |
region_x = region_tag + 'mwg-rs:Area/stArea:x' | |
region_y = region_tag + 'mwg-rs:Area/stArea:y' | |
region_w = region_tag + 'mwg-rs:Area/stArea:w' | |
region_h = region_tag + 'mwg-rs:Area/stArea:h' | |
region_unit = region_tag + 'mwg-rs:Area/stArea:unit' | |
i = 1 | |
while True: | |
name = metadata.get(region_name % i) | |
if name is None: | |
break | |
try: | |
x = float(metadata.get(region_x % i)) * 100 | |
y = float(metadata.get(region_y % i)) * 100 | |
w = float(metadata.get(region_w % i)) * 100 | |
h = float(metadata.get(region_h % i)) * 100 | |
except ValueError: | |
x = y = 50 | |
w = h = 100 | |
rtype = metadata.get(region_type % i) | |
unit = metadata.get(region_unit % i) | |
# ensure region does not exceed bounds of image | |
rect_p1 = x - (w / 2) | |
if rect_p1 < 0: | |
rect_p1 = 0 | |
rect_p2 = y - (h / 2) | |
if rect_p2 < 0: | |
rect_p2 = 0 | |
rect_p3 = x + (w / 2) | |
if rect_p3 > 100: | |
rect_p3 = 100 | |
rect_p4 = y + (h / 2) | |
if rect_p4 > 100: | |
rect_p4 = 100 | |
rect = (rect_p1, rect_p2, rect_p3, rect_p4) | |
coords = self.selection_widget.proportional_to_real_rect(rect) | |
xmp_region = Region(*coords) | |
xmp_region.xmp_person = name | |
# simple check to prevent infinite regions. If regions are already | |
# defined ignore the XMP regions. Probably there is a way to compare | |
# and merge the set of regions. | |
if not len(self.regions): self.xmp_regions.append(xmp_region) | |
i += 1 | |
# ====================================================== | |
# managing regions | |
# ====================================================== | |
def check_and_translate_to_proportional(self, mediaref, rect): | |
if mediaref: | |
return mediaref.get_rectangle() | |
else: | |
return self.selection_widget.real_to_proportional_rect(rect) | |
def intersects_any(self, region): | |
for r in self.regions: | |
if r.intersects(region): | |
return True | |
return False | |
def enclosing_region(self, region): | |
for r in self.regions: | |
if r.contains_rect(region): | |
return r | |
return None | |
def regions_referencing_person(self, person): | |
result = [] | |
for r in self.regions: | |
if r.person == person: | |
result.append(r) | |
return result | |
def all_referenced_persons(self): | |
result = [] | |
for r in self.regions: | |
if r.person is not None: | |
result.append(r.person) | |
return result | |
# ====================================================== | |
# utility functions for retrieving properties | |
# ====================================================== | |
def get_current_handle(self): | |
return self.get_active('Media') | |
def get_current_object(self): | |
try: | |
return self.dbstate.db.get_media_from_handle(self.get_current_handle()) | |
except: | |
return None | |
# ====================================================== | |
# helpers for updating database objects | |
# ====================================================== | |
def add_reference(self, person, rect): | |
""" | |
Add a reference to the media object to the specified person. | |
""" | |
mediaref = MediaRef() | |
mediaref.ref = self.get_current_handle() | |
mediaref.set_rectangle(rect) | |
person.add_media_reference(mediaref) | |
self.commit_person(person) | |
return mediaref | |
def remove_reference(self, person, mediaref): | |
""" | |
Removes the reference to the media object from the person. | |
""" | |
if mediaref in person.get_media_list(): | |
person.get_media_list().remove(mediaref) | |
self.commit_person(person) | |
def commit_person(self, person): | |
""" | |
Save the modifications made to a Person object to the database. | |
""" | |
with DbTxn('', self.dbstate.db) as trans: | |
self.dbstate.db.commit_person(person, trans) | |
msg = _("Edit Person (%s)") % name_displayer.display(person) | |
trans.set_description(msg) | |
# ====================================================== | |
# managing toolbar buttons | |
# ====================================================== | |
def enable_buttons(self): | |
selected = self.selection_widget.get_current() | |
self.button_index.set_sensitive(selected is not None) | |
self.button_add.set_sensitive(selected is not None) | |
self.button_del.set_sensitive( | |
selected is not None and | |
selected.person is not None) | |
self.button_clear.set_sensitive(selected is not None) | |
self.button_edit.set_sensitive( | |
selected is not None and | |
selected.person is not None) | |
self.button_zoom_in.set_sensitive( | |
self.selection_widget.is_image_loaded() and | |
self.selection_widget.can_zoom_in()) | |
self.button_zoom_out.set_sensitive( | |
self.selection_widget.is_image_loaded() and | |
self.selection_widget.can_zoom_out()) | |
self.button_detect.set_sensitive( | |
self.selection_widget.is_image_loaded() and | |
facedetection.computer_vision_available) | |
# ====================================================== | |
# managing context menu buttons | |
# ====================================================== | |
def prepare_context_menu(self): | |
selected = self.selection_widget.get_current() | |
has_person = selected is not None and selected.person is not None | |
self.context_button_active.set_sensitive(has_person) | |
self.context_button_add.set_sensitive(selected is not None) | |
self.context_button_select.set_sensitive(selected is not None) | |
self.context_button_clear.set_sensitive(has_person) | |
self.context_button_remove.set_sensitive(selected is not None) | |
# clear temporary items | |
for item in self.additional_items: | |
self.context_menu.remove(item) | |
self.additional_items = [] | |
# populate the context menu | |
persons = self.all_referenced_persons() | |
if selected is not None and selected.person is not None: | |
persons.remove(selected.person) | |
if persons: | |
self.additional_items.append(Gtk.SeparatorMenuItem()) | |
sorted_persons = sorted(list(persons), key=name_displayer.display) | |
for person in sorted_persons: | |
item = Gtk.MenuItem( | |
_("Replace to {0}").format(name_displayer.display(person))) | |
item.connect("activate", self.replace_reference, person) | |
self.additional_items.append(item) | |
for item in self.additional_items: | |
self.context_menu.append(item) | |
def show_context_menu(self): | |
""" | |
Show popup menu using different functions according to Gtk version. | |
'Gtk.Menu.popup' is deprecated since version 3.22, see: | |
https://lazka.github.io/pgi-docs/index.html#Gtk-3.0/classes/Menu.html#Gtk.Menu.popup | |
""" | |
self.prepare_context_menu() | |
self.context_menu.show_all() | |
if (Gtk.MAJOR_VERSION >= 3) and (Gtk.MINOR_VERSION >= 22): | |
self.context_menu.popup_at_pointer(None) | |
else: | |
self.context_menu.popup(None, None, None, None, 0, 0) | |
# ====================================================== | |
# selection event handlers | |
# ====================================================== | |
def region_modified(self, sender): | |
region = self.selection_widget.get_current() | |
person = region.person | |
mediaref = region.mediaref | |
if person and mediaref: | |
selection = self.selection_widget.get_selection() | |
rect = self.selection_widget.real_to_proportional_rect(selection) | |
mediaref.set_rectangle(rect) | |
self.commit_person(person) | |
self.enable_buttons() | |
self.refresh_list() | |
self.refresh_selection() | |
def region_created(self, sender): | |
self.enable_buttons() | |
self.refresh_list() | |
self.refresh_selection() | |
self.show_context_menu() | |
def right_button_clicked(self, sender): | |
self.show_context_menu() | |
def region_selected(self, sender): | |
self.enable_buttons() | |
self.refresh_selection() | |
def selection_cleared(self, sender): | |
self.enable_buttons() | |
self.refresh_selection() | |
def zoomed(self, sender): | |
self.enable_buttons() | |
# ====================================================== | |
# toolbar button event handles | |
# ====================================================== | |
def add_person_clicked(self, event): | |
if self.selection_widget.get_current(): | |
person = Person() | |
EditPerson(self.dbstate, self.uistate, self.track, person, | |
self.new_person_added) | |
def sel_person_clicked(self, event): | |
if self.selection_widget.get_current(): | |
SelectPerson = SelectorFactory('Person') | |
sel = SelectPerson(self.dbstate, self.uistate, self.track, | |
_("Select Person"), show_search_bar=True) | |
person = sel.run() | |
if person: | |
self.set_current_person(person) | |
self.selection_widget.clear_selection() | |
self.refresh() | |
self.enable_buttons() | |
def del_region_clicked(self, event): | |
if self.selection_widget.get_current(): | |
self.delete_region(self.selection_widget.get_current()) | |
self.selection_widget.clear_selection() | |
self.refresh() | |
self.enable_buttons() | |
def clear_ref_clicked(self, event): | |
if self.clear_ref(self.selection_widget.get_current()): | |
self.refresh() | |
def edit_person_clicked(self, event): | |
person = self.selection_widget.get_current().person | |
if person: | |
EditPerson(self.dbstate, self.uistate, self.track, person) | |
self.refresh() | |
def zoom_in_clicked(self, event): | |
self.selection_widget.zoom_in() | |
def zoom_out_clicked(self, event): | |
self.selection_widget.zoom_out() | |
def detect_faces_clicked(self, event): | |
self.uistate.push_message(self.dbstate, _("Detecting faces...")) | |
media = self.get_current_object() | |
image_path = media_path_full(self.dbstate.db, media.get_path()) | |
faces, img_size = facedetection.detect_faces(image_path, MIN_FACE_SIZE, | |
SENSITIVITY) | |
# verify and enlarge found faces regions | |
for (x, y, width, height) in faces: | |
# calculate enlarged region | |
new_x1 = x - width/5 | |
new_y1 = y - height/3 | |
new_x2 = x + width*6/5 | |
new_y2 = y + height*7/5 | |
# prevent overflow image size | |
new_y1 = 0 if new_y1 < 0 else new_y1 | |
new_y2 = img_size[0] if img_size[0] < new_y2 else new_y2 | |
new_x1 = 0 if new_x1 < 0 else new_x1 | |
new_x2 = img_size[1] if img_size[1] < new_x2 else new_x2 | |
region = Region(new_x1, new_y1, new_x2, new_y2) | |
if (DETECT_INSIDE_EXISTING_BOXES | |
or self.enclosing_region(region) is None): | |
self.regions.append(region) | |
self.refresh() | |
self.uistate.push_message(self.dbstate, _("Detection finished")) | |
def settings_clicked(self, event): | |
try: | |
SettingsDialog(self.gui.dbstate, self.gui.uistate, | |
_("Settings"), PhotoTaggingOptions()) | |
except WindowActiveError: | |
pass | |
def set_active_person(self, event): | |
""" | |
Set selected person as active. | |
""" | |
person = self.selection_widget.get_current().person | |
if person: | |
person_handle = person.get_handle() | |
self.set_active('Person', person_handle) | |
# ====================================================== | |
# helpers for toolbar event handlers | |
# ====================================================== | |
def delete_region(self, region): | |
self.regions.remove(region) | |
if region.person is not None: | |
self.remove_reference(region.person, region.mediaref) | |
def delete_regions(self, regions): | |
for r in regions: | |
self.delete_region(r) | |
def new_person_added(self, person): | |
self.set_current_person(person) | |
self.selection_widget.clear_selection() | |
self.refresh() | |
self.enable_buttons() | |
def set_current_person(self, person): | |
self.ask_and_set_person(self.selection_widget.get_current(), person) | |
def ask_and_set_person(self, region, person): | |
if region and person: | |
other_references = self.regions_referencing_person(person) | |
ref_count = len(other_references) | |
if ref_count > 0: | |
person = other_references[0].person | |
if REPLACE_WITHOUT_ASKING: | |
self.delete_regions(other_references) | |
else: | |
if ref_count == 1: | |
text = _("Another region of this image " | |
"is associated with {name}. Remove it?") | |
else: | |
text = _("{count} other regions of this image " | |
"are associated with {name}. Remove them?") | |
message = text.format(name=name_displayer.display(person), | |
count=ref_count) | |
dialog = Gtk.MessageDialog(parent=None, | |
type=Gtk.MessageType.QUESTION, | |
buttons=Gtk.ButtonsType.YES_NO, | |
message_format=message) | |
response = dialog.run() | |
dialog.destroy() | |
if response == Gtk.ResponseType.YES: | |
self.delete_regions(other_references) | |
self.set_person(region, person) | |
def set_person(self, region, person): | |
rect = self.check_and_translate_to_proportional(region.mediaref, | |
region.coords()) | |
self.clear_ref(region) | |
mediaref = self.add_reference(person, rect) | |
region.person = person | |
region.mediaref = mediaref | |
def clear_ref(self, region): | |
if region: | |
if region.person: | |
self.remove_reference(region.person, region.mediaref) | |
region.person = None | |
region.mediaref = None | |
return True | |
return False | |
# ====================================================== | |
# context menu event handles | |
# ====================================================== | |
def replace_reference(self, event, person): | |
other_references = self.regions_referencing_person(person) | |
self.delete_regions(other_references) | |
self.set_person(self.selection_widget.get_current(), person) | |
self.selection_widget.clear_selection() | |
self.refresh() | |
self.enable_buttons() | |
# ====================================================== | |
# list event handles | |
# ====================================================== | |
def cursor_changed(self, treeview): | |
selected = self.get_selected_region() | |
self.selection_widget.select(selected) | |
self.enable_buttons() | |
def row_activated(self, treeview, path, view_column): | |
self.edit_person_clicked(None) | |
def row_mouse_click(self, treeview, event): | |
""" | |
Handle right mouse click on treeview. | |
Show popup menu for row the same as for region. | |
""" | |
button = event.get_button()[1] | |
# right mouse button | |
if button == 3: | |
# change cursor position to apply row selection | |
pthinfo = self.treeview.get_path_at_pos(event.x, event.y) | |
if pthinfo is not None: | |
path, col, cellx, celly = pthinfo | |
self.treeview.grab_focus() | |
self.treeview.set_cursor(path, col, 0) | |
self.show_context_menu() | |
# stop signal emission | |
return True | |
# ====================================================== | |
# helpers for list event handlers | |
# ====================================================== | |
def get_selected_region(self): | |
selection = self.treeview.get_selection() | |
if selection: | |
(model, pathlist) = selection.get_selected_rows() | |
for path in pathlist: | |
tree_iter = model.get_iter(path) | |
i = model.get_value(tree_iter, 0) | |
try: | |
return self.regions[i - 1] | |
except: | |
return None | |
return None | |
# ====================================================== | |
# refreshing the list | |
# ====================================================== | |
def refresh_list(self): | |
self.treestore.clear() | |
for (i, region) in enumerate(self.regions, start=1): | |
if region.person: | |
name = name_displayer.display(region.person) | |
age = get_person_age(region.person, self.dbstate.db, | |
self.age_precision) | |
xmp_name = region.xmp_person | |
elif hasattr(region, 'xmp_person'): | |
name = "" | |
age = "" | |
xmp_name = region.xmp_person | |
else: | |
name = "" | |
age = "" | |
xmp_name = "" | |
thumbnail = self.selection_widget.get_thumbnail( | |
region, THUMBNAIL_IMAGE_SIZE) | |
self.treestore.append(None, (i, thumbnail, name, age, xmp_name)) | |
def refresh_selection(self): | |
current = self.selection_widget.get_current() | |
selection = self.treeview.get_selection() | |
if current and current in self.regions: | |
selection.select_path(self.regions.index(current),) | |
else: | |
selection.unselect_all() | |
def get_person_age(person, dbstate_db, age_precision): | |
""" | |
Function to get person age. | |
Returns string for age column. | |
""" | |
birth = get_birth_or_fallback(dbstate_db, person) | |
death = get_death_or_fallback(dbstate_db, person) | |
is_dead = False | |
if birth: | |
birth_date = birth.get_date_object() | |
if (birth_date and birth_date.get_valid()): | |
if death: | |
death_date = death.get_date_object() | |
if (death_date and death_date.get_valid()): | |
age = (death_date - birth_date) | |
is_dead = True | |
if not is_dead: | |
# if person is alive | |
age = (Today() - birth_date) | |
# formating age string | |
age_str = age.format(precision=age_precision) | |
if is_dead: | |
gender = person.get_gender() | |
# we get localized "Dead at %s age" | |
if gender == person.MALE: | |
age_str = _("Male person|Dead at %s") % (age_str) | |
elif gender == person.FEMALE: | |
age_str = _("Female person|Dead at %s") % (age_str) | |
else: | |
age_str = _("Unknown gender person|Dead at %s") % (age_str) | |
return age_str | |
return None |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment