summaryrefslogtreecommitdiff
path: root/src/faces
diff options
context:
space:
mode:
authorJörg Frings-Fürst <debian@jff.email>2023-06-14 20:36:17 +0200
committerJörg Frings-Fürst <debian@jff.email>2023-06-14 20:36:17 +0200
commit31804433d72460cbe0a39f9f8ea5e76058d84cda (patch)
tree2084a84c39f159c6aea254775dc0880d52579d45 /src/faces
parenta9898fb3f39c44a85876930ef6b2558052569ae6 (diff)
parentd443a3c2509889533ca812c163056bace396b586 (diff)
Update upstream source from tag 'upstream/0.32.1'
Update to upstream version '0.32.1' with Debian dir c460ad6e13d3c39eaa2d5399059385e64e6fba4c
Diffstat (limited to 'src/faces')
-rw-r--r--src/faces/Face.vala30
-rw-r--r--src/faces/FaceDetect.vala146
-rw-r--r--src/faces/FaceLocation.vala45
-rw-r--r--src/faces/FacePage.vala14
-rw-r--r--src/faces/FaceShape.vala120
-rw-r--r--src/faces/FacesTool.vala230
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();