updated
This commit is contained in:
parent
b0bb218b2a
commit
7afc2fa94b
|
@ -0,0 +1,54 @@
|
|||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
bin/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
||||
# Mr Developer
|
||||
.mr.developer.cfg
|
||||
.project
|
||||
.pydevproject
|
||||
|
||||
# Rope
|
||||
.ropeproject
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
*.pot
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
This implements a face alignment method, as preprocessing to tasks such as age and gender estimation,
|
||||
and face recognition as described in [1] and [2].
|
||||
|
||||
The code calls an executable of facial landmarks detection, by X.Zhu and D. Ramanan, implementing the algorithm described in [3].
|
||||
|
||||
(The rest of this text is a quotation from their code: Copyright (C) 2012 Xiangxin Zhu, Deva Ramanan)
|
||||
|
||||
It includes pre-trained face models.
|
||||
|
||||
Much of the detection code is built on top of part-based model implementation of [4].
|
||||
|
||||
The training code implements a quadratic program (QP) solver described in [5].
|
||||
|
||||
In the training code, we use the positive samples from MultiPIE dataset (available at www.multipie.org) and the negative images from the INRIAPerson dataset [6] (included in the package).
|
||||
|
||||
Acknowledgements: We graciously thank the authors of the previous code releases and image benchmarks for making them publicly available.
|
||||
|
||||
References
|
||||
==========
|
||||
|
||||
[1] E. Eidinger, R. Enbar, T. Hassner, Age and Gender Estimation of Unfiltered Faces, submitted to IEEE TRANSACTIONS ON INFORMATION FORENSICS AND SECURITY, 2014
|
||||
|
||||
[2] http://www.openu.ac.il/home/hassner/Adience/links.html
|
||||
|
||||
[3] X. Zhu, D. Ramanan. Face Detection, Pose Estimation and Landmark Localization in the Wild. CVPR 2012.
|
||||
|
||||
[4] P. Felzenszwalb, R. Girshick, D. McAllester. Discriminatively Trained Deformable Part Models. http://people.cs.uchicago.edu/~pff/latent.
|
||||
|
||||
[5] D. Ramanan. Dual Coordinate Descent Solvers for Large Structured Prediction Problems. UCI Technical Report, to appear.
|
||||
|
||||
[6] N. Dalal, B. Triggs. Histograms of Oriented Gradients for Human Detection. CVPR 2005.
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
Copyright (C) 2014 Adience SER Ltd. (www.adience.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.
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
adience_align
|
||||
========
|
||||
|
||||
This project provides alignment tools for faces, to be used as a preprocessing step before computer vision tasks on face images.
|
||||
|
||||
Homepage for the project: http://www.openu.ac.il/home/hassner/Adience/
|
||||
|
||||
|
||||
See the test for example usage.
|
||||
|
||||
Specificaly, the "pipeline" test, shows how to use the full process (just remember to change the location of the model files to where you stor the *.xml and other model files)
|
||||
|
||||
Installation
|
||||
=========
|
||||
in the root of the repository:
|
||||
|
||||
```
|
||||
python setup.py sdist
|
||||
sudo pip install dist/adience-<version_number>.tar.gz
|
||||
```
|
||||
|
||||
|
||||
|
||||
CopyRight
|
||||
=========
|
||||
(contact: Eran Eidinger (eran@adience.com), Roee Enbar (roee.e@adience.com))
|
||||
|
||||
See the LICENSE.txt file (basically, an MIT license).
|
||||
|
||||
|
||||
With any publication that uses this alignment code, or it's derivative, we kindly ask that you cite the paper:
|
||||
E. Eidinger, R. Enbar, and T. Hassner, Age and Gender Estimation of Unfiltered Faces, Transactions on Information Forensics and Security (IEEE-TIFS), special issue on Face Recognition in the Wild
|
||||
|
||||
For more details, please see:
|
||||
http://www.openu.ac.il/home/hassner/Adience/publications.html
|
||||
|
||||
Compilation notes
|
||||
========
|
||||
1. The shared objects were compiled for linux 64bit on Ubuntu 13.10
|
||||
2. The SO uses boost-1.53, so make sure it is installed on your system and available at /usr/local/, or use LD_LIBRARY_PATH="yourpath" to point it at the right place. Alternatively, place "libboost_system.so.1.53.0" and "libboost_filesystem.so.1.53.0". at the "adiencealign/resources/" subfolder
|
||||
3. For landmarks detection, we use the file libPartsBasedDetector.so, compiled from the project https://github.com/wg-perception/PartsBasedDetector. You can either compile it yourselves, or use the version under "resources" subfolder, compiled with boost 1.53, on a linux ubuntu 14.04 machine.
|
||||
|
||||
We will release the source code for the shared object in the near future
|
||||
|
||||
Running the test
|
||||
========
|
||||
1. run ```./clear_test.sh``` to delete results of old tests.
|
||||
2. run ```python test_pipeline.py```
|
||||
3. results are in the "outputs" subfolder
|
|
@ -0,0 +1,20 @@
|
|||
'''
|
||||
Created on May 7, 2014
|
||||
|
||||
@author: eran
|
||||
'''
|
||||
from adiencealign.common.landmarks import fidu_transform, shift_vector,\
|
||||
WEIGHTS3
|
||||
|
||||
class AffineAligner(object):
|
||||
def __init__(self, fidu_model_file, ):
|
||||
|
||||
self.shift = ( 0.25, 0.25 )
|
||||
fidu_model = [(int(x.split(',')[1]),int(x.split(',')[2])) for x in file(fidu_model_file,'r')]
|
||||
self.fidu_model = shift_vector(fidu_model, self.shift)
|
||||
self.WEIGHTS3 = WEIGHTS3
|
||||
|
||||
def align(self, img, fidu_points):
|
||||
# create bs1 image
|
||||
funneled_img, R = fidu_transform(self.fidu_model, fidu_points, WEIGHTS3, img, self.shift)
|
||||
return funneled_img, R
|
|
@ -0,0 +1,354 @@
|
|||
'''
|
||||
Created on May 7, 2014
|
||||
|
||||
@author: eran
|
||||
'''
|
||||
import cv2
|
||||
import pickle
|
||||
import numpy as np
|
||||
from shapely.geometry.polygon import Polygon
|
||||
import math
|
||||
from adiencealign.common.files import make_path, expand_path
|
||||
from adiencealign.common.images import pad_image_for_rotation
|
||||
|
||||
class CascadeDetector(object):
|
||||
'''
|
||||
This is a haar cascade classifier capable of detecting in multiple angles
|
||||
'''
|
||||
def __init__(self, cascade_file = './resources/haarcascade_frontalface_default.xml',
|
||||
min_size = (10, 10),
|
||||
min_neighbors = 20,
|
||||
scale_factor = 1.04,
|
||||
angles = [0],
|
||||
thr = 0.4,
|
||||
cascade_type = 'haar'):
|
||||
'''
|
||||
cascade_type - is a string defining the type of cascade
|
||||
'''
|
||||
print(expand_path('.'))
|
||||
self.cascade_file = cascade_file.rsplit('/',1)[1]
|
||||
self._cascade_classifier = cv2.CascadeClassifier(cascade_file)
|
||||
self.scale_factor = scale_factor
|
||||
self.min_neighbors = min_neighbors
|
||||
self.min_size = min_size
|
||||
self.cascade_type = cascade_type
|
||||
self.angles = angles
|
||||
self.thr = thr
|
||||
|
||||
def __str__(self):
|
||||
return ''.join([str(x) for x in ['cascade_file:',self.cascade_file,
|
||||
',scale_factor:',self.scale_factor,
|
||||
',min_neighbors:',self.min_neighbors,
|
||||
',min_neighbors:',self.min_neighbors,
|
||||
',cascade_type:',self.cascade_type
|
||||
]])
|
||||
|
||||
def save_configuration(self, target_file):
|
||||
file_path = target_file.rsplit('/',1)[0]
|
||||
make_path(file_path)
|
||||
config = {'min_size':self.min_size, 'min_neighbours':self.min_neighbors, 'scale_factor':self.scale_factor, 'cascade_file':self.cascade_file}
|
||||
pickle.dump(obj=config, file = open(target_file,'w'), protocol = 2)
|
||||
|
||||
@staticmethod
|
||||
def load_configuration(target_file):
|
||||
return pickle.load(open(target_file,'r'))
|
||||
|
||||
def detectMultiScaleWithScores(self, img, scaleFactor = None, minNeighbors = None, minSize = None, flags = 4):
|
||||
scaleFactor = self.scale_factor if not scaleFactor else scaleFactor
|
||||
minNeighbors = self.min_neighbors if not minNeighbors else minNeighbors
|
||||
minSize = self.min_size if not minSize else minSize
|
||||
return self._cascade_classifier.detectMultiScale(img,
|
||||
scaleFactor = scaleFactor,
|
||||
minNeighbors = minNeighbors,
|
||||
minSize = minSize,
|
||||
flags = flags)
|
||||
|
||||
def detectWithAngles(self, img, angels = None, resolve = True, thr = None ):
|
||||
'''
|
||||
angles - a list of angles to test. If None, default to the value created at the constructor (which defaults to [0])
|
||||
resolve - a boolean flag, whether or not to cluster the boxes, and resolve cluster by highest score.
|
||||
thr - the maximum area covered with objects, before we break from the angles loop
|
||||
|
||||
returns - a list of CascadeResult() objects
|
||||
'''
|
||||
|
||||
if thr == None:
|
||||
thr = self.thr
|
||||
|
||||
original_size = img.shape[0] * img.shape[0]
|
||||
if angels == None:
|
||||
angels = self.angles
|
||||
|
||||
results = []
|
||||
total_area = 0
|
||||
for angle in angels:
|
||||
|
||||
# the diagonal of the image is the diameter of the rotated image, so the big_image needs to bound this circle
|
||||
# by being that big
|
||||
|
||||
big_image, x_shift, y_shift, diag, rot_center = pad_image_for_rotation(img)
|
||||
|
||||
# find the rotation and the inverse rotation matrix, to allow translations between old and new coordinates and vice versa
|
||||
rot_mat = cv2.getRotationMatrix2D(rot_center, angle, scale = 1.0)
|
||||
inv_rot_mat = cv2.invertAffineTransform(rot_mat)
|
||||
|
||||
# rotate the image by the desired angle
|
||||
rot_image = cv2.warpAffine(big_image, rot_mat, (big_image.shape[1],big_image.shape[0]), flags=cv2.INTER_CUBIC)
|
||||
faces = self.detectMultiScaleWithScores(rot_image, scaleFactor = 1.03, minNeighbors = 20, minSize = (15,15), flags = 4)
|
||||
for face in faces:
|
||||
xp = face[0]
|
||||
dx = face[2]
|
||||
yp = face[1]
|
||||
dy = face[3]
|
||||
score = 1
|
||||
dots = np.matrix([[xp,xp+dx,xp+dx,xp], [yp,yp,yp+dy,yp+dy], [1, 1, 1, 1]])
|
||||
# these are the original coordinates in the "big_image"
|
||||
# print dots
|
||||
originals_in_big = inv_rot_mat * dots
|
||||
# print originals_in_big
|
||||
shifter = np.matrix([[x_shift]*4, [y_shift]*4])
|
||||
# print shifter
|
||||
# these are the original coordinate in the original image
|
||||
originals = originals_in_big - shifter
|
||||
# print originals
|
||||
points = np.array(originals.transpose())
|
||||
x = points[0,0]
|
||||
y = points[0,1]
|
||||
box_with_score = ([x,y,dx,dy], score)
|
||||
|
||||
cascade_result = CascadeResult.from_polygon_points(points, score, self.cascade_type)
|
||||
# print cascade_result
|
||||
|
||||
results.append(cascade_result)
|
||||
|
||||
#################
|
||||
# test and see, if we found enough objects, break out and don't waste our time
|
||||
total_area += cascade_result.area
|
||||
|
||||
if resolve:
|
||||
return resolve_angles(results, width = img.shape[1], height = img.shape[0])
|
||||
else:
|
||||
return results
|
||||
|
||||
class BoxInImage(object):
|
||||
def __init__(self, originals, dx, dy, score = None, angle = 0):
|
||||
self.originals = originals
|
||||
self.dx = dx
|
||||
self.dy = dy
|
||||
self.score = score
|
||||
self.angle = angle
|
||||
|
||||
def __str__(self):
|
||||
return ",".join([str(x) for x in [self.originals, self.dx, self.dy, self.score, self.angle]])
|
||||
|
||||
def resolve_angles(list_of_results, width, height, thr = 0.3):
|
||||
'''
|
||||
we want to cluster the boxes into clusters, and then choose the best box in each cluster by score
|
||||
* thr - decides what the maximum distance is for a box to join a cluster, in the sense of how much of it's area is covered by the best box in the cluster
|
||||
note, that two squares, centered, with 45 degrees rotation, will overlap on 77% of their area (thr == 0.22)
|
||||
'''
|
||||
clusters = []
|
||||
for box in list_of_results:
|
||||
# total_polygon = Polygon([(0,0), (width,0), (width,height), (0,height)])
|
||||
# if box.polygon.intersection(total_polygon).area < box.area:
|
||||
# # this means the box is outside the image somehow
|
||||
# continue
|
||||
|
||||
area = box.area
|
||||
closest_cluster = None
|
||||
dist_to_closest_cluster = 1.0
|
||||
for n,cluster in enumerate(clusters):
|
||||
dist = 1.0
|
||||
for cluster_box in cluster:
|
||||
local_dist = 1.0 - box.overlap(cluster_box)/area
|
||||
dist = min(dist, local_dist)
|
||||
if dist < dist_to_closest_cluster:
|
||||
dist_to_closest_cluster = dist
|
||||
closest_cluster = n
|
||||
if closest_cluster == None or dist_to_closest_cluster > thr:
|
||||
# no good cluster was found, open a new cluster
|
||||
clusters.append([box])
|
||||
else:
|
||||
clusters[n].append(box)
|
||||
|
||||
centroids = []
|
||||
for cluster in clusters:
|
||||
centroids.append(sorted(cluster,key=lambda x: x.score)[-1])
|
||||
|
||||
return centroids
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def resolve_boxes(dict_of_list_of_cascade_results, min_overlap = 0.7):
|
||||
'''
|
||||
Say you tried two different cascades to detect faces.
|
||||
enter a dictionary (the key is a string describing a cascade type) of detected objects
|
||||
This function returns a unified results list, where it resolves overlapping boxes, and chooses one of them.
|
||||
|
||||
The bigger boxes are selected instead of smaller ones, whether they contain them, or enough of them, determined by min_overlap
|
||||
|
||||
'''
|
||||
final_faces = []
|
||||
for cascade_str, faces in dict_of_list_of_cascade_results.items():
|
||||
# go through each cascade type
|
||||
for face in faces:
|
||||
if type(face) == CascadeResult:
|
||||
new_res = face
|
||||
else:
|
||||
new_res = CascadeResult(face,cascade_type = cascade_str)
|
||||
to_add = True
|
||||
for old_index,old_res in enumerate(final_faces):
|
||||
ratio = new_res.area / old_res.area
|
||||
if ratio >1.0:
|
||||
# new_box is bigger
|
||||
if new_res.overlap(old_res)/old_res.area > min_overlap:
|
||||
# the new box contains the old one, we want to replace it:
|
||||
final_faces[old_index] = new_res
|
||||
to_add = False
|
||||
break
|
||||
if ratio <=1.0:
|
||||
# the new_box is smaller
|
||||
if new_res.overlap(old_res)/new_res.area > min_overlap:
|
||||
# the old box contains the new one, we therefore dont need to add the new box:
|
||||
to_add = False
|
||||
break
|
||||
if to_add:
|
||||
# if there was no hit, this is a new face, we can add it
|
||||
final_faces.append(new_res)
|
||||
return final_faces
|
||||
|
||||
def most_centered_box( cascade_results, xxx_todo_changeme ):
|
||||
( rows, cols ) = xxx_todo_changeme
|
||||
best_err = 1e10
|
||||
for i, cascade in enumerate( cascade_results ):
|
||||
err = ( cascade.x + cascade.dx / 2 - cols / 2 ) ** 2 + ( cascade.y + cascade.dy / 2 - rows / 2 ) ** 2
|
||||
if err < best_err:
|
||||
index = i
|
||||
return cascade_results[ index ]
|
||||
|
||||
class CascadeResult(object):
|
||||
def __init__(self, box_with_score, cascade_type = None, angle = 0):
|
||||
self.x = box_with_score[0][0]
|
||||
self.y = box_with_score[0][1]
|
||||
self.dx = box_with_score[0][2]
|
||||
self.dy = box_with_score[0][3]
|
||||
self.score = box_with_score[1]
|
||||
self.cascade_type = cascade_type
|
||||
self.angle = angle
|
||||
|
||||
@staticmethod
|
||||
def from_polygon_points(points, score, cascade_type = None):
|
||||
'''
|
||||
an alternative generator, allows giving the polygon points instead of [x,y,dx,dy]
|
||||
'''
|
||||
x = points[0,0]
|
||||
y = points[0,1]
|
||||
top = points[1,] - points[0,]
|
||||
left = points[3,] - points[0,]
|
||||
dx = math.sqrt(sum([i*i for i in top]))
|
||||
dy = math.sqrt(sum([i*i for i in left]))
|
||||
angle = math.atan(float(top[1])/top[0]) * 180 / math.pi if top[0] != 0 else (970 if top[1] >0 else -90)
|
||||
return CascadeResult(([x,y,dx,dy],score), cascade_type, angle)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return ''.join([str(x) for x in ['center:',self.center,
|
||||
',\nx:',self.x,
|
||||
',\ny:',self.y,
|
||||
',\ndx:',self.dx,
|
||||
',\ndy:',self.dy,
|
||||
',\nscore:',self.score,
|
||||
',\nangle:',self.angle,
|
||||
',\ncascade_type:',self.cascade_type,
|
||||
',\npoints_int:\n',self.points_int
|
||||
]])
|
||||
|
||||
@property
|
||||
def points(self):
|
||||
x = self.x
|
||||
y = self.y
|
||||
dx = self.dx
|
||||
dy = self.dy
|
||||
a = self.angle/180.0*math.pi
|
||||
dots = np.matrix([[x,y,1],[x+dx,y,1],[x+dx,y+dy,1],[x,y+dy,1]])
|
||||
dots = dots.transpose()
|
||||
rot_mat = cv2.getRotationMatrix2D((dots[0,0],dots[1,0]), -self.angle, scale = 1.0)
|
||||
points = rot_mat * dots
|
||||
points = points.transpose()
|
||||
return points
|
||||
|
||||
@property
|
||||
def center(self):
|
||||
return tuple(int(x) for x in (self.points.sum(0)/4.0).tolist()[0])
|
||||
|
||||
@property
|
||||
def points_int(self):
|
||||
return self.points.astype(int)
|
||||
|
||||
@property
|
||||
def score_with_type(self):
|
||||
if self.cascade_type:
|
||||
return self.cascade_type + ' ' + str(self.score)
|
||||
else:
|
||||
return str(self.score)
|
||||
|
||||
@property
|
||||
def filename_encode(self):
|
||||
|
||||
return '_'.join([str(x) for x in ['loct'] + self.cvformat_result[0] + ['ang', int(self.angle),self.cascade_type, self.score]])
|
||||
|
||||
@property
|
||||
def cvformat_coords(self):
|
||||
if self.angle == 0:
|
||||
return [int(x) for x in [self.x, self.y, self.dx, self.dy]]
|
||||
else:
|
||||
raise Exception('cannot return [x,y,dx,dy] for a box with angle, use cvformat_result() instead')
|
||||
|
||||
@property
|
||||
def cvformat_result(self):
|
||||
return ([int(x) for x in [self.x, self.y, self.dx, self.dy]], self.score, self.angle)
|
||||
|
||||
# @property
|
||||
# def rot_matrix(self):
|
||||
# return array([[cos(math.radians(self.angle)), -sin(math.radians(self.angle))],
|
||||
# [sin(math.radians(self.angle)), cos(math.radians(self.angle))]])
|
||||
|
||||
@property
|
||||
def top_left(self):
|
||||
return tuple(self.points[0,].tolist()[0])
|
||||
|
||||
@property
|
||||
def top_right(self):
|
||||
return tuple(self.points[1,].tolist()[0])
|
||||
|
||||
@property
|
||||
def bottom_right(self):
|
||||
return tuple(self.points[2,].tolist()[0])
|
||||
|
||||
@property
|
||||
def bottom_left(self):
|
||||
return tuple(self.points[3,].tolist()[0])
|
||||
|
||||
@property
|
||||
def polygon(self):
|
||||
return Polygon([self.top_left, self.top_right, self.bottom_right, self.bottom_left])
|
||||
|
||||
def overlap(self, otherRect):
|
||||
return float(self.polygon.intersection(otherRect.polygon).area)
|
||||
|
||||
@property
|
||||
def area(self):
|
||||
return float(self.polygon.area)
|
||||
|
||||
def __gt__(self,b):
|
||||
return self.area>b.area
|
||||
def __ge__(self,b):
|
||||
return self.area>=b.area
|
||||
def __lt__(self,b):
|
||||
return self.area<b.area
|
||||
def __le__(self,b):
|
||||
return self.area<=b.area
|
||||
|
||||
|
|
@ -0,0 +1,353 @@
|
|||
'''
|
||||
Created on May 7, 2014
|
||||
|
||||
@author: eran
|
||||
'''
|
||||
import cv2
|
||||
import pickle
|
||||
import numpy as np
|
||||
from shapely.geometry.polygon import Polygon
|
||||
import math
|
||||
from adiencealign.common.files import make_path, expand_path
|
||||
from adiencealign.common.images import pad_image_for_rotation
|
||||
|
||||
class CascadeDetector(object):
|
||||
'''
|
||||
This is a haar cascade classifier capable of detecting in multiple angles
|
||||
'''
|
||||
def __init__(self, cascade_file = './resources/haarcascade_frontalface_default.xml',
|
||||
min_size = (10, 10),
|
||||
min_neighbors = 20,
|
||||
scale_factor = 1.04,
|
||||
angles = [0],
|
||||
thr = 0.4,
|
||||
cascade_type = 'haar'):
|
||||
'''
|
||||
cascade_type - is a string defining the type of cascade
|
||||
'''
|
||||
print expand_path('.')
|
||||
self.cascade_file = cascade_file.rsplit('/',1)[1]
|
||||
self._cascade_classifier = cv2.CascadeClassifier(cascade_file)
|
||||
self.scale_factor = scale_factor
|
||||
self.min_neighbors = min_neighbors
|
||||
self.min_size = min_size
|
||||
self.cascade_type = cascade_type
|
||||
self.angles = angles
|
||||
self.thr = thr
|
||||
|
||||
def __str__(self):
|
||||
return ''.join([str(x) for x in ['cascade_file:',self.cascade_file,
|
||||
',scale_factor:',self.scale_factor,
|
||||
',min_neighbors:',self.min_neighbors,
|
||||
',min_neighbors:',self.min_neighbors,
|
||||
',cascade_type:',self.cascade_type
|
||||
]])
|
||||
|
||||
def save_configuration(self, target_file):
|
||||
file_path = target_file.rsplit('/',1)[0]
|
||||
make_path(file_path)
|
||||
config = {'min_size':self.min_size, 'min_neighbours':self.min_neighbors, 'scale_factor':self.scale_factor, 'cascade_file':self.cascade_file}
|
||||
pickle.dump(obj=config, file = open(target_file,'w'), protocol = 2)
|
||||
|
||||
@staticmethod
|
||||
def load_configuration(target_file):
|
||||
return pickle.load(open(target_file,'r'))
|
||||
|
||||
def detectMultiScaleWithScores(self, img, scaleFactor = None, minNeighbors = None, minSize = None, flags = 4):
|
||||
scaleFactor = self.scale_factor if not scaleFactor else scaleFactor
|
||||
minNeighbors = self.min_neighbors if not minNeighbors else minNeighbors
|
||||
minSize = self.min_size if not minSize else minSize
|
||||
return self._cascade_classifier.detectMultiScale(img,
|
||||
scaleFactor = scaleFactor,
|
||||
minNeighbors = minNeighbors,
|
||||
minSize = minSize,
|
||||
flags = flags)
|
||||
|
||||
def detectWithAngles(self, img, angels = None, resolve = True, thr = None ):
|
||||
'''
|
||||
angles - a list of angles to test. If None, default to the value created at the constructor (which defaults to [0])
|
||||
resolve - a boolean flag, whether or not to cluster the boxes, and resolve cluster by highest score.
|
||||
thr - the maximum area covered with objects, before we break from the angles loop
|
||||
|
||||
returns - a list of CascadeResult() objects
|
||||
'''
|
||||
|
||||
if thr == None:
|
||||
thr = self.thr
|
||||
|
||||
original_size = img.shape[0] * img.shape[0]
|
||||
if angels == None:
|
||||
angels = self.angles
|
||||
|
||||
results = []
|
||||
total_area = 0
|
||||
for angle in angels:
|
||||
|
||||
# the diagonal of the image is the diameter of the rotated image, so the big_image needs to bound this circle
|
||||
# by being that big
|
||||
|
||||
big_image, x_shift, y_shift, diag, rot_center = pad_image_for_rotation(img)
|
||||
|
||||
# find the rotation and the inverse rotation matrix, to allow translations between old and new coordinates and vice versa
|
||||
rot_mat = cv2.getRotationMatrix2D(rot_center, angle, scale = 1.0)
|
||||
inv_rot_mat = cv2.invertAffineTransform(rot_mat)
|
||||
|
||||
# rotate the image by the desired angle
|
||||
rot_image = cv2.warpAffine(big_image, rot_mat, (big_image.shape[1],big_image.shape[0]), flags=cv2.INTER_CUBIC)
|
||||
faces = self.detectMultiScaleWithScores(rot_image, scaleFactor = 1.03, minNeighbors = 20, minSize = (15,15), flags = 4)
|
||||
for face in faces:
|
||||
xp = face[0]
|
||||
dx = face[2]
|
||||
yp = face[1]
|
||||
dy = face[3]
|
||||
score = 1
|
||||
dots = np.matrix([[xp,xp+dx,xp+dx,xp], [yp,yp,yp+dy,yp+dy], [1, 1, 1, 1]])
|
||||
# these are the original coordinates in the "big_image"
|
||||
# print dots
|
||||
originals_in_big = inv_rot_mat * dots
|
||||
# print originals_in_big
|
||||
shifter = np.matrix([[x_shift]*4, [y_shift]*4])
|
||||
# print shifter
|
||||
# these are the original coordinate in the original image
|
||||
originals = originals_in_big - shifter
|
||||
# print originals
|
||||
points = np.array(originals.transpose())
|
||||
x = points[0,0]
|
||||
y = points[0,1]
|
||||
box_with_score = ([x,y,dx,dy], score)
|
||||
|
||||
cascade_result = CascadeResult.from_polygon_points(points, score, self.cascade_type)
|
||||
# print cascade_result
|
||||
|
||||
results.append(cascade_result)
|
||||
|
||||
#################
|
||||
# test and see, if we found enough objects, break out and don't waste our time
|
||||
total_area += cascade_result.area
|
||||
|
||||
if resolve:
|
||||
return resolve_angles(results, width = img.shape[1], height = img.shape[0])
|
||||
else:
|
||||
return results
|
||||
|
||||
class BoxInImage(object):
|
||||
def __init__(self, originals, dx, dy, score = None, angle = 0):
|
||||
self.originals = originals
|
||||
self.dx = dx
|
||||
self.dy = dy
|
||||
self.score = score
|
||||
self.angle = angle
|
||||
|
||||
def __str__(self):
|
||||
return ",".join([str(x) for x in [self.originals, self.dx, self.dy, self.score, self.angle]])
|
||||
|
||||
def resolve_angles(list_of_results, width, height, thr = 0.3):
|
||||
'''
|
||||
we want to cluster the boxes into clusters, and then choose the best box in each cluster by score
|
||||
* thr - decides what the maximum distance is for a box to join a cluster, in the sense of how much of it's area is covered by the best box in the cluster
|
||||
note, that two squares, centered, with 45 degrees rotation, will overlap on 77% of their area (thr == 0.22)
|
||||
'''
|
||||
clusters = []
|
||||
for box in list_of_results:
|
||||
# total_polygon = Polygon([(0,0), (width,0), (width,height), (0,height)])
|
||||
# if box.polygon.intersection(total_polygon).area < box.area:
|
||||
# # this means the box is outside the image somehow
|
||||
# continue
|
||||
|
||||
area = box.area
|
||||
closest_cluster = None
|
||||
dist_to_closest_cluster = 1.0
|
||||
for n,cluster in enumerate(clusters):
|
||||
dist = 1.0
|
||||
for cluster_box in cluster:
|
||||
local_dist = 1.0 - box.overlap(cluster_box)/area
|
||||
dist = min(dist, local_dist)
|
||||
if dist < dist_to_closest_cluster:
|
||||
dist_to_closest_cluster = dist
|
||||
closest_cluster = n
|
||||
if closest_cluster == None or dist_to_closest_cluster > thr:
|
||||
# no good cluster was found, open a new cluster
|
||||
clusters.append([box])
|
||||
else:
|
||||
clusters[n].append(box)
|
||||
|
||||
centroids = []
|
||||
for cluster in clusters:
|
||||
centroids.append(sorted(cluster,key=lambda x: x.score)[-1])
|
||||
|
||||
return centroids
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def resolve_boxes(dict_of_list_of_cascade_results, min_overlap = 0.7):
|
||||
'''
|
||||
Say you tried two different cascades to detect faces.
|
||||
enter a dictionary (the key is a string describing a cascade type) of detected objects
|
||||
This function returns a unified results list, where it resolves overlapping boxes, and chooses one of them.
|
||||
|
||||
The bigger boxes are selected instead of smaller ones, whether they contain them, or enough of them, determined by min_overlap
|
||||
|
||||
'''
|
||||
final_faces = []
|
||||
for cascade_str, faces in dict_of_list_of_cascade_results.iteritems():
|
||||
# go through each cascade type
|
||||
for face in faces:
|
||||
if type(face) == CascadeResult:
|
||||
new_res = face
|
||||
else:
|
||||
new_res = CascadeResult(face,cascade_type = cascade_str)
|
||||
to_add = True
|
||||
for old_index,old_res in enumerate(final_faces):
|
||||
ratio = new_res.area / old_res.area
|
||||
if ratio >1.0:
|
||||
# new_box is bigger
|
||||
if new_res.overlap(old_res)/old_res.area > min_overlap:
|
||||
# the new box contains the old one, we want to replace it:
|
||||
final_faces[old_index] = new_res
|
||||
to_add = False
|
||||
break
|
||||
if ratio <=1.0:
|
||||
# the new_box is smaller
|
||||
if new_res.overlap(old_res)/new_res.area > min_overlap:
|
||||
# the old box contains the new one, we therefore dont need to add the new box:
|
||||
to_add = False
|
||||
break
|
||||
if to_add:
|
||||
# if there was no hit, this is a new face, we can add it
|
||||
final_faces.append(new_res)
|
||||
return final_faces
|
||||
|
||||
def most_centered_box( cascade_results, ( rows, cols ) ):
|
||||
best_err = 1e10
|
||||
for i, cascade in enumerate( cascade_results ):
|
||||
err = ( cascade.x + cascade.dx / 2 - cols / 2 ) ** 2 + ( cascade.y + cascade.dy / 2 - rows / 2 ) ** 2
|
||||
if err < best_err:
|
||||
index = i
|
||||
return cascade_results[ index ]
|
||||
|
||||
class CascadeResult(object):
|
||||
def __init__(self, box_with_score, cascade_type = None, angle = 0):
|
||||
self.x = box_with_score[0][0]
|
||||
self.y = box_with_score[0][1]
|
||||
self.dx = box_with_score[0][2]
|
||||
self.dy = box_with_score[0][3]
|
||||
self.score = box_with_score[1]
|
||||
self.cascade_type = cascade_type
|
||||
self.angle = angle
|
||||
|
||||
@staticmethod
|
||||
def from_polygon_points(points, score, cascade_type = None):
|
||||
'''
|
||||
an alternative generator, allows giving the polygon points instead of [x,y,dx,dy]
|
||||
'''
|
||||
x = points[0,0]
|
||||
y = points[0,1]
|
||||
top = points[1,] - points[0,]
|
||||
left = points[3,] - points[0,]
|
||||
dx = math.sqrt(sum([i*i for i in top]))
|
||||
dy = math.sqrt(sum([i*i for i in left]))
|
||||
angle = math.atan(float(top[1])/top[0]) * 180 / math.pi if top[0] != 0 else (970 if top[1] >0 else -90)
|
||||
return CascadeResult(([x,y,dx,dy],score), cascade_type, angle)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return ''.join([str(x) for x in ['center:',self.center,
|
||||
',\nx:',self.x,
|
||||
',\ny:',self.y,
|
||||
',\ndx:',self.dx,
|
||||
',\ndy:',self.dy,
|
||||
',\nscore:',self.score,
|
||||
',\nangle:',self.angle,
|
||||
',\ncascade_type:',self.cascade_type,
|
||||
',\npoints_int:\n',self.points_int
|
||||
]])
|
||||
|
||||
@property
|
||||
def points(self):
|
||||
x = self.x
|
||||
y = self.y
|
||||
dx = self.dx
|
||||
dy = self.dy
|
||||
a = self.angle/180.0*math.pi
|
||||
dots = np.matrix([[x,y,1],[x+dx,y,1],[x+dx,y+dy,1],[x,y+dy,1]])
|
||||
dots = dots.transpose()
|
||||
rot_mat = cv2.getRotationMatrix2D((dots[0,0],dots[1,0]), -self.angle, scale = 1.0)
|
||||
points = rot_mat * dots
|
||||
points = points.transpose()
|
||||
return points
|
||||
|
||||
@property
|
||||
def center(self):
|
||||
return tuple(int(x) for x in (self.points.sum(0)/4.0).tolist()[0])
|
||||
|
||||
@property
|
||||
def points_int(self):
|
||||
return self.points.astype(int)
|
||||
|
||||
@property
|
||||
def score_with_type(self):
|
||||
if self.cascade_type:
|
||||
return self.cascade_type + ' ' + str(self.score)
|
||||
else:
|
||||
return str(self.score)
|
||||
|
||||
@property
|
||||
def filename_encode(self):
|
||||
|
||||
return '_'.join([str(x) for x in ['loct'] + self.cvformat_result[0] + ['ang', int(self.angle),self.cascade_type, self.score]])
|
||||
|
||||
@property
|
||||
def cvformat_coords(self):
|
||||
if self.angle == 0:
|
||||
return [int(x) for x in [self.x, self.y, self.dx, self.dy]]
|
||||
else:
|
||||
raise Exception('cannot return [x,y,dx,dy] for a box with angle, use cvformat_result() instead')
|
||||
|
||||
@property
|
||||
def cvformat_result(self):
|
||||
return ([int(x) for x in [self.x, self.y, self.dx, self.dy]], self.score, self.angle)
|
||||
|
||||
# @property
|
||||
# def rot_matrix(self):
|
||||
# return array([[cos(math.radians(self.angle)), -sin(math.radians(self.angle))],
|
||||
# [sin(math.radians(self.angle)), cos(math.radians(self.angle))]])
|
||||
|
||||
@property
|
||||
def top_left(self):
|
||||
return tuple(self.points[0,].tolist()[0])
|
||||
|
||||
@property
|
||||
def top_right(self):
|
||||
return tuple(self.points[1,].tolist()[0])
|
||||
|
||||
@property
|
||||
def bottom_right(self):
|
||||
return tuple(self.points[2,].tolist()[0])
|
||||
|
||||
@property
|
||||
def bottom_left(self):
|
||||
return tuple(self.points[3,].tolist()[0])
|
||||
|
||||
@property
|
||||
def polygon(self):
|
||||
return Polygon([self.top_left, self.top_right, self.bottom_right, self.bottom_left])
|
||||
|
||||
def overlap(self, otherRect):
|
||||
return float(self.polygon.intersection(otherRect.polygon).area)
|
||||
|
||||
@property
|
||||
def area(self):
|
||||
return float(self.polygon.area)
|
||||
|
||||
def __gt__(self,b):
|
||||
return self.area>b.area
|
||||
def __ge__(self,b):
|
||||
return self.area>=b.area
|
||||
def __lt__(self,b):
|
||||
return self.area<b.area
|
||||
def __le__(self,b):
|
||||
return self.area<=b.area
|
||||
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
'''
|
||||
Created on May 7, 2014
|
||||
|
||||
@author: eran
|
||||
'''
|
||||
from adiencealign.common.images import extract_box
|
||||
import glob
|
||||
import os
|
||||
import time
|
||||
from adiencealign.cascade_detection.cascade_detector import CascadeDetector,\
|
||||
resolve_boxes, CascadeResult
|
||||
import cv2
|
||||
import csv
|
||||
'''
|
||||
Created on Dec 18, 2013
|
||||
|
||||
@author: eran
|
||||
'''
|
||||
'''
|
||||
Created on Nov 26, 2013
|
||||
|
||||
@author: eran
|
||||
'''
|
||||
|
||||
class CascadeFaceFinder(object):
|
||||
|
||||
def __init__(self,
|
||||
min_size = 32,
|
||||
drawn_target_res = 360*360,
|
||||
hangles = [0, -22, 22],
|
||||
langles = [0,-45,-22,22,45],
|
||||
haar_file = 'haarcascade_frontalface_default.xml',
|
||||
lbp_file = 'lbpcascade_frontalface.xml'):
|
||||
'''
|
||||
finder = CascadeFaceFinder(min_size = 32, drawn_target_res = 360*360, hangles = [0], langles = [0,-45,-22,22,45], parts_threshold = 0)
|
||||
|
||||
finder.get_faces_in_folder(input_folder, output_dir, drawn_folder, is_small_drawn)
|
||||
|
||||
or
|
||||
|
||||
finder.get_faces_in_photo(full_file, output_dir, drawn_folder, is_small_drawn)
|
||||
'''
|
||||
self.min_size = (min_size,min_size)
|
||||
self.drawn_target_res = drawn_target_res
|
||||
self._hangles = hangles
|
||||
self._langles = langles
|
||||
self.recalc_detectors(haar_file, lbp_file)
|
||||
|
||||
# self.funnel = FaceFunnel()
|
||||
|
||||
@property
|
||||
def hangles(self):
|
||||
return self._hangles
|
||||
|
||||
@hangles.setter
|
||||
def hangles(self,hangles):
|
||||
self._hangles = hangles
|
||||
self.recalc_detectors()
|
||||
|
||||
@property
|
||||
def langles(self):
|
||||
return self._langles
|
||||
|
||||
@langles.setter
|
||||
def langles(self,langles):
|
||||
self._langles = langles
|
||||
self.recalc_detectors()
|
||||
|
||||
def recalc_detectors(self, haar_file, lbp_file):
|
||||
self.haar_dtct = CascadeDetector(cascade_file = haar_file,
|
||||
min_size = self.min_size,
|
||||
min_neighbors = 20,
|
||||
scale_factor = 1.03,
|
||||
cascade_type = 'haar',
|
||||
thr = 0.4,
|
||||
angles = self.hangles)
|
||||
|
||||
self.lbp_dtct = CascadeDetector(cascade_file = lbp_file,
|
||||
min_size = self.min_size,
|
||||
min_neighbors = 15,
|
||||
scale_factor = 1.04,
|
||||
cascade_type = 'lbp',
|
||||
thr = 0.4,
|
||||
angles = self.langles)
|
||||
|
||||
|
||||
def get_faces_list_in_photo(self, img):
|
||||
if self.hangles:
|
||||
haar_faces = self.haar_dtct.detectWithAngles(img, resolve = True)
|
||||
else:
|
||||
haar_faces = []
|
||||
lbp_faces = self.lbp_dtct.detectWithAngles(img, resolve = True)
|
||||
faces = resolve_boxes({'haar':haar_faces, 'lbp':lbp_faces}, min_overlap = 0.6)
|
||||
|
||||
return faces
|
||||
|
||||
def create_faces_file(self, fname, is_overwrite = False, target_file = None):
|
||||
'''
|
||||
Runs facial detection on fname (say a.jpg, or a.png), and creates a results file (a.faces.txt)
|
||||
|
||||
target_file - override, and specify a specific target file
|
||||
is_overwrite - allow overwriting an existing results file
|
||||
'''
|
||||
faces = self.get_faces_list_in_photo(cv2.imread(fname))
|
||||
results_file = fname.rsplit('.',1)[0] + '.faces.txt' if target_file is None else target_file
|
||||
|
||||
if os.path.exists(results_file) and not is_overwrite:
|
||||
print("Warning, faces result file", results_file, "exists")
|
||||
else:
|
||||
with open(results_file,'w') as csvfile:
|
||||
csv_writer = csv.writer(csvfile, delimiter=',')
|
||||
header = ['x', 'y','dx','dy', 'score', 'angle', 'type']
|
||||
csv_writer.writerow(header)
|
||||
for face in faces:
|
||||
csv_writer.writerow([str(i) for i in [int(face.x), int(face.y), int(face.dx), int(face.dy), face.score, face.angle, face.cascade_type]])
|
||||
return results_file
|
||||
|
||||
def get_sub_images_from_file(self,original_image_file, faces_file):
|
||||
'''
|
||||
extracts all the face sub-images from an image file, based on the results in a faces file
|
||||
|
||||
returns - the list of face images (numpy arrays)
|
||||
'''
|
||||
img = cv2.imread(original_image_file)
|
||||
faces_reader = csv.reader(open(faces_file))
|
||||
next(faces_reader) # discard the headings
|
||||
padded_face_images = []
|
||||
for line in faces_reader:
|
||||
x, y, dx, dy, score, angle, cascade_type = line
|
||||
[x,y,dx,dy,score, angle] = [int(float(i)) for i in [x,y,dx,dy,score, angle]]
|
||||
face = CascadeResult(([x,y,dx,dy], score), cascade_type, angle)
|
||||
padded_face, bounding_box_in_padded_face, _, _ = extract_box(img, face, padding_factor = 0.25)
|
||||
padded_face_images.append(padded_face)
|
||||
return padded_face_images
|
||||
|
||||
def create_sub_images_from_file(self, original_image_file, faces_file, target_folder = None, img_type = 'png'):
|
||||
'''
|
||||
reads a faces file, created by "self.create_faces_file" and extracts padded faces from the original image
|
||||
The faces will be created in the same folder as the faces file, unless specified otherwise by "target_folder"
|
||||
|
||||
returns - the list of face files (strings)
|
||||
'''
|
||||
target_folder = os.path.split(faces_file)[0] if target_folder is None else target_folder
|
||||
padded_face_images = self.get_sub_images_from_file(original_image_file, faces_file)
|
||||
|
||||
base_image_name = os.path.split(faces_file)[1].split('.')[0]
|
||||
face_files = []
|
||||
for n_face, face_img in enumerate(padded_face_images):
|
||||
face_file = os.path.join(target_folder, base_image_name + '_face_%d.%s' %(n_face, img_type))
|
||||
cv2.imwrite( face_file , face_img )
|
||||
face_files.append(face_file)
|
||||
return face_files
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
'''
|
||||
Created on May 7, 2014
|
||||
|
||||
@author: eran
|
||||
'''
|
||||
from adiencealign.common.images import extract_box
|
||||
import glob
|
||||
import os
|
||||
import time
|
||||
from adiencealign.cascade_detection.cascade_detector import CascadeDetector,\
|
||||
resolve_boxes, CascadeResult
|
||||
import cv2
|
||||
import csv
|
||||
'''
|
||||
Created on Dec 18, 2013
|
||||
|
||||
@author: eran
|
||||
'''
|
||||
'''
|
||||
Created on Nov 26, 2013
|
||||
|
||||
@author: eran
|
||||
'''
|
||||
|
||||
class CascadeFaceFinder(object):
|
||||
|
||||
def __init__(self,
|
||||
min_size = 32,
|
||||
drawn_target_res = 360*360,
|
||||
hangles = [0, -22, 22],
|
||||
langles = [0,-45,-22,22,45],
|
||||
haar_file = 'haarcascade_frontalface_default.xml',
|
||||
lbp_file = 'lbpcascade_frontalface.xml'):
|
||||
'''
|
||||
finder = CascadeFaceFinder(min_size = 32, drawn_target_res = 360*360, hangles = [0], langles = [0,-45,-22,22,45], parts_threshold = 0)
|
||||
|
||||
finder.get_faces_in_folder(input_folder, output_dir, drawn_folder, is_small_drawn)
|
||||
|
||||
or
|
||||
|
||||
finder.get_faces_in_photo(full_file, output_dir, drawn_folder, is_small_drawn)
|
||||
'''
|
||||
self.min_size = (min_size,min_size)
|
||||
self.drawn_target_res = drawn_target_res
|
||||
self._hangles = hangles
|
||||
self._langles = langles
|
||||
self.recalc_detectors(haar_file, lbp_file)
|
||||
|
||||
# self.funnel = FaceFunnel()
|
||||
|
||||
@property
|
||||
def hangles(self):
|
||||
return self._hangles
|
||||
|
||||
@hangles.setter
|
||||
def hangles(self,hangles):
|
||||
self._hangles = hangles
|
||||
self.recalc_detectors()
|
||||
|
||||
@property
|
||||
def langles(self):
|
||||
return self._langles
|
||||
|
||||
@langles.setter
|
||||
def langles(self,langles):
|
||||
self._langles = langles
|
||||
self.recalc_detectors()
|
||||
|
||||
def recalc_detectors(self, haar_file, lbp_file):
|
||||
self.haar_dtct = CascadeDetector(cascade_file = haar_file,
|
||||
min_size = self.min_size,
|
||||
min_neighbors = 20,
|
||||
scale_factor = 1.03,
|
||||
cascade_type = 'haar',
|
||||
thr = 0.4,
|
||||
angles = self.hangles)
|
||||
|
||||
self.lbp_dtct = CascadeDetector(cascade_file = lbp_file,
|
||||
min_size = self.min_size,
|
||||
min_neighbors = 15,
|
||||
scale_factor = 1.04,
|
||||
cascade_type = 'lbp',
|
||||
thr = 0.4,
|
||||
angles = self.langles)
|
||||
|
||||
|
||||
def get_faces_list_in_photo(self, img):
|
||||
if self.hangles:
|
||||
haar_faces = self.haar_dtct.detectWithAngles(img, resolve = True)
|
||||
else:
|
||||
haar_faces = []
|
||||
lbp_faces = self.lbp_dtct.detectWithAngles(img, resolve = True)
|
||||
faces = resolve_boxes({'haar':haar_faces, 'lbp':lbp_faces}, min_overlap = 0.6)
|
||||
|
||||
return faces
|
||||
|
||||
def create_faces_file(self, fname, is_overwrite = False, target_file = None):
|
||||
'''
|
||||
Runs facial detection on fname (say a.jpg, or a.png), and creates a results file (a.faces.txt)
|
||||
|
||||
target_file - override, and specify a specific target file
|
||||
is_overwrite - allow overwriting an existing results file
|
||||
'''
|
||||
faces = self.get_faces_list_in_photo(cv2.imread(fname))
|
||||
results_file = fname.rsplit('.',1)[0] + '.faces.txt' if target_file is None else target_file
|
||||
|
||||
if os.path.exists(results_file) and not is_overwrite:
|
||||
print "Warning, faces result file", results_file, "exists"
|
||||
else:
|
||||
with open(results_file,'w') as csvfile:
|
||||
csv_writer = csv.writer(csvfile, delimiter=',')
|
||||
header = ['x', 'y','dx','dy', 'score', 'angle', 'type']
|
||||
csv_writer.writerow(header)
|
||||
for face in faces:
|
||||
csv_writer.writerow([str(i) for i in [int(face.x), int(face.y), int(face.dx), int(face.dy), face.score, face.angle, face.cascade_type]])
|
||||
return results_file
|
||||
|
||||
def get_sub_images_from_file(self,original_image_file, faces_file):
|
||||
'''
|
||||
extracts all the face sub-images from an image file, based on the results in a faces file
|
||||
|
||||
returns - the list of face images (numpy arrays)
|
||||
'''
|
||||
img = cv2.imread(original_image_file)
|
||||
faces_reader = csv.reader(open(faces_file))
|
||||
faces_reader.next() # discard the headings
|
||||
padded_face_images = []
|
||||
for line in faces_reader:
|
||||
x, y, dx, dy, score, angle, cascade_type = line
|
||||
[x,y,dx,dy,score, angle] = [int(float(i)) for i in [x,y,dx,dy,score, angle]]
|
||||
face = CascadeResult(([x,y,dx,dy], score), cascade_type, angle)
|
||||
padded_face, bounding_box_in_padded_face, _, _ = extract_box(img, face, padding_factor = 0.25)
|
||||
padded_face_images.append(padded_face)
|
||||
return padded_face_images
|
||||
|
||||
def create_sub_images_from_file(self, original_image_file, faces_file, target_folder = None, img_type = 'png'):
|
||||
'''
|
||||
reads a faces file, created by "self.create_faces_file" and extracts padded faces from the original image
|
||||
The faces will be created in the same folder as the faces file, unless specified otherwise by "target_folder"
|
||||
|
||||
returns - the list of face files (strings)
|
||||
'''
|
||||
target_folder = os.path.split(faces_file)[0] if target_folder is None else target_folder
|
||||
padded_face_images = self.get_sub_images_from_file(original_image_file, faces_file)
|
||||
|
||||
base_image_name = os.path.split(faces_file)[1].split('.')[0]
|
||||
face_files = []
|
||||
for n_face, face_img in enumerate(padded_face_images):
|
||||
face_file = os.path.join(target_folder, base_image_name + '_face_%d.%s' %(n_face, img_type))
|
||||
cv2.imwrite( face_file , face_img )
|
||||
face_files.append(face_file)
|
||||
return face_files
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
'''
|
||||
Created on May 7, 2014
|
||||
|
||||
@author: eran
|
||||
'''
|
||||
import cv2
|
||||
from adiencealign.cascade_detection.cascade_detector import CascadeResult
|
||||
import numpy as np
|
||||
|
||||
def draw_rect(img, r, angle = 0, color=(255,255,255), thickness = 4, alpha = 0.5):
|
||||
'''
|
||||
accepts:
|
||||
1. a (x,y,dx,dy) list
|
||||
4. a [(x,y,dx,dy),score] list, as returned by cv2.CascadeClassifier.detectMultiScaleWithScores()
|
||||
5. a CascadeResult object
|
||||
'''
|
||||
if type(r) == CascadeResult:
|
||||
color = tuple(list(color) + [alpha])
|
||||
cv2.polylines(img, pts = [r.points_int], isClosed = True, color = color, thickness = thickness)
|
||||
return
|
||||
elif len(r)==4 or len(r)==2: # [x,y,dx,dy]
|
||||
if len(r)==2:
|
||||
if len(r[0]) == 4:
|
||||
r = r[0]
|
||||
else:
|
||||
raise Exception("bad input to draw_rect...")
|
||||
pt1 = int(round(r[0])), int(round(r[1]))
|
||||
pt2 = int(round(r[0]+r[2])), int(round(r[1]+r[3]))
|
||||
color = tuple(list(color) + [alpha])
|
||||
cv2.rectangle(img, pt1, pt2, color, thickness = thickness)
|
||||
else:
|
||||
raise Exception("bad input to draw_rect...")
|
||||
return
|
|
@ -0,0 +1,22 @@
|
|||
'''
|
||||
Created on May 7, 2014
|
||||
|
||||
@author: eran
|
||||
'''
|
||||
import os
|
||||
import shutil
|
||||
from os.path import expanduser
|
||||
|
||||
def make_path(path, delete_content_if_exists = False):
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
elif delete_content_if_exists:
|
||||
shutil.rmtree(path)
|
||||
|
||||
def expand_path(file_or_path):
|
||||
if file_or_path.startswith('~/'):
|
||||
home = expanduser("~")
|
||||
file_at = os.path.join(home,file_or_path[2:])
|
||||
return file_at
|
||||
else:
|
||||
return file_or_path
|
|
@ -0,0 +1,87 @@
|
|||
'''
|
||||
Created on May 7, 2014
|
||||
|
||||
@author: eran
|
||||
'''
|
||||
import math
|
||||
import numpy as np
|
||||
import cv2
|
||||
|
||||
|
||||
|
||||
def pad_image_for_rotation(img):
|
||||
# we pad the image so, when we rotate it, it would never be clipped
|
||||
rot_y,rot_x = img.shape[:2]
|
||||
rot_x = int(rot_x / 2.0)
|
||||
rot_y = int(rot_y / 2.0)
|
||||
diag = int(math.sqrt(sum([math.pow(x,2) for x in img.shape])))
|
||||
diag = int(math.ceil(diag / 2.0) * 2.0) # make sure it is even
|
||||
if len(img.shape) == 3:
|
||||
big_image = np.zeros((diag, diag, 3), dtype=np.uint8)
|
||||
else:
|
||||
big_image = np.zeros((diag, diag), dtype=np.uint8)
|
||||
x_shift = int(diag/2-rot_x)
|
||||
y_shift = int(diag/2-rot_y)
|
||||
# the shift of the old image within big_image
|
||||
if len(img.shape) == 3:
|
||||
big_image[y_shift :y_shift+img.shape[0], x_shift:x_shift+img.shape[1], :] = img
|
||||
else:
|
||||
big_image[y_shift :y_shift+img.shape[0], x_shift:x_shift+img.shape[1]] = img
|
||||
|
||||
# the rotation center is no the radius (half the old image diagonal)
|
||||
rot_center = diag/2, diag/2
|
||||
return big_image, x_shift, y_shift, diag, rot_center
|
||||
|
||||
|
||||
def extract_rect(img, rect, factor = 0.2):
|
||||
(x,y,dx,dy) = rect
|
||||
new_x = max(0, int(x-dx*factor))
|
||||
new_y = max(0, int(y-dy*factor))
|
||||
new_dx = min(int(dx+2*factor*dx), img.shape[1] - new_x)
|
||||
new_dy = min(int(dy+2*factor*dy), img.shape[0] - new_y)
|
||||
Dx = x - new_x
|
||||
Dy = y - new_y
|
||||
#return [new_x, new_y, new_dx, new_dy]
|
||||
return img[new_y:new_y+new_dy,new_x:new_x+new_dx,:], Dx, Dy
|
||||
|
||||
|
||||
def extract_box(img, box, padding_factor = 0.2):
|
||||
'''
|
||||
we can search for whatever we want in the rotated bordered image,
|
||||
|
||||
Any point found can be translated back to the original image by:
|
||||
1. adding the origins of the bordered area,
|
||||
2. rotating the point using the inverse rotation matrix
|
||||
|
||||
'''
|
||||
|
||||
if box.angle != 0:
|
||||
|
||||
b_w = max(img.shape)*2
|
||||
b_h = b_w
|
||||
dx_center = b_w / 2 - box.center[0]
|
||||
dy_center = b_h / 2 - box.center[1]
|
||||
new_img = np.zeros((b_w, b_h, 3), dtype = img.dtype)
|
||||
new_img[dy_center:(dy_center + img.shape[0]), dx_center:(dx_center + img.shape[1]), :] = img
|
||||
|
||||
box_in_big_image = box.points + np.c_[np.ones((4,1)) * dx_center, np.ones((4,1)) * dy_center]
|
||||
|
||||
rot_mat = cv2.getRotationMatrix2D((b_w/2, b_h/2), box.angle, scale = 1.0)
|
||||
inv_rot_mat = cv2.invertAffineTransform(rot_mat)
|
||||
rot_image = cv2.warpAffine(new_img, rot_mat, (new_img.shape[1],new_img.shape[0]), flags=cv2.INTER_CUBIC)
|
||||
box_UL_in_rotated = (rot_mat * np.matrix([box_in_big_image[0,0], box_in_big_image[0,1], 1]).transpose()).transpose().tolist()[0]
|
||||
box_coords_in_rotated = np.matrix(np.c_[box_in_big_image, np.ones((4,1))]) * rot_mat.T
|
||||
box_coords_in_rotated = box_coords_in_rotated[0,:].tolist()[0] + [box.dx, box.dy]
|
||||
else:
|
||||
rot_mat = cv2.getRotationMatrix2D(box.center, box.angle, scale = 1.0)
|
||||
inv_rot_mat = cv2.invertAffineTransform(rot_mat)
|
||||
# for efficiency
|
||||
rot_image = img.copy()
|
||||
box_UL_in_rotated = (rot_mat * np.matrix([box.points[0,0], box.points[0,1], 1]).transpose()).transpose().tolist()[0]
|
||||
box_coords_in_rotated = box_UL_in_rotated + [box.dx, box.dy]
|
||||
|
||||
img_with_border, Dx, Dy = extract_rect(rot_image, box_coords_in_rotated, padding_factor)
|
||||
box_coords_in_bordered = [Dx, Dy] + [box.dx, box.dy]
|
||||
border_UL_in_rotated = [box_UL_in_rotated[0]-Dx, box_UL_in_rotated[1]-Dy]
|
||||
|
||||
return img_with_border, box_coords_in_bordered, border_UL_in_rotated, inv_rot_mat
|
|
@ -0,0 +1,113 @@
|
|||
'''
|
||||
Created on May 7, 2014
|
||||
|
||||
@author: eran
|
||||
'''
|
||||
|
||||
import csv
|
||||
import cv2
|
||||
from numpy import linalg
|
||||
import numpy as np
|
||||
|
||||
|
||||
WEIGHTS3 = [11.1088851746,15.8721645013,12.3189439894,15.9467104922,13.9265119716,17.2447706133,11.4118267639,17.0728365324,12.7831886739,17.1908773151,9.6639887492,13.8443342456,8.76890470223,11.4441704453,7.52083144762,10.3245662427,6.35563072919,7.55739887985,6.42340544936,7.48786881875,10.8720924456,8.1349958353,12.3664410374,9.58137800608,6.29390307208,9.47697088783,8.49859202931,9.43946799727,7.92920023102,10.6126442536,10.2953809171,11.299323189,11.1181958685,12.9374719654,12.3764338392,14.7823225327,13.086272904,16.0571795811,15.079169884,17.5936174739,8.39112414861,7.68532826996,8.89386612449,8.70173923211,10.0826620269,8.70286074207,8.13121344224,9.80805203263,7.76044090777,9.2502084627,7.61334683331,10.4813589698,8.64831020289,11.0452512508,9.19528177019,13.0171747152,10.1204323102,14.0189765809,11.0232436734,14.7355286373,12.4881579947,15.4279914333,11.5785971474,16.7942051778 |