Commit 56acbed6 authored by NMA's avatar NMA Committed by Jens Georg

Wip/faces

parent 8d5e870d
# Hide sublime text stuff
*.sublime-project
*.sublime-workspace
# Hide some OS X stuff
.DS_Store
.AppleDouble
.LSOverride
Icon
# Thumbnails
._*
.idea
# pytest
.cache
# GITHUB Proposed Python stuff:
*.py[cod]
# C extensions
*.so
# Packages
*.egg
*.egg-info
dist
build
eggs
.eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
lib
lib64
# Logs
*.log
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
nosetests.xml
# Translations
*.mo
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
.python-version
# emacs auto backups
*~
*#
*.orig
# venv stuff
pyvenv.cfg
pip-selfcheck.json
venv
.venv
# vimmy stuff
*.swp
*.swo
ctags.tmp
# vagrant stuff
virtualization/vagrant/setup_done
virtualization/vagrant/.vagrant
virtualization/vagrant/config
# Visual Studio Code
.vscode
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'))
/* Copyright 2016 Software Freedom Conservancy Inc.
*
* Copyright 2011 Valentín Barros Puertas <valentin(at)sanva(dot)net>
* Copyright 2018 Ricardo Fantin da Costa <ricardofantin(at)gmail(dot)com>
*
* This software is licensed under the GNU LGPL (version 2.1 or later).
* See the COPYING file in this distribution.
*/
#include <opencv2/objdetect/objdetect.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/imgcodecs/imgcodecs.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;
}
{
"app-id" : "org.gnome.Shotwell.Faces1",
"runtime" : "org.gnome.Platform",
"runtime-version" : "3.30",
"sdk" : "org.gnome.Sdk",
"tags" : [
"nightly"
],
"desktop-file-name-prefix" : "(Nightly) ",
"finish-args" : [
"--env=DCONF_USER_CONFIG_DIR=.config/dconf",
"--filesystem=~/.config/dconf:ro",
"--filesystem=xdg-download",
"--filesystem=xdg-pictures",
"--share=ipc",
"--talk-name=org.gtk.vfs",
"--talk-name=org.gtk.vfs.*"
],
"cleanup" : [
"/include",
"/lib/pkconfig",
"/share/pkgconfig",
"/share/gtk-doc",
"/share/man",
"/share/vala",
"/lib/girepository",
"*.la",
"*.a"
],
"modules" : [
{
"name" : "opencv",
"buildsystem" : "cmake",
"builddir" : true,
"cleanup" : [
"/share/OpenCV/*.cmake",
"/share/OpenCV/*.supp"
],
"config-opts" : [
"-DBUILD_TESTS=OFF",
"-DBUILD_EXAMPLES=OFF",
"-DBUILD_PERF_TESTS=OFF",
"-DWITH_FFMPEG=OFF",
"-DWITH_GTK=OFF",
"-DWITH_GSTREAMER=OFF",
"-DWITH_JASPER=OFF",
"-DWITH_OPENEXR=OFF",
"-DWITH_GDAL=OFF",
"-DWITH_GDCM=OFF",
"-DBUILD_opencv_apps=OFF",
"-DCMAKE_INSTALL_LIBDIR=lib",
"-DBUILD_LIST=imgproc,imgcodecs,objdetect,dnn"
],
"sources" : [
{
"type" : "git",
"tag" : "3.4.1",
"commit" : "6ffc48769ac60d53c4bd1913eac15117c9b1c9f7",
"url" : "https://github.com/opencv/opencv"
}
]
},
{
"name" : "shotwell-facedetect",
"buildsystem" : "meson",
"subdir" : "subprojects/shotwell-facedetect",
"sources" : [
{
"type" : "git",
"path": "/home/jgeorg/Source/GNOME/shotwell",
"branch" : "enhanced-faces"
},
{
"type" : "extra-data",
"filename" : "openface.nn4.small2.v1.t7",
"url" : "https://storage.cmusatyalab.org/openface-models/nn4.small2.v1.t7",
"sha256" : "9b72d54aeb24a64a8135dca8e792f7cc675c99a884a6940350a6cedcf7b7ba08",
"size" : 31510785
},
{
"type" : "extra-data",
"filename" : "res10_300x300_ssd_iter_140000_fp16.caffemodel",
"url" : "https://raw.githubusercontent.com/opencv/opencv_3rdparty/19512576c112aa2c7b6328cb0e8d589a4a90a26d/res10_300x300_ssd_iter_140000_fp16.caffemodel",
"sha256" : "510ffd2471bd81e3fcc88a5beb4eae4fb445ccf8333ebc54e7302b83f4158a76",
"size" : 5351047
},
{
"type" : "extra-data",
"filename" : "deploy.prototxt",
"url" : "https://raw.githubusercontent.com/opencv/opencv/master/samples/dnn/face_detector/deploy.prototxt",
"sha256" : "f62621cac923d6f37bd669298c428bb7ee72233b5f8c3389bb893e35ebbcf795",
"size" : 28092
}
]
}
]
}
......@@ -221,7 +221,8 @@
"config-opts" : [
"-Dudev=false",
"-Dinstall-apport-hook=false",
"-Dface-detection=true"
"-Dface-detection=true",
"-Dface-detection-helper=false"
],
"sources" : [
{
......
......@@ -88,7 +88,9 @@ endif
if get_option('face-detection')
add_global_arguments(['--define=ENABLE_FACES'], language : 'vala')
subdir('facedetect')
if get_option('face-detection-helper')
subproject('shotwell-facedetect')
endif
endif
json_glib = dependency('json-glib-1.0')
......
......@@ -7,3 +7,4 @@ option('dupe-detection', type: 'boolean', value : 'true', description: 'Disable
option('udev', type: 'boolean', value : 'true', description: 'Enable or disable udev support')
option('install-apport-hook', type : 'boolean', value : 'true', description: 'Enable Ubuntu apport hook')
option('face-detection', type:'boolean', value:false)
option('face-detection-helper', type : 'boolean', value : 'true', description : 'If face-detection is enabled, build the external helper tool')
......@@ -210,7 +210,7 @@ class AppDirs {
return tmp_dir;
}
public static File get_data_subdir(string name, string? subname = null) {
File subdir = get_data_dir().get_child(name);
if (subname != null)
......@@ -338,7 +338,7 @@ class AppDirs {
}
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
......@@ -346,6 +346,10 @@ class AppDirs {
}
return get_resources_dir().get_child("facedetect-haarcascade.xml");
}
public static File get_openface_dnn_dir() {
return get_data_subdir("data"); //get_child("openface.nn4.small2.v1.t7");
}
#endif
}
......
......@@ -566,7 +566,7 @@ public abstract class AppWindow : PageWindow {
panic(_("A fatal error occurred when accessing Shotwell’s library. Shotwell cannot continue.\n\n%s").printf(
err.message));
}
public static void panic(string msg) {
critical(msg);
error_message(msg);
......
......@@ -2542,7 +2542,8 @@ public class RemoveFacesFromPhotosCommand : SimpleProxyableCommand {
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);
FaceLocation.create(face.get_face_id(), ((Photo) entry.key).get_photo_id(),
{ entry.value, null });
}
private void on_source_destroyed(DataSource source) {
......@@ -2573,6 +2574,26 @@ public class RenameFaceCommand : SimpleProxyableCommand {
}
}
public class SetFaceRefCommand : SimpleProxyableCommand {
private FaceLocation face_loc;
public SetFaceRefCommand(Face face, MediaSource source) {
base (face, Resources.set_face_from_photo_label(face.get_name()), face.get_name());
Gee.Map<FaceID?, FaceLocation>? face_loc_map = FaceLocation.get_locations_by_photo((Photo)source);
face_loc = face_loc_map.get(face.get_face_id());
}
protected override void execute_on_source(DataSource source) {
if (!((Face) source).set_reference(face_loc))
AppWindow.error_message(Resources.set_face_from_photo_error());
}
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);
......@@ -2608,7 +2629,8 @@ public class DeleteFaceCommand : SimpleProxyableCommand {
Face face = (Face) source;
face.attach(photo);
FaceLocation.create(face.get_face_id(), entry.key, entry.value);
FaceLocation.create(face.get_face_id(), entry.key,
{ entry.value, null });
}
}
}
......@@ -2618,10 +2640,10 @@ 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>();
private Gee.Map<SourceProxy, FaceLocationData?> to_update = new Gee.HashMap<SourceProxy, FaceLocationData?>();
private Gee.Map<SourceProxy, FaceLocationData?> geometries = new Gee.HashMap<SourceProxy, FaceLocationData?>();
public ModifyFacesCommand(MediaSource media, Gee.Map<Face, string> new_face_list) {
public ModifyFacesCommand(MediaSource media, Gee.Map<Face, FaceLocationData?> new_face_list) {
base (media, Resources.MODIFY_FACES_LABEL, "");
this.media = media;
......@@ -2640,13 +2662,13 @@ public class ModifyFacesCommand : SingleDataSourceCommand {
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());
geometries.set(proxy, face_location.get_face_data());
}
}
}
// 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) {
foreach (Gee.Map.Entry<Face, FaceLocationData?> entry in new_face_list.entries) {
if (original_faces == null || !original_faces.contains(entry.key)) {
SourceProxy proxy = entry.key.get_proxy();
......@@ -2662,13 +2684,13 @@ public class ModifyFacesCommand : SingleDataSourceCommand {
assert(face_location != null);
string old_geometry = face_location.get_serialized_geometry();
if (old_geometry != entry.value) {
if (old_geometry != entry.value.geometry) {
SourceProxy proxy = entry.key.get_proxy();
to_update.set(proxy, entry.value);
proxy.broken.connect(on_proxy_broken);
geometries.set(proxy, old_geometry);
geometries.set(proxy, face_location.get_face_data());
}
}
}
......@@ -2695,7 +2717,7 @@ public class ModifyFacesCommand : SingleDataSourceCommand {
foreach (SourceProxy proxy in to_remove)
((Face) proxy.get_source()).detach(media);
foreach (Gee.Map.Entry<SourceProxy, string> entry in to_update.entries) {
foreach (Gee.Map.Entry<SourceProxy, FaceLocationData?> 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);
}
......
......@@ -5222,7 +5222,7 @@ public class LibraryPhoto : Photo, Flaggable, Monitorable {
if (location != null) {
face.attach(dupe);
FaceLocation.create(face.get_face_id(), dupe.get_photo_id(),
location.get_serialized_geometry());
location.get_face_data());
}
}
}
......
......@@ -411,6 +411,18 @@ along with Shotwell; if not, write to the Free Software Foundation, Inc.,
return ngettext ("Remove Face “%s” From Photo",
"Remove Face “%s” From Photos", count).printf(name);
}
public string set_face_from_photo_menu(string name) {
return _("_Train Face “%s” From Photo").printf(name);
}
public string set_face_from_photo_label(string name) {
return _("_Train Face “%s” From Photo").printf(name);
}
public static string set_face_from_photo_error() {
return "Unable to set face as reference";
}
public string rename_face_menu(string name) {
return _("Re_name Face “%s”…").printf(name);
......
......@@ -21,7 +21,7 @@ public abstract class DatabaseTable {
* tables are created on demand and tables and columns are easily ignored when already present.
* However, the change should be noted in upgrade_database() as a comment.
***/
public const int SCHEMA_VERSION = 20;
public const int SCHEMA_VERSION = 21;
protected static Sqlite.Database db;
......
......@@ -349,6 +349,32 @@ private VerifyResult upgrade_database(int input_version) {
//
version = 20;
#if ENABLE_FACES
//
// Version 21:
// * Added face pixels column to FaceLocationTable
// * Added face vector column to FaceTable
//
if (!DatabaseTable.has_column("FaceLocationTable", "vec")) {
message("upgrade_database: adding vec column to FaceLocationTable");
if (!DatabaseTable.add_column("FaceLocationTable", "vec", "TEXT"))
return VerifyResult.UPGRADE_ERROR;
}
if (!DatabaseTable.has_column("FaceLocationTable", "guess")) {
message("upgrade_database: adding guess column to FaceLocationTable");
if (!DatabaseTable.add_column("FaceLocationTable", "guess", "INTEGER DEFAULT 0"))
return VerifyResult.UPGRADE_ERROR;
}
if (!DatabaseTable.has_column("FaceTable", "ref")) {
message("upgrade_database: adding ref column to FaceTable");
if (!DatabaseTable.add_column("FaceTable", "ref", "INTEGER DEFAULT -1"))
return VerifyResult.UPGRADE_ERROR;
}
version = 21;
#endif
//
// Finalize the upgrade process
......
......@@ -29,6 +29,7 @@ public class FaceLocationRow {
public FaceID face_id;
public PhotoID photo_id;
public string geometry;
public string vec;
}
public class FaceLocationTable : DatabaseTable {
......@@ -44,7 +45,9 @@ public class FaceLocationTable : DatabaseTable {
+ "id INTEGER NOT NULL PRIMARY KEY, "
+ "face_id INTEGER NOT NULL, "
+ "photo_id INTEGER NOT NULL, "
+ "geometry TEXT"
+ "geometry TEXT, "
+ "vec TEXT, "
+ "guess INTEGER DEFAULT 0"
+ ")", -1, out stmt);
assert(res == Sqlite.OK);
......@@ -60,10 +63,10 @@ public class FaceLocationTable : DatabaseTable {
return instance;
}
public FaceLocationRow add(FaceID face_id, PhotoID photo_id, string geometry) throws DatabaseError {
public FaceLocationRow add(FaceID face_id, PhotoID photo_id, string geometry, string? vec = null) throws DatabaseError {
Sqlite.Statement stmt;
int res = db.prepare_v2(
"INSERT INTO FaceLocationTable (face_id, photo_id, geometry) VALUES (?, ?, ?)",
"INSERT INTO FaceLocationTable (face_id, photo_id, geometry, vec) VALUES (?, ?, ?, ?)",
-1, out stmt);
assert(res == Sqlite.OK);
......@@ -73,6 +76,9 @@ public class FaceLocationTable : DatabaseTable {
assert(res == Sqlite.OK);
res = stmt.bind_text(3, geometry);
assert(res == Sqlite.OK);
if (vec == null) vec = "";
res = stmt.bind_text(4, vec);
assert(res == Sqlite.OK);
res = stmt.step();
if (res != Sqlite.DONE)
......@@ -83,6 +89,7 @@ public class FaceLocationTable : DatabaseTable {
row.face_id = face_id;
row.photo_id = photo_id;
row.geometry = geometry;
row.vec = vec;
return row;
}
......@@ -90,7 +97,7 @@ public class FaceLocationTable : DatabaseTable {
public Gee.List<FaceLocationRow?> get_all_rows() throws DatabaseError {
Sqlite.Statement stmt;
int res = db.prepare_v2(
"SELECT id, face_id, photo_id, geometry FROM FaceLocationTable",
"SELECT id, face_id, photo_id, geometry, vec FROM FaceLocationTable",
-1, out stmt);
assert(res == Sqlite.OK);
......@@ -109,6 +116,7 @@ public class FaceLocationTable : DatabaseTable {
row.face_id = FaceID(stmt.column_int64(1));
row.photo_id = PhotoID(stmt.column_int64(2));
row.geometry = stmt.column_text(3);
row.vec = stmt.column_text(4);
rows.add(row);
}
......@@ -197,6 +205,66 @@ public class FaceLocationTable : DatabaseTable {
if (res != Sqlite.DONE)
throw_error("FaceLocationTable.update_face_location_serialized_geometry", res);
}
public void update_face_location_face_data(FaceLocation face_location)
throws DatabaseError {
Sqlite.Statement stmt;
int res = db.prepare_v2("UPDATE FaceLocationTable SET geometry=?, vec=? WHERE id=?", -1, out stmt);
assert(res == Sqlite.OK);
FaceLocationData face_data = face_location.get_face_data();
res = stmt.bind_text(1, face_data.geometry);
assert(res == Sqlite.OK);
res = stmt.bind_text(2, face_data.vec);
assert(res == Sqlite.OK);
res = stmt.bind_int64(3, face_location.get_face_location_id().id);
assert(res == Sqlite.OK);
res = stmt.step();
if (res != Sqlite.DONE)
throw_error("FaceLocationTable.update_face_location_serialized_geometry", res);
}
public Gee.List<FaceLocationRow?> get_face_ref_vecs(Gee.List<FaceRow?> face_rows)
throws DatabaseError {
Sqlite.Statement stmt;
string[] where_in = {};
foreach (var r in face_rows) {
if (r != null) where_in += "?";
}
int res = db.prepare_v2(
"SELECT id, face_id, photo_id, geometry, vec FROM FaceLocationTable WHERE photo_id IN (%s)"
.printf(string.joinv(",", where_in)),
-1, out stmt);
assert(res == Sqlite.OK);
int c = 1;
foreach (var r in face_rows) {
if (r != null) {
res = stmt.bind_int64(c, r.ref.id);
assert(res == Sqlite.OK);
}
c++;
}
Gee.List<FaceLocationRow?> rows = new Gee.ArrayList<FaceLocationRow?>();
for (;;) {
res = stmt.step();
if (res == Sqlite.DONE)
break;
else if (res != Sqlite.ROW)
throw_error("FaceLocationTable.get_face_ref_vecs", res);
FaceLocationRow row = new FaceLocationRow();
row.face_location_id = FaceLocationID(stmt.column_int64(0));
row.face_id = FaceID(stmt.column_int64(1));
row.photo_id = PhotoID(stmt.column_int64(2));
row.geometry = stmt.column_text(3);
row.vec = stmt.column_text(4);
rows.add(row);
}
return rows;
}
}
#endif
......@@ -27,6 +27,8 @@ public class FaceRow {
public FaceID face_id;
public string name;
public time_t time_created;
public PhotoID ref;
public string vec;
}
public class FaceTable : DatabaseTable {
......@@ -41,7 +43,8 @@ public class FaceTable : DatabaseTable {
+ "("
+ "id INTEGER NOT NULL PRIMARY KEY, "
+ "name TEXT NOT NULL, "
+ "time_created TIMESTAMP"
+ "time_created TIMESTAMP, "
+ "ref INTEGER DEFAULT -1"
+ ")", -1, out stmt);
assert(res == Sqlite.OK);
......@@ -165,5 +168,48 @@ public class FaceTable : DatabaseTable {
public void rename(FaceID face_id, string new_name) throws DatabaseError {
update_text_by_id_2(face_id.id, "name", new_name);
}
public void set_reference(FaceID face_id, PhotoID photo_id)
throws DatabaseError {
Sqlite.Statement stmt;