Commit 2848610f authored by Ricardo Fantin da Costa's avatar Ricardo Fantin da Costa Committed by Jens Georg

A clean-diff of faces branch on top of master

parent da54c796
This diff is collapsed.
subproject = ('facedetect')
add_languages('cpp')
facedetect_dep = dependency('opencv', version : ['>= 2.3.0'], required : true)
executable('shotwell-facedetect',
'shotwell-facedetect.cpp',
dependencies : facedetect_dep,
install : true,
install_dir : join_paths(get_option('libexecdir'), 'shotwell'))
install_data('facedetect-haarcascade.xml',
install_dir : join_paths(get_option('datadir'), 'shotwell'))
#include "opencv2/objdetect/objdetect.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include <iostream>
#include <stdio.h>
using namespace std;
using namespace cv;
void help() {
cout <<
"Usage:" << endl <<
"./facedetect --cascade=<cascade_path> "
"--scale=<image scale greater or equal to 1, try 1.3 for example> "
"filename" << endl << endl <<
"Example:" << endl <<
"./facedetect --cascade=\"./data/haarcascades/haarcascade_frontalface_alt.xml\" "
"--scale=1.3 ./photo.jpg" << endl << endl <<
"Using OpenCV version " << CV_VERSION << endl;
}
void detectFaces(Mat &img, CascadeClassifier &cascade, double scale) {
Mat gray;
cvtColor(img, gray, CV_BGR2GRAY);
Mat smallImg(cvRound(img.rows / scale), cvRound(img.cols / scale), CV_8UC1);
Size smallImgSize = smallImg.size();
resize(gray, smallImg, smallImgSize, 0, 0, INTER_LINEAR);
equalizeHist(smallImg, smallImg);
vector<Rect> faces;
cascade.detectMultiScale(smallImg, faces, 1.1, 2, CV_HAAR_SCALE_IMAGE, Size(30, 30));
int i = 0;
for (vector<Rect>::const_iterator r = faces.begin(); r != faces.end(); r++, i++) {
printf(
"face;x=%f&y=%f&width=%f&height=%f\n",
(float) r->x / smallImgSize.width,
(float) r->y / smallImgSize.height,
(float) r->width / smallImgSize.width,
(float) r->height / smallImgSize.height
);
}
}
int main(int argc, const char** argv) {
const std::string scaleOpt = "--scale=";
size_t scaleOptLen = scaleOpt.length();
const std::string cascadeOpt = "--cascade=";
size_t cascadeOptLen = cascadeOpt.length();
std::string cascadeName, inputName;
double scale = 1;
for (int i = 1; i < argc; i++) {
if (cascadeOpt.compare(0, cascadeOptLen, argv[i], cascadeOptLen) == 0) {
cascadeName.assign(argv[i] + cascadeOptLen);
} else if (scaleOpt.compare(0, scaleOptLen, argv[i], scaleOptLen) == 0) {
if (!sscanf(argv[i] + scaleOpt.length(), "%lf", &scale) || scale < 1)
scale = 1;
} else if (argv[i][0] == '-') {
cout << "warning;Unknown option " << argv[i] << endl;
} else
inputName.assign(argv[i]);
}
if (cascadeName.empty()) {
cout << "error;You must specify the cascade." << endl;
help();
return -1;
}
CascadeClassifier cascade;
if (!cascade.load(cascadeName)) {
cout << "error;Could not load classifier cascade. Filename: \"" << cascadeName << "\"" << endl;
return -1;
}
if (inputName.empty()) {
cout << "error;You must specify the file to process." << endl;
help();
return -1;
}
Mat image = imread(inputName, 1);
if (image.empty()) {
cout << "error;Could not load the file to process. Filename: \"" << inputName << "\"" << endl;
return -1;
}
detectFaces(image, cascade, scale);
return 0;
}
......@@ -82,6 +82,11 @@ if not get_option('dupe-detection')
add_global_arguments(['--define=NO_DUPE_DETECTION'], language : vala)
endif
if get_option('face')
add_global_arguments(['--define=ENABLE_FACES'], language : 'vala')
subdir('facedetect')
endif
json_glib = dependency('json-glib-1.0')
gdata = dependency('libgdata')
gcr = dependency('gcr-3')
......
......@@ -5,3 +5,4 @@ option('trace', type: 'string', value : '', description: 'Enable various trace
option('measure', type: 'string', value : '', description : 'Enable various timing measurements(available : enhance, import, pipeline, view-filtering, thumbnail-cache)')
option('dupe-detection', type: 'boolean', value : 'true', description: 'Disable duplicate checks')
option('install-apport-hook', type : 'boolean', value : 'true', description: 'Enable Ubuntu apport hook')
option('face', type:'boolean', value:false)
......@@ -7,6 +7,7 @@
<file preprocess="xml-stripblanks">ui/direct.ui</file>
<file preprocess="xml-stripblanks">ui/events_directory.ui</file>
<file preprocess="xml-stripblanks">ui/event.ui</file>
<file preprocess="xml-stripblanks">ui/faces.ui</file>
<file preprocess="xml-stripblanks">ui/fullscreen.ui</file>
<file preprocess="xml-stripblanks">ui/import_queue.ui</file>
<file preprocess="xml-stripblanks">ui/import.ui</file>
......
......@@ -258,6 +258,15 @@ class AppDirs {
return subdir;
}
#if ENABLE_FACES
public static File get_resources_dir() {
File? install_dir = get_install_dir();
return (install_dir != null) ? install_dir.get_child("share").get_child("shotwell")
: get_exec_dir();
}
#endif
public static File get_lib_dir() {
File? install_dir = get_install_dir();
......@@ -319,5 +328,25 @@ class AppDirs {
}
return f;
}
#if ENABLE_FACES
public static File get_facedetect_bin() {
const string filename = "shotwell-facedetect";
File f = AppDirs.get_libexec_dir().get_parent().get_child("facedetect").get_child (filename);
if (!f.query_exists()) {
f = AppDirs.get_libexec_dir().get_child("shotwell").get_child(filename);
}
return f;
}
public static File get_haarcascade_file() {
File f = File.new_for_path(AppDirs.get_exec_dir().get_parent().get_parent().get_child("facedetect").get_child("facedetect-haarcascade.xml").get_path());
if (f.query_exists()) {//testing meson builddir
return f;
}
return get_resources_dir().get_child("facedetect-haarcascade.xml");
}
#endif
}
......@@ -920,7 +920,6 @@ public abstract class MovePhotosCommand : Command {
}
public override void execute() {
// create the new event
base.execute();
......@@ -2507,3 +2506,220 @@ public class FlagUnflagCommand : MultipleDataSourceAtOnceCommand {
}
}
}
#if ENABLE_FACES
public class RemoveFacesFromPhotosCommand : SimpleProxyableCommand {
private Gee.Map<MediaSource, string> map_source_geometry = new Gee.HashMap<MediaSource, string>();
public RemoveFacesFromPhotosCommand(Face face, Gee.Collection<MediaSource> sources) {
base (face,
Resources.remove_face_from_photos_label(face.get_name(), sources.size),
face.get_name());
foreach (MediaSource source in sources) {
FaceLocation? face_location =
FaceLocation.get_face_location(face.get_face_id(), ((Photo) source).get_photo_id());
assert(face_location != null);
this.map_source_geometry.set(source, face_location.get_serialized_geometry());
}
LibraryPhoto.global.item_destroyed.connect(on_source_destroyed);
Video.global.item_destroyed.connect(on_source_destroyed);
}
~RemoveFacesFromPhotosCommand() {
LibraryPhoto.global.item_destroyed.disconnect(on_source_destroyed);
Video.global.item_destroyed.disconnect(on_source_destroyed);
}
public override void execute_on_source(DataSource source) {
((Face) source).detach_many(map_source_geometry.keys);
}
public override void undo_on_source(DataSource source) {
Face face = (Face) source;
face.attach_many(map_source_geometry.keys);
foreach (Gee.Map.Entry<MediaSource, string> entry in map_source_geometry.entries)
FaceLocation.create(face.get_face_id(), ((Photo) entry.key).get_photo_id(), entry.value);
}
private void on_source_destroyed(DataSource source) {
if (map_source_geometry.keys.contains((MediaSource) source))
get_command_manager().reset();
}
}
public class RenameFaceCommand : SimpleProxyableCommand {
private string old_name;
private string new_name;
public RenameFaceCommand(Face face, string new_name) {
base (face, Resources.rename_face_label(face.get_name(), new_name), face.get_name());
old_name = face.get_name();
this.new_name = new_name;
}
protected override void execute_on_source(DataSource source) {
if (!((Face) source).rename(new_name))
AppWindow.error_message(Resources.rename_face_exists_message(new_name));
}
protected override void undo_on_source(DataSource source) {
if (!((Face) source).rename(old_name))
AppWindow.error_message(Resources.rename_face_exists_message(old_name));
}
}
public class DeleteFaceCommand : SimpleProxyableCommand {
private Gee.Map<PhotoID?, string> photo_geometry_map = new Gee.HashMap<PhotoID?, string>
((Gee.HashDataFunc)FaceLocation.photo_id_hash, (Gee.EqualDataFunc)FaceLocation.photo_ids_equal);
public DeleteFaceCommand(Face face) {
base (face, Resources.delete_face_label(face.get_name()), face.get_name());
// we can't use the Gee.Map returned by FaceLocation.get_locations_by_face
// because it will be modified in execute_on_source
Gee.Map<PhotoID?, FaceLocation>? temp = FaceLocation.get_locations_by_face(face);
assert(temp != null);
foreach (Gee.Map.Entry<PhotoID?, FaceLocation> entry in temp.entries)
photo_geometry_map.set(entry.key, entry.value.get_serialized_geometry());
}
protected override void execute_on_source(DataSource source) {
FaceID face_id = ((Face) source).get_face_id();
foreach (PhotoID photo_id in photo_geometry_map.keys)
FaceLocation.destroy(face_id, photo_id);
Face.global.destroy_marked(Face.global.mark(source), false);
}
protected override void undo_on_source(DataSource source) {
// merely instantiating the Face will rehydrate it ... should always work, because the
// undo stack is cleared if the proxy ever breaks
assert(source is Face);
foreach (Gee.Map.Entry<PhotoID?, string> entry in photo_geometry_map.entries) {
Photo? photo = LibraryPhoto.global.fetch(entry.key);
if (photo != null) {
Face face = (Face) source;
face.attach(photo);
FaceLocation.create(face.get_face_id(), entry.key, entry.value);
}
}
}
}
public class ModifyFacesCommand : SingleDataSourceCommand {
private MediaSource media;
private Gee.ArrayList<SourceProxy> to_add = new Gee.ArrayList<SourceProxy>();
private Gee.ArrayList<SourceProxy> to_remove = new Gee.ArrayList<SourceProxy>();
private Gee.Map<SourceProxy, string> to_update = new Gee.HashMap<SourceProxy, string>();
private Gee.Map<SourceProxy, string> geometries = new Gee.HashMap<SourceProxy, string>();
public ModifyFacesCommand(MediaSource media, Gee.Map<Face, string> new_face_list) {
base (media, Resources.MODIFY_FACES_LABEL, "");
this.media = media;
// Remove any face that's in the original list but not the new one
Gee.Collection<Face>? original_faces = Face.global.fetch_for_source(media);
if (original_faces != null) {
foreach (Face face in original_faces) {
if (!new_face_list.keys.contains(face)) {
SourceProxy proxy = face.get_proxy();
to_remove.add(proxy);
proxy.broken.connect(on_proxy_broken);
FaceLocation? face_location =
FaceLocation.get_face_location(face.get_face_id(), ((Photo) media).get_photo_id());
assert(face_location != null);
geometries.set(proxy, face_location.get_serialized_geometry());
}
}
}
// Add any face that's in the new list but not the original
foreach (Gee.Map.Entry<Face, string> entry in new_face_list.entries) {
if (original_faces == null || !original_faces.contains(entry.key)) {
SourceProxy proxy = entry.key.get_proxy();
to_add.add(proxy);
proxy.broken.connect(on_proxy_broken);
geometries.set(proxy, entry.value);
} else {
// If it is already in the original list we need to check if it's
// geometry has changed.
FaceLocation? face_location =
FaceLocation.get_face_location(entry.key.get_face_id(), ((Photo) media).get_photo_id());
assert(face_location != null);
string old_geometry = face_location.get_serialized_geometry();
if (old_geometry != entry.value) {
SourceProxy proxy = entry.key.get_proxy();
to_update.set(proxy, entry.value);
proxy.broken.connect(on_proxy_broken);
geometries.set(proxy, old_geometry);
}
}
}
}
~ModifyFacesCommand() {
foreach (SourceProxy proxy in to_add)
proxy.broken.disconnect(on_proxy_broken);
foreach (SourceProxy proxy in to_remove)
proxy.broken.disconnect(on_proxy_broken);
foreach (SourceProxy proxy in to_update.keys)
proxy.broken.disconnect(on_proxy_broken);
}
public override void execute() {
foreach (SourceProxy proxy in to_add) {
Face face = (Face) proxy.get_source();
face.attach(media);
FaceLocation.create(face.get_face_id(), ((Photo) media).get_photo_id(), geometries.get(proxy));
}
foreach (SourceProxy proxy in to_remove)
((Face) proxy.get_source()).detach(media);
foreach (Gee.Map.Entry<SourceProxy, string> entry in to_update.entries) {
Face face = (Face) entry.key.get_source();
FaceLocation.create(face.get_face_id(), ((Photo) media).get_photo_id(), entry.value);
}
}
public override void undo() {
foreach (SourceProxy proxy in to_add)
((Face) proxy.get_source()).detach(media);
foreach (SourceProxy proxy in to_remove) {
Face face = (Face) proxy.get_source();
face.attach(media);
FaceLocation.create(face.get_face_id(), ((Photo) media).get_photo_id(), geometries.get(proxy));
}
foreach (SourceProxy proxy in to_update.keys) {
Face face = (Face) proxy.get_source();
FaceLocation.create(face.get_face_id(), ((Photo) media).get_photo_id(), geometries.get(proxy));
}
}
private void on_proxy_broken() {
get_command_manager().reset();
}
}
#endif
......@@ -46,6 +46,21 @@ public bool confirm_warn_developer_changed(int number) {
return response == Gtk.ResponseType.YES;
}
#if ENABLE_FACES
public bool confirm_delete_face(Face face) {
int count = face.get_sources_count();
string msg = ngettext(
"This will remove the face \"%s\" from one photo. Continue?",
"This will remove the face \"%s\" from %d photos. Continue?",
count).printf(face.get_name(), count);
return AppWindow.negate_affirm_question(msg, _("_Cancel"), _("_Delete"),
Resources.DELETE_FACE_TITLE);
}
#endif
}
namespace ExportUI {
......@@ -133,7 +148,6 @@ public Gtk.ResponseType export_error_dialog(File dest, bool photos_remaining) {
return response;
}
namespace ImportUI {
private const int REPORT_FAILURE_COUNT = 4;
internal const string SAVE_RESULTS_BUTTON_NAME = _("Save Details…");
......@@ -644,7 +658,6 @@ public string build_alert_body_text(string? primary_text, string? secondary_text
guarded_markup_escape_text(primary_text), secondary_text);
}
public class EventRenameDialog : TextEntryDialogMediator {
public EventRenameDialog(string? event_name) {
base (_("Rename Event"), _("Name:"), event_name);
......
......@@ -5216,6 +5216,22 @@ public class LibraryPhoto : Photo, Flaggable, Monitorable {
}
}
#if ENABLE_FACES
// Attach faces.
Gee.Collection<Face>? faces = Face.global.fetch_for_source(this);
if (faces != null) {
foreach (Face face in faces) {
FaceLocation? location = FaceLocation.get_face_location(face.get_face_id(),
this.get_photo_id());
if (location != null) {
face.attach(dupe);
FaceLocation.create(face.get_face_id(), dupe.get_photo_id(),
location.get_serialized_geometry());
}
}
}
#endif
return dupe;
}
......
......@@ -396,6 +396,9 @@ public abstract class EditingHostPage : SinglePhotoPage {
private Gtk.ToggleToolButton redeye_button = null;
private Gtk.ToggleToolButton adjust_button = null;
private Gtk.ToggleToolButton straighten_button = null;
#if ENABLE_FACES
private Gtk.ToggleToolButton faces_button = null;
#endif
private Gtk.ToolButton enhance_button = null;
private Gtk.Scale zoom_slider = null;
private Gtk.ToolButton prev_button = new Gtk.ToolButton(null, Resources.PREVIOUS_LABEL);
......@@ -488,6 +491,13 @@ public abstract class EditingHostPage : SinglePhotoPage {
enhance_button.is_important = true;
toolbar.insert(enhance_button, -1);
#if ENABLE_FACES
// faces tool
insert_faces_button(toolbar);
faces_button = new Gtk.ToggleToolButton();
//face_button
#endif
// separator to force next/prev buttons to right side of toolbar
Gtk.SeparatorToolItem separator = new Gtk.SeparatorToolItem();
separator.set_expand(true);
......@@ -2325,6 +2335,14 @@ public abstract class EditingHostPage : SinglePhotoPage {
protected void unset_view_collection() {
parent_view = null;
}
// This method is intentionally empty --its purpose is to allow overriding
// it in LibraryPhotoPage, since FacesTool must only be present in
// LibraryMode, but it need to be called from constructor of EditingHostPage
// to place it correctly in the toolbar.
protected virtual void insert_faces_button(Gtk.Toolbar toolbar) {
;
}
}
//
......@@ -2339,6 +2357,9 @@ public class LibraryPhotoPage : EditingHostPage {
}
}
#if ENABLE_FACES
private Gtk.ToggleToolButton faces_button = null;
#endif
private CollectionPage? return_page = null;
private bool return_to_collection_on_release = false;
private LibraryPhotoPageViewFilter filter = new LibraryPhotoPageViewFilter();
......@@ -3146,5 +3167,25 @@ public class LibraryPhotoPage : EditingHostPage {
get_command_manager().execute(new ModifyTagsCommand(photo, new_tags));
}
#if ENABLE_FACES
private void on_faces_toggled() {
on_tool_button_toggled(faces_button, EditingTools.FacesTool.factory);
}
protected void toggle_faces() {
faces_button.set_active(!faces_button.get_active());
}
protected override void insert_faces_button(Gtk.Toolbar toolbar) {
faces_button = new Gtk.ToggleToolButton.from_stock(Resources.FACES_TOOL);
faces_button.set_icon_name(Resources.ICON_FACES);
faces_button.set_label(Resources.FACES_LABEL);
faces_button.set_tooltip_text(Resources.FACES_TOOLTIP);
faces_button.toggled.connect(on_faces_toggled);
faces_button.is_important = true;
toolbar.insert(faces_button, -1);
}
#endif
}
......@@ -76,6 +76,9 @@ along with Shotwell; if not, write to the Free Software Foundation, Inc.,
public const string IMPORT_ALL = "filter-photos-symbolic";
public const string ENHANCE = "image-auto-adjust-symbolic";
public const string PUBLISH = "send-to-symbolic";
#if ENABLE_FACES
public const string FACES_TOOL = "faces";
#endif
public const string GO_NEXT = "go-next-symbolic";
public const string GO_PREVIOUS = "go-previous-symbolic";
......@@ -103,6 +106,10 @@ along with Shotwell; if not, write to the Free Software Foundation, Inc.,
public const string ICON_SINGLE_PHOTO = "image-x-generic-symbolic";
public const string ICON_TRASH_EMPTY = "user-trash-symbolic";
public const string ICON_TRASH_FULL = "user-trash-full-symbolic";
#if ENABLE_FACES
public const string ICON_ONE_FACE = "one-face";
public const string ICON_FACES = "faces-tool";
#endif
public const string ROTATE_CW_MENU = _("Rotate _Right");
public const string ROTATE_CW_LABEL = _("Rotate");
......@@ -299,6 +306,19 @@ along with Shotwell; if not, write to the Free Software Foundation, Inc.,
public const string UNFLAG_MENU = _("Un_flag");
#if ENABLE_FACES
public const string FACES_MENU = _("Faces");
public const string FACES_LABEL = _("Faces");
public const string FACES_TOOLTIP = _("Mark faces of people in the photo");
public const string MODIFY_FACES_LABEL = _("Modify Faces");
public const string DELETE_FACE_TITLE = _("Delete Face");
public const string DELETE_FACE_SIDEBAR_MENU = _("_Delete");
public const string RENAME_FACE_SIDEBAR_MENU = _("_Rename…");
public const string FACES_MENU_SECTION = _("FacesMenuPlaceholder");
#endif
public string launch_editor_failed(Error err) {
return _("Unable to launch editor: %s").printf(err.message);
}
......@@ -390,6 +410,38 @@ along with Shotwell; if not, write to the Free Software Foundation, Inc.,
public string delete_search_label(string name) {
return _("Delete Search “%s”").printf(name);
}
#if ENABLE_FACES
public static string rename_face_exists_message(string name) {
return _("Unable to rename face to \"%s\" because the face already exists.").printf(name);
}
public string remove_face_from_photos_menu(string name, int count) {
return ((count == 1) ? _("Remove Face \"%s\" From _Photo") :
_("Remove Face \"%s\" From _Photos")).printf(name);
}
public string remove_face_from_photos_label(string name, int count) {
return ((count == 1) ? _("Remove Face \"%s\" From Photo") :
_("Remove Face \"%s\" From Photos")).printf(name);
}
public string rename_face_menu(string name) {
return _("Re_name Face \"%s\"...").printf(name);
}
public string rename_face_label(string old_name, string new_name) {
return _("Rename Face \"%s\" to \"%s\"").printf(old_name, new_name);
}
public string delete_face_menu(string name) {
return _("_Delete Face \"%s\"").printf(name);
}
public string delete_face_label(string name) {
return _("Delete Face \"%s\"").printf(name);
}
#endif
private unowned string rating_label(Rating rating) {
switch (rating) {
......@@ -984,7 +1036,7 @@ along with Shotwell; if not, write to the Free Software Foundation, Inc.,
if (dir.get_path().has_suffix("src")) {
dir = dir.get_parent().get_parent();
}
File help_dir = dir.get_child("help").get_child("C");
File help_index = help_dir.get_child("index.page");
......
......@@ -223,6 +223,10 @@ public abstract class DefaultSearchViewFilter : SearchViewFilter {
Gee.List<Tag>? tags = Tag.global.fetch_for_source(source);
int tags_size = (tags != null) ? tags.size : 0;
#if ENABLE_FACES
Gee.List<Face>? faces = Face.global.fetch_for_source(source);
#endif
foreach (unowned string word in get_search_filter_words()) {
if (media_keywords != null && media_keywords.contains(word))
continue;
......@@ -245,6 +249,22 @@ public abstract class DefaultSearchViewFilter : SearchViewFilter {
continue;
}
#if ENABLE_FACES
if (faces != null) {
bool found = false;
foreach (Face f in faces) {
unowned string? face_keywords = f.get_indexable_keywords();
if (face_keywords != null && face_keywords.contains(word)) {
found = true;
break;
}
}
if (found)
continue;
}
#endif
// failed all tests (this even works if none of the Indexables have strings,
// as they fail the implicit AND test)
return false;
......
/* Copyright 2011-2012 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
#if ENABLE_FACES
public struct FaceLocationID {
public const int64 INVALID = -1;
public int64 id;
public FaceLocationID(int64 id = INVALID) {
this.id = id;
}
public bool is_invalid() {
return (id == INVALID);
}
public bool is_valid() {
return (id != INVALID);
}
}