diff options
author | Jörg Frings-Fürst <debian@jff.email> | 2023-06-14 20:35:58 +0200 |
---|---|---|
committer | Jörg Frings-Fürst <debian@jff.email> | 2023-06-14 20:35:58 +0200 |
commit | d443a3c2509889533ca812c163056bace396b586 (patch) | |
tree | e94ffc0d9c054ca4efb8fb327e18dfac88e15dc7 /src/faces | |
parent | bb9797c14470641b082ebf635e2ae3cfd5f27a3b (diff) |
New upstream version 0.32.1upstream/0.32.1
Diffstat (limited to 'src/faces')
-rw-r--r-- | src/faces/Face.vala | 30 | ||||
-rw-r--r-- | src/faces/FaceDetect.vala | 146 | ||||
-rw-r--r-- | src/faces/FaceLocation.vala | 45 | ||||
-rw-r--r-- | src/faces/FacePage.vala | 14 | ||||
-rw-r--r-- | src/faces/FaceShape.vala | 120 | ||||
-rw-r--r-- | src/faces/FacesTool.vala | 230 |
6 files changed, 423 insertions, 162 deletions
diff --git a/src/faces/Face.vala b/src/faces/Face.vala index 9304023..cdccc1b 100644 --- a/src/faces/Face.vala +++ b/src/faces/Face.vala @@ -345,9 +345,19 @@ public class Face : DataSource, ContainerSource, Proxyable, Indexable { // add them all at once to the SourceCollection global.add_many(faces); global.init_add_many_unlinked(unlinked); + +#if ENABLE_FACE_DETECTION + // Start the face detection background process + // FaceTool talks to it over DBus + start_facedetect_process(); +#endif } public static void terminate() { + try { + if (FaceDetect.face_detect_proxy != null) + FaceDetect.face_detect_proxy.terminate(); + } catch(Error e) {} } public static int compare_names(void *a, void *b) { @@ -365,6 +375,14 @@ public class Face : DataSource, ContainerSource, Proxyable, Indexable { public static bool equal_name_strings(void *a, void *b) { return String.collated_equals(a, b); } + +#if ENABLE_FACE_DETECTION + private static void start_facedetect_process() { + message("Launching facedetect process: %s", AppDirs.get_facedetect_bin().get_path()); + // Start the watcher, process started via DBus service + FaceDetect.init(AppDirs.get_openface_dnn_system_dir().get_path() + ":" + AppDirs.get_openface_dnn_dir().get_path()); + } +#endif // Returns a Face for the name, creating a new empty one if it does not already exist. // name should have already been prepared by prep_face_name. @@ -387,7 +405,7 @@ public class Face : DataSource, ContainerSource, Proxyable, Indexable { return face; } - + // Utility function to cleanup a face name that comes from user input and prepare it for use // in the system and storage in the database. Returns null if the name is unacceptable. public static string? prep_face_name(string name) { @@ -574,6 +592,16 @@ public class Face : DataSource, ContainerSource, Proxyable, Indexable { return true; } + + public bool set_reference(FaceLocation face_loc) { + try { + FaceTable.get_instance().set_reference(row.face_id, face_loc.get_photo_id()); + } catch (DatabaseError err) { + AppWindow.database_error(err); + return false; + } + return true; + } public bool contains(MediaSource source) { return media_views.has_view_for_source(source); diff --git a/src/faces/FaceDetect.vala b/src/faces/FaceDetect.vala new file mode 100644 index 0000000..83caa4d --- /dev/null +++ b/src/faces/FaceDetect.vala @@ -0,0 +1,146 @@ +/** + * Face detection and recognition functions + * Copyright 2018 Narendra A (narendra_m_a(at)yahoo(dot)com) + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, copy, + * modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// DBus face_detect_proxy definition +public struct FaceRect { + public double x; + public double y; + public double width; + public double height; + public double[] vec; +} + +[DBus (name = "org.gnome.Shotwell.Faces1")] +public interface FaceDetectInterface : DBusProxy { + public abstract FaceRect[] detect_faces(string inputName, string cascadeName, double scale, bool infer) + throws IOError, DBusError; + public abstract bool load_net(string netFile) + throws IOError, DBusError; + public abstract void terminate() throws IOError, DBusError; +} + +// Class to communicate with facedetect process over DBus +public class FaceDetect { + public const string DBUS_NAME = "org.gnome.Shotwell.Faces1"; + public const string DBUS_PATH = "/org/gnome/shotwell/faces"; + public static bool connected = false; + public static string net_file; + public const string ERROR_MESSAGE = "Unable to connect to facedetect service"; + + public static FaceDetectInterface face_detect_proxy; + +#if FACEDETECT_BUS_PRIVATE + private static GLib.DBusServer dbus_server; + private static Subprocess process; +#endif + + public static void create_face_detect_proxy(DBusConnection connection, string bus_name, string owner) { + if (bus_name == DBUS_NAME) { + message("Dbus name %s available", bus_name); + + try { + // Service file should automatically run the facedetect binary + face_detect_proxy = Bus.get_proxy_sync (BusType.SESSION, DBUS_NAME, DBUS_PATH); + face_detect_proxy.load_net(net_file); + connected = true; + } catch(IOError e) { + AppWindow.error_message(ERROR_MESSAGE); + } catch(DBusError e) { + AppWindow.error_message(ERROR_MESSAGE); + } + } + } + + public static void interface_gone(DBusConnection connection, string bus_name) { + message("Dbus name %s gone", bus_name); + connected = false; + face_detect_proxy = null; + } + +#if FACEDETECT_BUS_PRIVATE + private static bool on_new_connection(DBusServer server, DBusConnection connection) { + try { + face_detect_proxy = connection.get_proxy_sync(null, DBUS_PATH, + DBusProxyFlags.DO_NOT_LOAD_PROPERTIES + | DBusProxyFlags.DO_NOT_CONNECT_SIGNALS, + null); + Idle.add(() => { + try { + face_detect_proxy.load_net(net_file); + connected = true; + } catch (Error error) { + critical("Failed to call load_net: %s", error.message); + AppWindow.error_message(ERROR_MESSAGE); + } + return false; + }); + + return true; + } catch (Error error) { + critical("Failed to create face_detect_proxy for face detect: %s", error.message); + AppWindow.error_message(ERROR_MESSAGE); + + return false; + } + } +#endif + + public static void init(string net_file) { + FaceDetect.net_file = net_file; +#if FACEDETECT_BUS_PRIVATE + var address = "unix:tmpdir=%s".printf(Environment.get_tmp_dir()); + var observer = new DBusAuthObserver(); + observer.authorize_authenticated_peer.connect((stream, credentials) => { + debug("Observer trying to authorize for %s", credentials.to_string()); + if (credentials == null) + return false; + + try { + if (!credentials.is_same_user(new Credentials())) + return false; + return true; + } catch (Error error) { + return false; + } + }); + + try { + dbus_server = new GLib.DBusServer.sync(address, DBusServerFlags.NONE, DBus.generate_guid(), observer, null); + dbus_server.new_connection.connect(on_new_connection); + dbus_server.start(); + process = new Subprocess(SubprocessFlags.NONE, AppDirs.get_facedetect_bin().get_path(), + "--address=" + dbus_server.get_client_address()); + + } catch (Error error) { + warning("Failed to create private DBus server: %s", error.message); + AppWindow.error_message(ERROR_MESSAGE); + } +#else + Bus.watch_name(BusType.SESSION, DBUS_NAME, BusNameWatcherFlags.NONE, + create_face_detect_proxy, interface_gone); +#endif + } + +} diff --git a/src/faces/FaceLocation.vala b/src/faces/FaceLocation.vala index e143b2e..0f4e383 100644 --- a/src/faces/FaceLocation.vala +++ b/src/faces/FaceLocation.vala @@ -4,6 +4,11 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ +// Encapsulate geometry and pixels of a Face +public struct FaceLocationData { + public string geometry; + public string vec; +} public class FaceLocation : Object { private static Gee.Map<FaceID?, Gee.Map<PhotoID?, FaceLocation>> face_photos_map; @@ -12,17 +17,17 @@ public class FaceLocation : Object { private FaceLocationID face_location_id; private FaceID face_id; private PhotoID photo_id; - private string geometry; - + private FaceLocationData face_data; + private FaceLocation(FaceLocationID face_location_id, FaceID face_id, PhotoID photo_id, - string geometry) { + FaceLocationData face_data) { this.face_location_id = face_location_id; this.face_id = face_id; this.photo_id = photo_id; - this.geometry = geometry; + this.face_data = face_data; } - public static FaceLocation create(FaceID face_id, PhotoID photo_id, string geometry) { + public static FaceLocation create(FaceID face_id, PhotoID photo_id, FaceLocationData face_data) { FaceLocation face_location = null; // Test if that FaceLocation already exists (that face in that photo) ... @@ -33,12 +38,11 @@ public class FaceLocation : Object { face_location = faces_map.get(face_id); - if (face_location.get_serialized_geometry() != geometry) { - face_location.set_serialized_geometry(geometry); + if (face_location.get_serialized_geometry() != face_data.geometry) { + face_location.set_face_data(face_data); try { - FaceLocationTable.get_instance().update_face_location_serialized_geometry( - face_location); + FaceLocationTable.get_instance().update_face_location_face_data(face_location); } catch (DatabaseError err) { AppWindow.database_error(err); } @@ -51,7 +55,7 @@ public class FaceLocation : Object { try { face_location = FaceLocation.add_from_row( - FaceLocationTable.get_instance().add(face_id, photo_id, geometry)); + FaceLocationTable.get_instance().add(face_id, photo_id, face_data.geometry, face_data.vec)); } catch (DatabaseError err) { AppWindow.database_error(err); } @@ -84,7 +88,8 @@ public class FaceLocation : Object { public static FaceLocation add_from_row(FaceLocationRow row) { FaceLocation face_location = - new FaceLocation(row.face_location_id, row.face_id, row.photo_id, row.geometry); + new FaceLocation(row.face_location_id, row.face_id, row.photo_id, + { row.geometry, row.vec }); Gee.Map<PhotoID?, FaceLocation> photos_map = face_photos_map.get(row.face_id); if (photos_map == null) {photos_map = new Gee.HashMap<PhotoID?, FaceLocation> @@ -196,10 +201,22 @@ public class FaceLocation : Object { } public string get_serialized_geometry() { - return geometry; + return face_data.geometry; + } + + public string get_serialized_vec() { + return face_data.vec; + } + + public FaceLocationData get_face_data() { + return face_data; + } + + public PhotoID get_photo_id() { + return photo_id; } - private void set_serialized_geometry(string geometry) { - this.geometry = geometry; + private void set_face_data(FaceLocationData face_data) { + this.face_data = face_data; } } diff --git a/src/faces/FacePage.vala b/src/faces/FacePage.vala index f2512d5..1766b91 100644 --- a/src/faces/FacePage.vala +++ b/src/faces/FacePage.vala @@ -44,6 +44,7 @@ public class FacePage : CollectionPage { { "DeleteFace", on_delete_face }, { "RenameFace", on_rename_face }, { "RemoveFaceFromPhotos", on_remove_face_from_photos }, + { "SetFaceRefFromPhoto", on_set_face_ref }, { "DeleteFaceSidebar", on_delete_face }, { "RenameFaceSidebar", on_rename_face } }; @@ -74,6 +75,7 @@ public class FacePage : CollectionPage { menuFaces.add_menu_item(Resources.remove_face_from_photos_menu(this.face.get_name(), get_view().get_count()), "RemoveFaceFromPhotos", "<Primary>r"); menuFaces.add_menu_item(Resources.rename_face_menu(this.face.get_name()), "RenameFace", "<Primary>e"); + menuFaces.add_menu_item(Resources.set_face_from_photo_menu(this.face.get_name()), "SetFaceRefFromPhoto", null); menuFaces.add_menu_item(Resources.delete_face_menu(this.face.get_name()), "DeleteFace", "<Primary>t"); return menuFaces; @@ -102,6 +104,11 @@ public class FacePage : CollectionPage { null, selected_count > 0); + set_action_details("SetFaceRefFromPhoto", + Resources.set_face_from_photo_menu(face.get_name()), + null, + selected_count == 1); + base.update_actions(selected_count, count); } @@ -120,4 +127,11 @@ public class FacePage : CollectionPage { (Gee.Collection<MediaSource>) get_view().get_selected_sources())); } } + + private void on_set_face_ref() { + if (get_view().get_selected_count() == 1) { + get_command_manager().execute(new SetFaceRefCommand(face, + (MediaSource) get_view().get_selected_at(0).get_source())); + } + } } diff --git a/src/faces/FaceShape.vala b/src/faces/FaceShape.vala index 1ff01fd..f90f254 100644 --- a/src/faces/FaceShape.vala +++ b/src/faces/FaceShape.vala @@ -18,14 +18,16 @@ public abstract class FaceShape : Object { protected Gdk.CursorType current_cursor_type = Gdk.CursorType.BOTTOM_RIGHT_CORNER; protected EditingTools.PhotoCanvas canvas; protected string serialized = null; + protected double[] face_vec; private bool editable = true; private bool visible = true; private bool known = true; + private double guess = 0.0; private weak FacesTool.FaceWidget face_widget = null; - protected FaceShape(EditingTools.PhotoCanvas canvas) { + protected FaceShape(EditingTools.PhotoCanvas canvas, double[] vec) { this.canvas = canvas; this.canvas.new_surface.connect(prepare_ctx); @@ -37,19 +39,21 @@ public abstract class FaceShape : Object { face_window.show_all(); face_window.hide(); - this.canvas.get_drawing_window().set_cursor(new Gdk.Cursor(current_cursor_type)); + this.face_vec = vec; + this.canvas.set_cursor(current_cursor_type); } ~FaceShape() { - if (visible) + if (visible) { erase(); + } face_window.destroy(); canvas.new_surface.disconnect(prepare_ctx); // make sure the cursor isn't set to a modify indicator - canvas.get_drawing_window().set_cursor(new Gdk.Cursor(Gdk.CursorType.LEFT_PTR)); + canvas.set_cursor(Gdk.CursorType.LEFT_PTR); } public static FaceShape from_serialized(EditingTools.PhotoCanvas canvas, string serialized) @@ -88,7 +92,15 @@ public abstract class FaceShape : Object { public bool get_known() { return known; } + + public void set_guess(double guess) { + this.guess = guess; + } + public double get_guess() { + return guess; + } + public void set_widget(FacesTool.FaceWidget face_widget) { this.face_widget = face_widget; } @@ -107,7 +119,7 @@ public abstract class FaceShape : Object { face_window.hide(); // make sure the cursor isn't set to a modify indicator - canvas.get_drawing_window().set_cursor(new Gdk.Cursor(Gdk.CursorType.LEFT_PTR)); + canvas.set_cursor(Gdk.CursorType.LEFT_PTR); } public void show() { @@ -160,7 +172,7 @@ public abstract class FaceShape : Object { return true; } - public abstract string serialize(); + public abstract string serialize(bool geometry_only = false); public abstract void update_face_window_position(); public abstract void prepare_ctx(Cairo.Context ctx, Dimensions dim); public abstract void on_resized_pixbuf(Dimensions old_dim, Gdk.Pixbuf scaled); @@ -170,6 +182,7 @@ public abstract class FaceShape : Object { public abstract bool cursor_is_over(int x, int y); public abstract bool equals(FaceShape face_shape); public abstract double get_distance(int x, int y); + public abstract double[] get_face_vec(); protected abstract void paint(); protected abstract void erase(); @@ -186,13 +199,17 @@ public class FaceRectangle : FaceShape { private BoxLocation in_manipulation = BoxLocation.OUTSIDE; private Cairo.Context wide_black_ctx = null; private Cairo.Context wide_white_ctx = null; - private Cairo.Context thin_white_ctx = null; private int last_grab_x = -1; private int last_grab_y = -1; public FaceRectangle(EditingTools.PhotoCanvas canvas, int x, int y, - int half_width = NULL_SIZE, int half_height = NULL_SIZE) { - base(canvas); + int half_width = NULL_SIZE, int half_height = NULL_SIZE, double[] vec = {}) { + double[] int_vec; + if (vec.length == 0) + int_vec = create_empty_vec(); + else + int_vec = vec; + base(canvas, int_vec); Gdk.Rectangle scaled_pixbuf_pos = canvas.get_scaled_pixbuf_position(); x -= scaled_pixbuf_pos.x; @@ -219,6 +236,14 @@ public class FaceRectangle : FaceShape { if (!is_editable()) erase_label(); } + + public static double[] create_empty_vec() { + double[] empty_vec = new double[128]; + for (int i = 0; i < 128; i++) { + empty_vec[i] = 0; + } + return empty_vec; + } public static new FaceRectangle from_serialized(EditingTools.PhotoCanvas canvas, string[] args) throws FaceShapeError { @@ -226,7 +251,9 @@ public class FaceRectangle : FaceShape { Photo photo = canvas.get_photo(); Dimensions raw_dim = photo.get_raw_dimensions(); - + + // 1, 2 is the center of the rectangle, 3, 4 is the half width / height of the rectangle, + // normalized int x = (int) (raw_dim.width * double.parse(args[1])); int y = (int) (raw_dim.height * double.parse(args[2])); int half_width = (int) (raw_dim.width * double.parse(args[3])); @@ -265,9 +292,21 @@ public class FaceRectangle : FaceShape { if (half_width < FACE_MIN_SIZE || half_height < FACE_MIN_SIZE) throw new FaceShapeError.CANT_CREATE("FaceShape is out of cropped photo area"); - + + string[] vec_str; + if (args.length == 6) + vec_str = args[5].split(","); + else + vec_str = {}; + double[] vec = new double[128]; + for (int i = 0; i < 128; i++) { + if (vec_str.length > i) + vec[i] = double.parse(vec_str[i]); + else + vec[i] = 0; + } return new FaceRectangle(canvas, box.left + half_width, box.top + half_height, - half_width, half_height); + half_width, half_height, vec); } public override void update_face_window_position() { @@ -283,32 +322,35 @@ public class FaceRectangle : FaceShape { face_window.get_allocation(out face_window_alloc); - x += scaled_pixbuf_pos.x + box.left + ((box.get_width() - face_window_alloc.width) >> 1); - y += scaled_pixbuf_pos.y + box.bottom + FACE_WINDOW_MARGIN; + var scale = Application.get_scale(); + var left = (int)Math.lround((scaled_pixbuf_pos.x + box.left) / scale); + var width = (int)Math.lround(box.get_width() / scale); + var top = (int)Math.lround((scaled_pixbuf_pos.y + box.bottom) / scale); + x += (left + ((width - face_window_alloc.width) >> 1)); + y += top + FACE_WINDOW_MARGIN; face_window.move(x, y); } protected override void paint() { + // The box is in image coordinates. Need to scale down to device coordinates canvas.draw_box(wide_black_ctx, box); canvas.draw_box(wide_white_ctx, box.get_reduced(1)); canvas.draw_box(wide_white_ctx, box.get_reduced(2)); - canvas.invalidate_area(box); + //canvas.invalidate_area(box); if (!is_editable()) paint_label(); } protected override void erase() { - canvas.erase_box(box); - canvas.erase_box(box.get_reduced(1)); - canvas.erase_box(box.get_reduced(2)); - canvas.invalidate_area(box); if (!is_editable()) erase_label(); + +// canvas.repaint(); } private void paint_label() { @@ -317,6 +359,9 @@ public class FaceRectangle : FaceShape { ctx.save(); + ctx.select_font_face("Sans", Cairo.FontSlant.NORMAL, Cairo.FontWeight.NORMAL); + ctx.set_font_size(10.0 * Application.get_scale()); + Cairo.TextExtents text_extents = Cairo.TextExtents(); ctx.text_extents(get_name(), out text_extents); @@ -368,7 +413,7 @@ public class FaceRectangle : FaceShape { ctx.restore(); } - public override string serialize() { + public override string serialize(bool geometry_only = false) { if (serialized != null) return serialized; @@ -378,10 +423,15 @@ public class FaceRectangle : FaceShape { double half_height; get_geometry(out x, out y, out half_width, out half_height); - - serialized = "%s;%s;%s;%s;%s".printf(SHAPE_TYPE, x.to_string(), + serialized = "%s;%s;%s;%s;%s;".printf(SHAPE_TYPE, x.to_string(), y.to_string(), half_width.to_string(), half_height.to_string()); - + if (!geometry_only) { + string face_vec_str = ""; + foreach (var d in face_vec[0:-2]) + face_vec_str += d.to_string() + ","; + face_vec_str += face_vec[-1].to_string(); + serialized += face_vec_str; + } return serialized; } @@ -425,23 +475,23 @@ public class FaceRectangle : FaceShape { half_width = (width_right_end - width_left_end) / 2; half_height = (height_bottom_end - height_top_end) / 2; } + + public override double[] get_face_vec() { + return face_vec; + } public override bool equals(FaceShape face_shape) { - return serialize() == face_shape.serialize(); + return serialize(true) == face_shape.serialize(true); } public override void prepare_ctx(Cairo.Context ctx, Dimensions dim) { wide_black_ctx = new Cairo.Context(ctx.get_target()); set_source_color_from_string(wide_black_ctx, "#000"); - wide_black_ctx.set_line_width(1); + wide_black_ctx.set_line_width(1 * Application.get_scale()); wide_white_ctx = new Cairo.Context(ctx.get_target()); - set_source_color_from_string(wide_black_ctx, "#FFF"); - wide_white_ctx.set_line_width(1); - - thin_white_ctx = new Cairo.Context(ctx.get_target()); - set_source_color_from_string(wide_black_ctx, "#FFF"); - thin_white_ctx.set_line_width(0.5); + set_source_color_from_string(wide_white_ctx, "#FFF"); + wide_white_ctx.set_line_width(1 * Application.get_scale()); } private bool on_canvas_manipulation(int x, int y) { @@ -620,17 +670,20 @@ public class FaceRectangle : FaceShape { Box new_box = Box(left, top, right, bottom); if (!box.equals(new_box)) { - erase(); + canvas.invalidate_area(box); if (in_manipulation != BoxLocation.INSIDE) check_resized_box(new_box); box = new_box; paint(); + canvas.invalidate_area(new_box); } if (is_editable()) update_face_window_position(); + + canvas.repaint(); serialized = null; @@ -698,8 +751,7 @@ public class FaceRectangle : FaceShape { } if (cursor_type != current_cursor_type) { - Gdk.Cursor cursor = new Gdk.Cursor(cursor_type); - canvas.get_drawing_window().set_cursor(cursor); + canvas.set_cursor(cursor_type); current_cursor_type = cursor_type; } } diff --git a/src/faces/FacesTool.vala b/src/faces/FacesTool.vala index 9803787..d399b38 100644 --- a/src/faces/FacesTool.vala +++ b/src/faces/FacesTool.vala @@ -119,7 +119,7 @@ public class FacesTool : EditingTools.EditingTool { private EditingPhase editing_phase = EditingPhase.NOT_EDITING; private Gtk.Box help_layout = null; private Gtk.Box response_layout = null; - private Gtk.HSeparator buttons_text_separator = null; + private Gtk.Separator buttons_text_separator = null; private Gtk.Label help_text = null; private Gtk.Box face_widgets_layout = null; private Gtk.Box layout = null; @@ -163,7 +163,7 @@ public class FacesTool : EditingTools.EditingTool { layout = new Gtk.Box(Gtk.Orientation.VERTICAL, CONTROL_SPACING); layout.pack_start(face_widgets_layout, false); layout.pack_start(help_layout, false); - layout.pack_start(new Gtk.HSeparator(), false); + layout.pack_start(new Gtk.Separator(Gtk.Orientation.HORIZONTAL), false); layout.pack_start(response_layout, false); add(layout); @@ -178,7 +178,7 @@ public class FacesTool : EditingTools.EditingTool { case EditingPhase.CLICK_TO_EDIT: assert(face_shape != null); - help_text.set_markup(Markup.printf_escaped(_("Click to edit face <i>%s</i>"), + help_text.set_markup(Markup.printf_escaped(_("Click to edit face “%s”"), face_shape.get_name())); break; @@ -254,7 +254,7 @@ public class FacesTool : EditingTools.EditingTool { face_widgets_layout.pack_start(event_box, false); if (buttons_text_separator == null) { - buttons_text_separator = new Gtk.HSeparator(); + buttons_text_separator = new Gtk.Separator(Gtk.Orientation.HORIZONTAL); face_widgets_layout.pack_end(buttons_text_separator, false); } @@ -315,121 +315,49 @@ public class FacesTool : EditingTools.EditingTool { private class FaceDetectionJob : BackgroundJob { private Gee.Queue<string> faces = null; private string image_path; - private string output; - public SpawnError? spawnError; + private float scale; + public string? spawnError; - public FaceDetectionJob(FacesToolWindow owner, string image_path, + public FaceDetectionJob(FacesToolWindow owner, string image_path, float scale, CompletionCallback completion_callback, Cancellable cancellable, CancellationCallback cancellation_callback) { base(owner, completion_callback, cancellable, cancellation_callback); this.image_path = image_path; + this.scale = scale; } public override void execute() { + if (!FaceDetect.connected) { + spawnError = "Face detect process not connected!\n"; + return; + } + FaceRect[] rects; try { - string[] argv = { - AppDirs.get_facedetect_bin().get_path(), - "--cascade=" + AppDirs.get_haarcascade_file().get_path(), - "--scale=1.2", - image_path - }; - Process.spawn_sync(null, argv, null, SpawnFlags.STDERR_TO_DEV_NULL, null, out output); - - } catch (SpawnError e) { - spawnError = e; - critical(e.message); - + rects = FaceDetect.face_detect_proxy.detect_faces(image_path, + AppDirs.get_haarcascade_file().get_path(), scale, true); + } catch(Error e) { + spawnError = "DBus error: " + e.message + "!\n"; return; } - faces = new Gee.PriorityQueue<string>(); - string[] lines = output.split("\n"); - foreach (string line in lines) { - if (line.length == 0) - continue; - - debug("shotwell-facedetect: %s", line); - - string[] type_and_serialized = line.split(";"); - if (type_and_serialized.length != 2) { - // Pass on external helper log output as our debug log - continue; - } - - switch (type_and_serialized[0]) { - case "face": - StringBuilder serialized_geometry = new StringBuilder(); - serialized_geometry.append(FaceRectangle.SHAPE_TYPE); - serialized_geometry.append(";"); - serialized_geometry.append(parse_serialized_geometry(type_and_serialized[1])); - - faces.add(serialized_geometry.str); - break; - - case "warning": - warning("%s\n", type_and_serialized[1]); - break; - - case "error": - critical("%s\n", type_and_serialized[1]); - assert_not_reached(); - - default: - break; + for (int i = 0; i < rects.length; i++) { + double rect_x, rect_y, rect_w, rect_h; + string face_vec_str = ""; + rect_w = rects[i].width / 2; + rect_h = rects[i].height / 2; + rect_x = rects[i].x + rect_w; + rect_y = rects[i].y + rect_h; + if (rects[i].vec != null) { + foreach (var d in rects[i].vec) { face_vec_str += d.to_string() + ","; } } + string serialized = "%s;%f;%f;%f;%f;%s".printf(FaceRectangle.SHAPE_TYPE, + rect_x, rect_y, rect_w, rect_h, + face_vec_str); + faces.add(serialized); } } - private string parse_serialized_geometry(string serialized_geometry) { - string[] serialized_geometry_pieces = serialized_geometry.split("&"); - if (serialized_geometry_pieces.length != 4) { - critical("Wrong serialized line in face detection program output."); - assert_not_reached(); - } - - double x = 0; - double y = 0; - double width = 0; - double height = 0; - foreach (string piece in serialized_geometry_pieces) { - - string[] name_and_value = piece.split("="); - if (name_and_value.length != 2) { - critical("Wrong serialized line in face detection program output."); - assert_not_reached(); - } - - switch (name_and_value[0]) { - case "x": - x = name_and_value[1].to_double(); - break; - - case "y": - y = name_and_value[1].to_double(); - break; - - case "width": - width = name_and_value[1].to_double(); - break; - - case "height": - height = name_and_value[1].to_double(); - break; - - default: - critical("Wrong serialized line in face detection program output."); - assert_not_reached(); - } - } - - double half_width = width / 2; - double half_height = height / 2; - - return "%s;%s;%s;%s".printf((x + half_width).to_string(), (y + half_height).to_string(), - half_width.to_string(), half_height.to_string()); - } - public string? get_next() { if (faces == null) return null; @@ -450,6 +378,7 @@ public class FacesTool : EditingTools.EditingTool { private Workers workers; private FaceShape editing_face_shape = null; private FacesToolWindow faces_tool_window = null; + private const int FACE_DETECT_MAX_WIDTH = 1200; private FacesTool() { base("FacesTool"); @@ -481,8 +410,10 @@ public class FacesTool : EditingTools.EditingTool { foreach (Gee.Map.Entry<FaceID?, FaceLocation> entry in face_locations.entries) { FaceShape new_face_shape; string serialized_geometry = entry.value.get_serialized_geometry(); + string serialized_vec = entry.value.get_serialized_vec(); + string face_shape_str = serialized_geometry + ";" + serialized_vec; try { - new_face_shape = FaceShape.from_serialized(canvas, serialized_geometry); + new_face_shape = FaceShape.from_serialized(canvas, face_shape_str); } catch (FaceShapeError e) { if (e is FaceShapeError.CANT_CREATE) continue; @@ -502,9 +433,12 @@ public class FacesTool : EditingTools.EditingTool { face_detection_cancellable = new Cancellable(); workers = new Workers(1, false); + Dimensions dimensions = canvas.get_photo().get_dimensions(); + float scale_factor = (float)dimensions.width / FACE_DETECT_MAX_WIDTH; face_detection = new FaceDetectionJob(faces_tool_window, - canvas.get_photo().get_file().get_path(), on_faces_detected, - face_detection_cancellable, on_detection_cancelled); + canvas.get_photo().get_file().get_path(), scale_factor, + on_faces_detected, + face_detection_cancellable, on_detection_cancelled); bind_window_handlers(); @@ -591,6 +525,10 @@ public class FacesTool : EditingTools.EditingTool { } public override void on_left_click(int x, int y) { + var scale = Application.get_scale(); + x = (int) Math.lround(x * scale); + y = (int) Math.lround(y * scale); + if (editing_face_shape != null && editing_face_shape.on_left_click(x, y)) return; @@ -607,6 +545,10 @@ public class FacesTool : EditingTools.EditingTool { } public override void on_left_released(int x, int y) { + var scale = Application.get_scale(); + x = (int) Math.lround(x * scale); + y = (int) Math.lround(y * scale); + if (editing_face_shape != null) { editing_face_shape.on_left_released(x, y); @@ -616,6 +558,10 @@ public class FacesTool : EditingTools.EditingTool { } public override void on_motion(int x, int y, Gdk.ModifierType mask) { + var scale = Application.get_scale(); + x = (int) Math.lround(x * scale); + y = (int) Math.lround(y * scale); + if (editing_face_shape == null) { FaceShape to_show = null; double distance = 0; @@ -784,14 +730,21 @@ public class FacesTool : EditingTools.EditingTool { if (face_shapes == null) return; - Gee.Map<Face, string> new_faces = new Gee.HashMap<Face, string>(); + Gee.Map<Face, FaceLocationData?> new_faces = new Gee.HashMap<Face, FaceLocationData?>(); foreach (FaceShape face_shape in face_shapes.values) { if (!face_shape.get_known()) continue; Face new_face = Face.for_name(face_shape.get_name()); - - new_faces.set(new_face, face_shape.serialize()); + string[] face_string = face_shape.serialize().split(";"); + string face_vec_str, face_geometry; + face_geometry = string.joinv(";", face_string[0:5]); + face_vec_str = face_string[5]; + FaceLocationData face_data = + { + face_geometry, face_vec_str + }; + new_faces.set(new_face, face_data); } ModifyFacesCommand command = new ModifyFacesCommand(canvas.get_photo(), new_faces); @@ -848,7 +801,7 @@ public class FacesTool : EditingTools.EditingTool { private void delete_face(string face_name) { face_shapes.unset(face_name); - // It is posible to have two visible faces at the same time, this happens + // It is possible to have two visible faces at the same time, this happens // if you are editing one face and you move the pointer around the // FaceWidgets area in FacesToolWindow. And you can delete one of that // faces, so the other visible face must be repainted. @@ -908,7 +861,6 @@ public class FacesTool : EditingTools.EditingTool { private void detect_faces() { faces_tool_window.detection_button.set_sensitive(false); faces_tool_window.set_editing_phase(EditingPhase.DETECTING_FACES); - workers.enqueue(face_detection); } @@ -945,19 +897,71 @@ public class FacesTool : EditingTools.EditingTool { continue; c++; + // Reference faces to match with + Face? guess = get_face_match(face_shape, 0.7); - face_shape.set_name("Unknown face #%d".printf(c)); - face_shape.set_known(false); + if (guess == null) { + face_shape.set_name("Unknown face #%d".printf(c)); + face_shape.set_known(false); + } else { + string name_str; + name_str = "%s (%0.2f%%)".printf(guess.get_name(), face_shape.get_guess() * 100); + face_shape.set_name(name_str); + face_shape.set_known(true); + } add_face(face_shape); } } + private double dot_product(double[] vec1, double[] vec2) { + if (vec1.length != vec2.length) { + return 0; + } + + double ret = 0; + for (var i = 0; i < vec1.length; i++) { + ret += vec1[i] * vec2[i]; + } + return ret; + } + + private Face? get_face_match(FaceShape face_shape, double threshold) { + Gee.List<FaceLocationRow?> face_vecs; + try { + Gee.List<FaceRow?> face_rows = FaceTable.get_instance().get_ref_rows(); + face_vecs = FaceLocationTable.get_instance().get_face_ref_vecs(face_rows); + } catch(DatabaseError err) { + warning("Cannot get reference faces from DB"); + return null; + } + FaceID? guess_id = null; + double max_product = threshold; + foreach (var row in face_vecs) { + string[] vec_str = row.vec.split(","); + double[] vec = {}; + foreach (var d in vec_str) vec += double.parse(d); + double product = dot_product(face_shape.get_face_vec(), vec[0:128]); + if (product > max_product) { + max_product = product; + guess_id = row.face_id; + } + } + + Face? face = null; + if (guess_id != null) { + face = Face.global.fetch(guess_id); + face_shape.set_guess(max_product); + assert(face != null); + } + return face; + } + private void on_faces_detected() { face_detection_cancellable.reset(); if (face_detection.spawnError != null){ string spawnErrorMessage = _("Error trying to spawn face detection program:\n"); - AppWindow.error_message(spawnErrorMessage + face_detection.spawnError.message + "\n"); + AppWindow.error_message(spawnErrorMessage + face_detection.spawnError + "\n"); faces_tool_window.set_editing_phase(EditingPhase.DETECTING_FACES_FINISHED); } else pick_faces_from_autodetected(); |