├── .gitignore ├── Face3D.py ├── LICENSE ├── README.md └── face3d ├── __init__.py ├── absfile.py ├── algorithms.py ├── database.py ├── face.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /Face3D.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2012 Bas Stottelaar, Jeroen Senden 3 | See the file LICENSE for copying permission. 4 | """ 5 | 6 | from face3d import AbsFile, Face, Database 7 | from face3d import utils, algorithms 8 | 9 | import argparse 10 | import glob 11 | import os 12 | import sys 13 | import numpy 14 | import threading 15 | 16 | parameters = {"N": 67, "K": 12} 17 | parameter_types = {"N": int, "K": int} 18 | database = None 19 | debug = False 20 | 21 | def main(): 22 | global database, parameters 23 | success = False 24 | 25 | # Parse arguments 26 | arguments, parser = argument_parser() 27 | 28 | # Parse parameters 29 | try: 30 | for parameter in arguments.parameters.split(','): 31 | key, value = parameter.split('=', 1) 32 | parameters[key] = value 33 | except: 34 | print "Invalid parameters: %s" % arguments.parameters 35 | sys.exit(1) 36 | 37 | # Make sure parameters are of right type 38 | for key, value in parameters.iteritems(): 39 | try: 40 | parameters[key] = parameter_types[key](value) 41 | except: 42 | print "Parameter '%s' of incorrect type" % key 43 | sys.exit(1) 44 | 45 | # Initialize a database 46 | database = Database(arguments.database) 47 | 48 | # Enrollment 49 | if arguments.enroll: 50 | success = True 51 | is_directory, path = arguments.enroll 52 | 53 | if is_directory: 54 | files = glob.glob("%s/*.abs" % path) 55 | thread_count = 16 56 | chunks = [ files[i::thread_count] for i in range(thread_count) ] 57 | threads = [] 58 | 59 | # Process each thread 60 | for chunk in chunks: 61 | thread = threading.Thread(target=lambda x: [ enroll_face(c, arguments.person_id, arguments.auto_id) for c in x ], args=(chunk, )) 62 | thread.daemon = True 63 | threads.append(thread) 64 | thread.start() 65 | 66 | # Wait for the threads to finish 67 | for thread in threads: 68 | thread.join() 69 | 70 | else: 71 | enroll_face(path, arguments.person_id, arguments.auto_id) 72 | 73 | # Caches for 74 | faces = False 75 | matrix = False 76 | rocs = False 77 | 78 | # Authenticating 79 | if arguments.authenticate: 80 | print "Authenticating face from '%s'" % arguments.authenticate 81 | success = True 82 | 83 | # Create face from file 84 | face = Face(AbsFile(arguments.authenticate)) 85 | 86 | # Normalize it 87 | algorithms.process(face, parameters["N"], parameters["K"]) 88 | 89 | # Get the other data 90 | if not faces: faces = list(database.iterator()) 91 | matrix = algorithms.similarity_matrix([face] + [ face[2] for face in faces ], limit=1) # One line matrix 92 | 93 | # Evaluate result 94 | methods, _, _ = matrix.shape 95 | tresholds = [0.90, 0.90, 0.90] 96 | 97 | for i in range(methods): 98 | # Select indexes of candidates 99 | vector = numpy.array(matrix[i][0][1:]) 100 | candidates, = numpy.where(vector >= tresholds[i]) 101 | persons = {} 102 | 103 | # Verify candidates 104 | if len(candidates) == 0: 105 | print "Method %d does not yield any candidates!" % i 106 | continue 107 | 108 | # Print method 109 | print "Results for method %d:" % i 110 | 111 | # Print each candidate 112 | for candidate in candidates: 113 | if candidate == 0: 114 | continue 115 | 116 | filename, person_id, data = faces[candidate] 117 | 118 | # Add person to list of persons 119 | if person_id not in persons: 120 | persons[person_id] = [] 121 | 122 | persons[person_id].append(matrix[i][0][candidate + 1]) 123 | 124 | # Print results 125 | for person, scores in persons.iteritems(): 126 | print "Match with person %s with scores %s" % (person, [ "%.2f" % s for s in scores ]) 127 | 128 | # Reevaluation 129 | if arguments.reevaluate: 130 | print "Reevaluate faces" 131 | success = True 132 | 133 | # Get data 134 | if not faces: faces = list(database.iterator()) 135 | 136 | # Action 137 | [ algorithms.features_histogram(face[2], parameters["N"], parameters["K"]) for face in faces ] 138 | 139 | 140 | # Visualizing 141 | if arguments.depth_map: 142 | print "Generating depth map" 143 | success = True 144 | 145 | # Get data 146 | if not faces: faces = list(database.iterator()) 147 | 148 | # Action 149 | utils.generate_depth_map(faces, arguments.depth_map, arguments.draw_key_points) 150 | 151 | if arguments.feature_map: 152 | print "Generating feature map" 153 | success = True 154 | 155 | # Get data 156 | if not faces: faces = list(database.iterator()) 157 | 158 | # Action 159 | utils.generate_feature_map(faces, arguments.feature_map) 160 | 161 | if arguments.similarity_matrix: 162 | print "Generating similarity matrix" 163 | success = True 164 | 165 | # Get data 166 | if not faces: faces = list(database.iterator()) 167 | if not matrix: matrix = algorithms.similarity_matrix([ face[2] for face in faces ]) 168 | 169 | # Action 170 | utils.generate_similarity_matrix(matrix, faces, arguments.similarity_matrix) 171 | 172 | if arguments.roc_curve: 173 | print "Generating ROC curve" 174 | success = True 175 | 176 | # Get data 177 | if not faces: faces = list(database.iterator()) 178 | if not matrix: matrix = algorithms.similarity_matrix([ face[2] for face in faces ]) 179 | if not rocs: rocs = algorithms.calculate_roc_eer(matrix, [ face[1] for face in faces ]) 180 | 181 | utils.generate_roc_curve(rocs, arguments.roc_curve) 182 | 183 | # Print help in case of no action 184 | if not success: 185 | parser.print_help() 186 | sys.exit(1) 187 | else: 188 | sys.exit(0) 189 | 190 | def enroll_face(file, person_id=None, auto_id=False, force=False): 191 | filename = os.path.basename(file) 192 | 193 | # Check for duplicates 194 | if database.exists(file) and not force: 195 | print "File '%s' already enrolled" % filename 196 | return 197 | 198 | # Make sure we have an identifier 199 | if not person_id and not auto_id: 200 | print "Auto person identification disabled and no identification specified." 201 | return 202 | 203 | # File not yet enrolled 204 | print "Processing %s" % filename 205 | 206 | # Read data file 207 | absfile = AbsFile(file) 208 | 209 | # Derrive filename 210 | if auto_id: 211 | basename = os.path.basename(file) 212 | person_id = basename[:basename.index('d')] 213 | 214 | # Create Face object 215 | face = Face(absfile) 216 | 217 | # Apply algorithms to process raw data 218 | try: 219 | # Apply selected algorithms 220 | algorithms.process(face, parameters["N"], parameters["K"]) 221 | 222 | # Compress data 223 | face.compress() 224 | except: 225 | print "File '%s' failed" % file 226 | 227 | # In debug mode, show exceptions 228 | if debug: 229 | raise 230 | else: 231 | return 232 | 233 | # Enroll to database 234 | database.save(file, person_id, face) 235 | 236 | def argument_parser(): 237 | # Helper for argparse to match file and/or directory 238 | def helper_file_or_directory(parser): 239 | def _inner(path): 240 | # First, resolve 241 | path = os.path.realpath(path) 242 | 243 | # Then check path 244 | if not os.path.exists(path): 245 | parser.error("The file or directory '%s' does not exist" % path) 246 | else: 247 | if os.path.isdir(path): 248 | return (True, path) 249 | else: 250 | return (False, open(path, "rb")) 251 | return _inner 252 | 253 | # Helper for argparse to match file 254 | def helper_file(parser): 255 | def _inner(file): 256 | # First, resolve 257 | path = os.path.realpath(file) 258 | 259 | # Then check path 260 | if not os.path.exists(file) or os.path.isdir(file): 261 | parser.error("The file '%s' does not exist" % path) 262 | 263 | # Done 264 | return file 265 | return _inner 266 | 267 | # Create parser 268 | parser = argparse.ArgumentParser(description='Enroll, match and visualize 3D faces') 269 | 270 | # Add the arguments 271 | parser.add_argument('-d', '--database', default='database.db', help='path to cache file') 272 | parser.add_argument('-p', '--parameters', default='K=12,N=67', action='store', help='algorithm parameters, comma seperated') 273 | 274 | group = parser.add_argument_group(title="Face management") 275 | group.add_argument('--authenticate', type=helper_file(parser), help='authenticate a face to enrolled faces') 276 | group.add_argument('--enroll', type=helper_file_or_directory(parser), help='enroll face from file or directory') 277 | group.add_argument('--person-id', action='store', help='number or name identifing person') 278 | group.add_argument('--auto-id', action='store_true', help='derrive person identifier from filename') 279 | group.add_argument('--reevaluate', action='store_true', help='reevaluation enrolled faces, but do not save') 280 | 281 | group = parser.add_argument_group(title="Visualization") 282 | group.add_argument('--depth-map', action='store', help='generate a depth map of enrolled faces') 283 | group.add_argument('--feature-map', action='store', help='generate a feature map of enrolled faces') 284 | group.add_argument('--similarity-matrix', action='store', help='generate a similarity matrix of all implemented methods') 285 | group.add_argument('--roc-curve', action='store', help='generate a ROC curve of all implemented methods') 286 | group.add_argument('--draw-key-points', action='store_true', help='include key points on depth map') 287 | 288 | # Done 289 | return parser.parse_args(), parser 290 | 291 | # Application main 292 | if __name__ == '__main__': 293 | main() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Bas Stottelaar, Jeroen Senden 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Face3D 2 | Prototype system for 3D Face Recognition built by Bas Stottelaar and Jeroen Senden for the course Introduction to Biometrics. Written in Python, applicable to the FRGC 3D data set. 3 | 4 | ## Features 5 | The following algorithms have been implemented: 6 | 7 | ### Normalization 8 | * Smoothing 9 | * Interpolation 10 | * Cropping 11 | * Zooming 12 | * Key point extraction 13 | * Rotation 14 | 15 | ### Feature extraction 16 | Histogram based, as proposed by Zhou et al. See http://www.3dface.org/files/papers/zhou-EG08-histogram-face-rec.pdf for more information. 17 | 18 | ### Distance Metrics 19 | * Method 0: Euclidean Distance with threshold 0.9 20 | * Method 1: City Block Distance with threshold 0.9 21 | * Method 2: Sample Correlation Coefficient with threshold 0.9 22 | 23 | ## Installation 24 | The following dependencies are expected to be on your system 25 | 26 | * Python 2.7 (version 2.6 should work) 27 | * NumPy 1.6 28 | * SciPy 0.11 29 | * Matplotlib 1.2 30 | * SciKit-learn 0.12 31 | 32 | In case of missing dependencies, they can be installed via the Python Packet Manager, via the command `pip install `. 33 | 34 | ## Quick Start 35 | A few examples to get started: 36 | 37 | * `python Face3D.py --enroll /path/to/abs/files --auto-id` — Enrolls all files in the folder and determines person identification based on filename. 38 | * `python Face3D.py --authenticate /path/to/file.abs` — Authenticate given file against the enrolled images. Will output the matches with scores. 39 | * `python Face3D.py --reevaluate --parameters N=48,K=12` — Reevaluate the data set with the given parameters. Does not save data, but you could visualize something with this data. 40 | 41 | ## Usage 42 | Face3D is a commandline only application. Start a terminal and navigate to this directory where Face3D is extracted. Start the application with the command `python Face3D.py`. 43 | 44 | ### General 45 | * `python Face3D.py --help` — Show help. 46 | * `python Face3D.py --parameters` — Comma seperated key-value parameters for the algorithms. Defaults (and only parameters supported) are `N=67,K=12`. 47 | * `python Face3D.py --database` — Specify the Face3D Database to work on. Default is `database.db`. You need to specify this option each time if you would like to use another database for operations below. 48 | 49 | ### Face management 50 | * `python Face3D.py --enroll --person-id |--auto-id` — Enroll a single file or a complete directory to the Face3D Database. Multiple threads will be spawned in case of multiple files. You have to specify a person ID. In case of auto ID, it will be derrived from the `*.abs` filename (xxxxxd123.abs). This process can take up to 15 minutes for 350+ faces on a Intel Core i7. If a face has already been enrolled, it will notify the user. Simply delete the database file to start over. 51 | * `python Face3D.py --authenticate ` — Match a given face to a face in the database. 52 | * `python Face3D.py --reevaluate` — Reevaluate the faces with another set of parameters. Works only for feature extraction and other calculations after feature extraction. This comes in handy when evaluating different parameters. 53 | 54 | ### Visualization & Statistics 55 | * `python Face3D.py --depth-map [--with-key-points]` — Write a 3D depth map of enrolled faces to a PDF file, with or without key points. 56 | * `python Face3D.py --feature-map ` — Write a feature map of enrolled faces to a PDF file. 57 | * `python Face3D.py --similarity-matrix ` — Write a similarity matrix to a HTML file. 58 | * `python Face3D.py --roc-curve ` — Write a ROC curve to a HTML file. 59 | 60 | ## Source code 61 | The main application logic is defined in `Face3D.py`. The rest of the code is stored in the folder `face3d/`. 62 | 63 | One important file is `face3d/algorithms.py`. Here are all the algorithms programmed that are used for smoothing, interpolating, finding key points, cropping, feature extracting. Dependencies are `face3d/absfile.py` and `face3d/face.py`. The first reads `*.abs` files into memory and the second one is a wrapper for the data and handles views and compression. 64 | 65 | On of the two files left is `face3d/database.py`, a wrapper for an SQLite3 database file. It reads and writes faces and features. Last but not least is `face3d/utils.py` as a place for common used methods. 66 | 67 | ## Licence 68 | See the LICENCE file. -------------------------------------------------------------------------------- /face3d/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2012 Bas Stottelaar, Jeroen Senden 3 | See the file LICENSE for copying permission. 4 | """ 5 | 6 | from absfile import AbsFile 7 | from face import Face 8 | from database import Database -------------------------------------------------------------------------------- /face3d/absfile.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2012 Bas Stottelaar, Jeroen Senden 3 | See the file LICENSE for copying permission. 4 | """ 5 | 6 | import os 7 | import numpy 8 | 9 | class AbsFile(object): 10 | # Class variables 11 | invalid_value = -999999 12 | 13 | def __init__(self, filename): 14 | # Check if file exists 15 | if not os.path.exists(filename): 16 | raise Exception("Data file does not exist") 17 | 18 | # Create variables 19 | self.data = {} 20 | self.row_size = False 21 | self.col_size = False 22 | 23 | # Now read the file and create an XYZ matrix 24 | with open(filename, 'r') as file_handle: 25 | # Helper for dimension 26 | def read_dimension(dimension): 27 | line = file_handle.readline() 28 | data = line.strip().split(" ") 29 | 30 | if len(data) == 2 and data[1] == dimension: 31 | return int(data[0]) 32 | else: 33 | raise Exception("Invalid header: expected '%s'" % dimension) 34 | 35 | # Helper for data order 36 | def read_data_type(): 37 | line = file_handle.readline() 38 | data = line.strip().split(" ", 1) 39 | 40 | if len(data) == 2 and data[0] == "pixels": 41 | return data[1][1:-2].split(" ") 42 | else: 43 | raise Exception("Invalid header: expected data type") 44 | 45 | # Helper for reading data lines 46 | def read_data(data_type): 47 | # Initialize result array 48 | data_type = numpy.int if data_type == 'flag' else numpy.float 49 | result = numpy.zeros(self.col_size * self.row_size, dtype=data_type) 50 | index = 0 51 | 52 | # Read line 53 | line = file_handle.readline() 54 | data = line.strip().split(" ") 55 | 56 | for value in data: 57 | try: 58 | # Convert string to correct format 59 | result[index] = data_type(value) 60 | 61 | # Increment index 62 | index = index + 1 63 | except ValueError: 64 | print "Unexpected input: expected '%s', got '%s'" % (data_type, value) 65 | 66 | # In case of invalid values, mask invalid values 67 | if data_type == numpy.float: 68 | result[result == AbsFile.invalid_value] = numpy.nan 69 | 70 | # Return reshaped array 71 | return result.reshape(self.row_size, self.col_size) 72 | 73 | # Read the header of the file 74 | self.row_size = read_dimension('rows') 75 | self.col_size = read_dimension('columns') 76 | self.data_type = read_data_type() 77 | 78 | # Read the actual data 79 | for current_data_type in self.data_type: 80 | self.data[current_data_type] = read_data(current_data_type) 81 | 82 | @property 83 | def width(self): 84 | return self.col_size 85 | 86 | @property 87 | def height(self): 88 | return self.row_size -------------------------------------------------------------------------------- /face3d/algorithms.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2012 Bas Stottelaar, Jeroen Senden 3 | See the file LICENSE for copying permission. 4 | """ 5 | 6 | from utils import intertial_axis, max_xy, min_xy, find_peak_start, find_peak_stop 7 | 8 | from scipy.ndimage import filters, interpolation 9 | from scipy.interpolate import griddata 10 | from scipy import stats, signal, optimize, interpolate 11 | from sklearn import metrics 12 | 13 | import numpy 14 | import scipy 15 | import math 16 | import pylab 17 | import threading 18 | 19 | def process(face, N, K): 20 | """ Apply the selected algorithms on a given face """ 21 | 22 | # Normalize face 23 | smooth(face) 24 | repair(face) 25 | crop(face) 26 | zoom(face) 27 | key_points(face) 28 | rotate(face) 29 | key_points(face) 30 | fit(face) 31 | 32 | # Extract features 33 | features_histogram(face, N, K) 34 | 35 | def smooth(face): 36 | """ Smooth data. Removes peaks """ 37 | 38 | # Helper method 39 | def smooth_axis(axis): 40 | face.abs_file.data[axis] = filters.median_filter(face.abs_file.data[axis], size=4) 41 | face.abs_file.data[axis] = filters.gaussian_filter(face.abs_file.data[axis], sigma=1, mode='nearest') 42 | 43 | # Smooth it 44 | smooth_axis('X') 45 | smooth_axis('Y') 46 | smooth_axis('Z') 47 | 48 | def repair(face): 49 | """ Fill missing data by interpolating """ 50 | 51 | # Helper method 52 | def interpolate_axis(axis): 53 | A = face.abs_file.data[axis] 54 | 55 | # Calculate parameters 56 | mask = numpy.isfinite(A) 57 | points = mask.nonzero() 58 | values = A[points] 59 | grid_coords = numpy.meshgrid(numpy.arange(0, len(A[:, 0]), 1), numpy.arange(0, len(A[0, :]), 1)) 60 | 61 | # Apply interpolation 62 | face.abs_file.data[axis] = griddata(points, values, grid_coords, method='linear').T 63 | 64 | interpolate_axis('X') 65 | interpolate_axis('Y') 66 | interpolate_axis('Z') 67 | 68 | def key_points(face, 69 | d_nose_x1=30, d_nose_x2=5, d_nose_y=5, 70 | d_lip_y1=25, d_lip_y2=70, d_lip_y3=4, d_lip_x1=50, 71 | d_chin_x=3, d_chin_y1=50, d_chin_y2=75, 72 | d_eye_x=2, d_eye_y=50): 73 | 74 | """ 75 | Rotate and zoom the face to create a full frame face. This is based on the 76 | fact that the nose is the highest point of the picture 77 | """ 78 | 79 | # We apply surfature to calculate the first and second derivates 80 | K, H, Pmax, Pmin = surfature(face) 81 | 82 | # Remove all key points 83 | face.key_points.clear() 84 | 85 | # 86 | # Nose 87 | # 88 | nose_x, nose_y = max_xy(face.Z) 89 | face.key_points["nose"] = (nose_x, nose_y) 90 | 91 | # 92 | # Nose left and right 93 | # 94 | nose_left = Pmin[(nose_y - d_nose_y):(nose_y + d_nose_y), (nose_x - d_nose_x1):(nose_x - d_nose_x2)] 95 | nose_right = Pmin[(nose_y - d_nose_y):(nose_y + d_nose_y), (nose_x + d_nose_x2):(nose_x + d_nose_x1)] 96 | 97 | nose_left_x, nose_left_y = min_xy(nose_left, offset_x=(nose_x - d_nose_x1), offset_y=(nose_y - d_nose_y)) 98 | nose_right_x, nose_right_y = min_xy(nose_right, offset_x=(nose_x + d_nose_x2), offset_y=(nose_y - d_nose_y)) 99 | 100 | face.key_points["nose_left"] = (nose_left_x, nose_left_y) 101 | face.key_points["nose_right"] = (nose_right_x, nose_right_y) 102 | 103 | # 104 | # Upper, lower, left right lip 105 | # 106 | lip_y = numpy.nanargmax(Pmax[(nose_y + d_lip_y1):(nose_y + d_lip_y2), nose_x]) + (nose_y + d_lip_y1) 107 | lip_left = Pmax[(lip_y - d_lip_y3):(lip_y + d_lip_y3), (nose_x - d_lip_x1):nose_x] 108 | lip_right = Pmax[(lip_y - d_lip_y3):(lip_y + d_lip_y3), nose_x:(nose_x + d_lip_x1)] 109 | 110 | lip_left_x = find_peak_start(numpy.sum(lip_left, axis=0)) + (nose_x - d_lip_x1) 111 | lip_left_y = numpy.nanargmax(Pmax[(lip_y - d_lip_y3):(lip_y + d_lip_y3), lip_left_x]) + (lip_y - d_lip_y3) 112 | 113 | lip_right_x = find_peak_stop(numpy.sum(lip_right, axis=0)) + nose_x 114 | lip_right_y = numpy.nanargmax(Pmax[(lip_y - d_lip_y3):(lip_y + d_lip_y3), lip_right_x]) + (lip_y - d_lip_y3) 115 | 116 | face.key_points['lip'] = (nose_x, lip_y) 117 | face.key_points['lip_left'] = (lip_left_x, lip_left_y) 118 | face.key_points['lip_right'] = (lip_right_x, lip_right_y) 119 | 120 | # 121 | # Chin 122 | # 123 | chin = numpy.gradient(signal.bspline(face.Z[(lip_y + d_chin_y1):, nose_x], 25)) 124 | chin_x, chin_y = nose_x, numpy.nanargmin(chin) + (lip_y + d_chin_y1) 125 | 126 | face.key_points["chin"] = (chin_x, chin_y) 127 | 128 | # 129 | # Eyes 130 | # 131 | eye_left = Pmax[d_eye_y:nose_left_y - d_eye_y, nose_left_x - d_eye_x:nose_left_x + d_eye_x] 132 | eye_right = Pmax[d_eye_y:nose_right_y - d_eye_y, nose_right_x - d_eye_x:nose_right_x + d_eye_x] 133 | 134 | eye_left_x, eye_left_y = max_xy(eye_left, nose_left_x - d_eye_x, d_eye_y) 135 | eye_right_x, eye_right_y = max_xy(eye_right, nose_right_x - d_eye_x, d_eye_y) 136 | 137 | face.key_points["eye_left"] = (eye_left_x, eye_left_y) 138 | face.key_points["eye_right"] = (eye_right_x, eye_right_y) 139 | 140 | # 141 | # Nose face border 142 | # 143 | nose_line = numpy.gradient(face.Z[nose_y, :]) 144 | border_nose_left_x, border_nose_left_y = numpy.nanargmax(nose_line[:lip_left_x - 10]), nose_y 145 | border_nose_right_x, border_nose_right_y = numpy.nanargmin(nose_line[lip_right_x + 10:]) + lip_right_x + 10, nose_y 146 | 147 | face.key_points["border_nose_left"] = (border_nose_left_x, border_nose_left_y) 148 | face.key_points["border_nose_right"] = (border_nose_right_x, border_nose_right_y) 149 | 150 | # 151 | # Lip face border 152 | # 153 | lip_line = numpy.gradient(face.Z[lip_y, :]) 154 | border_lip_left_x, border_lip_left_y = numpy.nanargmax(lip_line[:lip_left_x - 10]), lip_y 155 | border_lip_right_x, border_lip_right_y = numpy.nanargmin(lip_line[lip_right_x + 10:]) + lip_right_x + 10, lip_y 156 | 157 | face.key_points["border_lip_left"] = (border_lip_left_x, border_lip_left_y) 158 | face.key_points["border_lip_right"] = (border_lip_right_x, border_lip_right_y) 159 | 160 | # 161 | # Forehead border 162 | # 163 | forehead_line = numpy.gradient(face.Z[nose_y - (chin_y - nose_y), :]) 164 | border_forehead_left_x, border_forehead_left_y = numpy.nanargmax(forehead_line[:lip_left_x - 10]), nose_y - (chin_y - nose_y) 165 | border_forehead_right_x, border_forehead_right_y = numpy.nanargmin(forehead_line[lip_right_x + 10:]) + lip_right_x + 10, nose_y - (chin_y - nose_y) 166 | 167 | face.key_points["border_forehead_left"] = (border_forehead_left_x, border_forehead_left_y) 168 | face.key_points["border_forehead_right"] = (border_forehead_right_x, border_forehead_right_y) 169 | 170 | def rotate(face): 171 | """ Rotate the face by taking the mean slope of the nose and lip """ 172 | 173 | # Nose rotation 174 | d_nose_y = face.key_points["nose_left"][1] - face.key_points["nose_right"][1] 175 | d_nose_x = face.key_points["nose_right"][0] - face.key_points["nose_left"][0] 176 | degrees_nose = math.degrees(math.atan2(d_nose_y, d_nose_x)) 177 | 178 | # Lip rotation 179 | d_lip_y = face.key_points["lip_left"][1] - face.key_points["lip_right"][1] 180 | d_lip_x = face.key_points["lip_right"][0] - face.key_points["lip_left"][0] 181 | degrees_lip = math.degrees(math.atan2(d_lip_y, d_lip_x)) 182 | 183 | # Calculate average rotation and rotate 184 | degrees = (degrees_nose + degrees_lip) / 2 185 | face.abs_file.data['X'] = interpolation.rotate(face.abs_file.data['X'], degrees, mode='nearest', prefilter=False, reshape=False) 186 | face.abs_file.data['Y'] = interpolation.rotate(face.abs_file.data['Y'], degrees, mode='nearest', prefilter=False, reshape=False) 187 | face.abs_file.data['Z'] = interpolation.rotate(face.abs_file.data['Z'], degrees, mode='nearest', prefilter=False, reshape=False) 188 | 189 | def zoom(face): 190 | """ Move everything such that nose is at depth 0 """ 191 | 192 | # Correct the nose tip to be at 0 193 | point = max_xy(face.Z) 194 | 195 | face.abs_file.data['X'] = face.abs_file.data['X'] + abs(face.X[point]) 196 | face.abs_file.data['Y'] = face.abs_file.data['Y'] + abs(face.Y[point]) 197 | face.abs_file.data['Z'] = face.abs_file.data['Z'] + abs(face.Z[point]) 198 | 199 | def fit(face): 200 | """ Crops the image to face width and face height """ 201 | 202 | chin_x, chin_y = face.key_points["chin"] 203 | nose_x, nose_y = face.key_points["nose"] 204 | 205 | border_lip_left_x, border_lip_left_y = face.key_points["border_lip_left"] 206 | border_lip_right_x, border_lip_right_y = face.key_points["border_lip_right"] 207 | 208 | border_forehead_left_x, border_forehead_left_y = face.key_points["border_forehead_left"] 209 | border_forehead_right_x, border_forehead_right_y = face.key_points["border_forehead_right"] 210 | 211 | golden_ratio = 1.61803399 212 | face_height = (chin_y - nose_y) + (chin_y - nose_y) * golden_ratio 213 | #face_width = stats.nanmean(numpy.array([border_forehead_right_x, border_lip_right_x])) - stats.nanmean(numpy.array([border_forehead_left_x + border_lip_left_x])) 214 | face_width = face_height / golden_ratio 215 | 216 | # Overscan 217 | face_height = face_height * 0.90 218 | face_width = face_width * 0.95 219 | 220 | # Fit region 221 | face.center_at(nose_x, chin_y - (face_height / 2.0), face_width / 2.0, face_height / 2.0) 222 | 223 | def crop(face): 224 | """ 225 | Crop the image to remove as much unneeded information as possible. This 226 | works by applying PCA to find the torso and then find the nose. 227 | 228 | The result is a view that is centered at the nose. 229 | """ 230 | 231 | # Reset image first to make sure we take all of the image 232 | face.reset() 233 | 234 | # Calculate the position of the image 235 | masked_z = numpy.ma.masked_array(face.Z, numpy.isnan(face.Z)) 236 | x, y, covariance = intertial_axis(masked_z) 237 | 238 | # Center at the point 239 | overscan_x = face.width * 0.25 240 | overscan_y = face.height * 0.25 241 | face.center_at(x, y, overscan_x, overscan_y) 242 | 243 | # Calculate max Z-value x and y 244 | x, y = max_xy(face.Z) 245 | 246 | # Set view to center of nose 247 | face.center_at(x, y, 240 / 2.0, 320 / 2.0) 248 | 249 | def features_histogram(face, N=67, K=12): 250 | """ 251 | From 'A 3D Face Recognition Algorithm Using Histogram-based Features' 252 | """ 253 | 254 | # It only works with non-nan values 255 | masked_z = numpy.ma.masked_array(face.Z, numpy.isnan(face.Z)) 256 | results = [] 257 | 258 | # Split the complete Z matrix into N smaller Zi 259 | for i, Zi in zip(range(N), numpy.array_split(masked_z, N)): 260 | result, temp = numpy.histogram(Zi, bins=K, range=(Zi.min(), Zi.max()), density=False) 261 | results.append(result) 262 | 263 | # Convert back to array 264 | face.features = ("histogram", numpy.array(results).reshape(-1), (N, K)) 265 | 266 | def distance_histogram_city_block(face1, face2): 267 | """ 268 | Calculate the City Block distance of two histogram feature vectors 269 | """ 270 | 271 | def _func(U, V, U_V): 272 | return numpy.sum([ numpy.abs(Ui - Vi) for Ui, Vi in U_V ]) 273 | 274 | return distance_histogram(face1, face2, _func) 275 | 276 | def distance_histogram_euclidean(face1, face2): 277 | """ 278 | Calculate the Euclidean distance of two histogram feature vectors 279 | """ 280 | 281 | def _func(U, V, U_V): 282 | return numpy.sqrt(numpy.sum([ numpy.power(Ui - Vi, 2) for Ui, Vi in U_V ])) 283 | 284 | return distance_histogram(face1, face2, _func) 285 | 286 | def distance_histogram_correlation(face1, face2): 287 | """ 288 | Calculate the Sample Correlation Coefficient of two histogram feature vectors 289 | """ 290 | 291 | def _func(U, V, U_V): 292 | Umean = U.mean() 293 | Vmean = V.mean() 294 | Ustd = U.std() 295 | Vstd = V.std() 296 | 297 | samples = [ (Ui - Umean)*(Vi - Vmean) for Ui, Vi in U_V ] 298 | return numpy.sum(samples) / ((len(samples) - 1) * Ustd * Vstd) 299 | 300 | return distance_histogram(face1, face2, _func) 301 | 302 | def distance_histogram(face1, face2, func): 303 | """ Base method for distance funcions """ 304 | 305 | U = face1.features[1] 306 | V = face2.features[1] 307 | 308 | # Make sure both are same size 309 | if U.shape != V.shape: 310 | raise Exception("Feature vectors do not match size") 311 | 312 | # Calculate the distance 313 | return func(U, V, zip(U, V)) 314 | 315 | def similarity_matrix(faces, methods=None, normalizers=None, limit=None): 316 | """ 317 | Calculate the similarity matrix for given set of faces with a given 318 | set of methods and normalizers. For each method, a seperate thread will 319 | be spawned 320 | """ 321 | 322 | # Set default methods 323 | if not methods: 324 | methods = [distance_histogram_euclidean, distance_histogram_city_block, distance_histogram_correlation] 325 | 326 | # Set default normalizers 327 | if not normalizers: 328 | normalizers = [score_normalization_min_max, score_normalization_min_max, False] 329 | 330 | # Create output array 331 | output = numpy.zeros(shape=(len(methods), len(faces), len(faces))) 332 | output[:] = numpy.nan 333 | 334 | # Precalculations 335 | count_faces = len(faces) 336 | count_methods = len(methods) 337 | count_faces_limited = count_faces if not limit else limit 338 | threads = [] 339 | 340 | # Iterate each face 341 | def _func(index, method, normalizer): 342 | # Create similarity matrix 343 | for i in range(count_faces_limited): 344 | if i % 25 == 0: 345 | print "Method %d: %d/%d" % (index, i, count_faces) 346 | 347 | for j in range(i, count_faces): 348 | output[(index, i, j)] = method(faces[i], faces[j]) 349 | 350 | # Normalize matrix 351 | if normalizer: 352 | normalizer(output[index]) 353 | 354 | # Print some info 355 | print "Finished similarity matrix for method %d" % index 356 | 357 | # Spawn the threads 358 | for i in range(count_methods): 359 | thread = threading.Thread(target=_func, args=(i, methods[i], normalizers[i])) 360 | thread.daemon = True 361 | threads.append(thread) 362 | thread.start() 363 | 364 | # Wait for all threads to complete 365 | for thread in threads: 366 | thread.join() 367 | 368 | # Done 369 | return output 370 | 371 | def score_normalization_min_max(matrix): 372 | """ In place normalization to min-max scores """ 373 | 374 | # Calculate min/max 375 | dmin = numpy.nanmin(matrix) 376 | dmax = numpy.nanmax(matrix) 377 | 378 | # In-place transformation 379 | matrix -= dmin 380 | matrix /= (dmax - dmin) 381 | 382 | # Convert to score 383 | numpy.subtract(1, matrix, matrix) 384 | 385 | def calculate_roc_eer(matrix, person_ids): 386 | """ Calculate the ROC curve and estimate the EER """ 387 | 388 | methods, _, _ = matrix.shape 389 | count = len(person_ids) 390 | result = [False, False, False] 391 | threads = [] 392 | 393 | # Calculate ROC curve and EER for each method 394 | def _func(index): 395 | targets = [] 396 | outputs = [] 397 | 398 | for i in range(count): 399 | if i % 25 == 0: 400 | print "Method %d: %d/%d" % (index, i, count) 401 | 402 | for j in range(i, count): 403 | if person_ids[i] == person_ids[j]: 404 | targets.append(1) 405 | else: 406 | targets.append(0) 407 | 408 | outputs.append(matrix[index][i][j]) 409 | 410 | # Calculate ROC curve 411 | tpr, fpr, _ = metrics.roc_curve(targets, outputs) 412 | 413 | # Create three function for solving 414 | f = interpolate.interp1d(tpr, fpr, bounds_error=False) 415 | g = lambda x: 1 - x 416 | 417 | # Estimate the EER -- the intersection of f(x) and g(x) 418 | for x in numpy.linspace(0, 1, 1000): 419 | # Skip boundaries as they are invalid for the interpolator 420 | if x == 0.0 or x == 1.0: 421 | continue 422 | 423 | # Check intersection point 424 | if f(x) >= g(x): 425 | eer = x 426 | break 427 | 428 | # Append data to result list 429 | result[index] = ((tpr, fpr), eer) 430 | 431 | # Print some info 432 | print "Finished ROC and EER for method %d" % index 433 | 434 | 435 | # Spawn the threads 436 | for i in range(methods): 437 | thread = threading.Thread(target=_func, args=(i,)) 438 | thread.daemon = True 439 | threads.append(thread) 440 | thread.start() 441 | 442 | # Wait for all threads to complete 443 | for thread in threads: 444 | thread.join() 445 | 446 | # Done 447 | return result 448 | 449 | def surfature(face): 450 | """ 451 | Calculate the surfatures of a given face. Based on a Matlab implementation 452 | http://stackoverflow.com/questions/11317579/surface-curvature-matlab-equivalent-in-python 453 | """ 454 | 455 | # First derivatives 456 | Xu, Xv = numpy.gradient(face.X) 457 | Yu, Yv = numpy.gradient(face.Y) 458 | Zu, Zv = numpy.gradient(face.Z) 459 | 460 | # Second derivates 461 | Xuu, Xuv = numpy.gradient(Xu) 462 | Yuu, Yuv = numpy.gradient(Yu) 463 | Zuu, Zuv = numpy.gradient(Zu) 464 | 465 | Xuv, Xvv = numpy.gradient(Xv) 466 | Yuv, Yvv = numpy.gradient(Yv) 467 | Zuv, Zvv = numpy.gradient(Zv) 468 | 469 | # Reshape to vector 470 | Xu = Xu.reshape(-1, 1) 471 | Yu = Yu.reshape(-1, 1) 472 | Zu = Zu.reshape(-1, 1) 473 | 474 | Xv = Xv.reshape(-1, 1) 475 | Yv = Yv.reshape(-1, 1) 476 | Zv = Zv.reshape(-1, 1) 477 | 478 | Xuu = Xuu.reshape(-1, 1) 479 | Yuu = Yuu.reshape(-1, 1) 480 | Zuu = Zuu.reshape(-1, 1) 481 | 482 | Xuv = Xuv.reshape(-1, 1) 483 | Yuv = Yuv.reshape(-1, 1) 484 | Zuv = Zuv.reshape(-1, 1) 485 | 486 | Xvv = Xvv.reshape(-1, 1) 487 | Yvv = Yvv.reshape(-1, 1) 488 | Zvv = Zvv.reshape(-1, 1) 489 | 490 | # Reshape data 491 | XYZu = numpy.concatenate((Xu, Yu, Zu), 1) 492 | XYZv = numpy.concatenate((Xv, Yv, Zv), 1) 493 | XYZuu = numpy.concatenate((Xuu, Yuu, Zuu), 1) 494 | XYZuv = numpy.concatenate((Xuv, Yuv, Zuv), 1) 495 | XYZvv = numpy.concatenate((Xvv, Yvv, Zvv), 1) 496 | 497 | # First fundamental coefficients 498 | E = numpy.sum(XYZu * XYZu, 1) 499 | F = numpy.sum(XYZu * XYZv, 1) 500 | G = numpy.sum(XYZv * XYZv, 1) 501 | 502 | m = numpy.cross(XYZu, XYZv) 503 | p = numpy.sqrt(numpy.sum(m * m, 1)) 504 | n = numpy.divide(m, numpy.array([p, p, p]).T) 505 | 506 | # Second fundamental coefficients 507 | L = numpy.sum(XYZuu * n, 1) 508 | M = numpy.sum(XYZuv * n, 1) 509 | N = numpy.sum(XYZvv * n, 1) 510 | 511 | # Retrieve size 512 | s, t = face.Z.shape 513 | 514 | # Gaussian curvature 515 | K1 = numpy.multiply(L, N) - numpy.power(M, 2) 516 | K2 = numpy.multiply(E, G) - numpy.power(F, 2) 517 | K = numpy.divide(K1, K2).reshape(s, t) 518 | 519 | # Mean curvature 520 | H1 = numpy.multiply(E, N) + numpy.multiply(G, L) - numpy.multiply(numpy.multiply(2, F), M) 521 | H2 = numpy.multiply(2, numpy.multiply(E, G) - numpy.power(F, 2)) 522 | H = numpy.divide(H1, H2).reshape(s, t) 523 | 524 | # Determine min and max curvatures 525 | Pmax = H + numpy.sqrt(numpy.power(H, 2) - K) 526 | Pmin = H - numpy.sqrt(numpy.power(H, 2) - K) 527 | 528 | # Done 529 | return K, H, Pmax, Pmin -------------------------------------------------------------------------------- /face3d/database.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2012 Bas Stottelaar, Jeroen Senden 3 | See the file LICENSE for copying permission. 4 | """ 5 | 6 | from utils import GeneratorLen 7 | 8 | import sqlite3 9 | import cPickle 10 | import cStringIO 11 | import gzip 12 | import threading 13 | 14 | class Database(object): 15 | 16 | def __init__(self, path): 17 | self.lock = threading.Lock() 18 | self.connection = sqlite3.connect(path, check_same_thread=False) 19 | self.connection.execute("CREATE TABLE IF NOT EXISTS faces (filename TEXT, person_id TEXT, data BLOB);") 20 | 21 | def flush(self): 22 | with self.lock: 23 | self.connection.execute("DELETE FROM faces;") 24 | self.connection.commit() 25 | 26 | def exists(self, filename): 27 | with self.lock: 28 | cursor = self.connection.cursor() 29 | cursor.execute("SELECT filename FROM faces WHERE filename = ? LIMIT 1", (filename, )) 30 | 31 | for row in cursor: 32 | return True 33 | 34 | return False 35 | 36 | def load(self, filename): 37 | with self.lock: 38 | cursor = self.connection.cursor() 39 | cursor.execute("SELECT filename, data FROM faces WHERE filename = ? LIMIT 1", (filename, )) 40 | 41 | for filename, data in cursor: 42 | string_file = cStringIO.StringIO(data) 43 | return cPickle.load(gzip.GzipFile(fileobj=string_file, mode='rb')) 44 | 45 | def save(self, filename, person_id, data): 46 | # Serialize and compress data 47 | string_file = cStringIO.StringIO() 48 | 49 | with gzip.GzipFile(fileobj=string_file, mode='wb') as gzip_file: 50 | gzip_file.write(cPickle.dumps(data)) 51 | 52 | # Create SQLite compatible data 53 | data = sqlite3.Binary(string_file.getvalue()) 54 | 55 | # Insert data and commit 56 | with self.lock: 57 | self.connection.execute("INSERT INTO faces(filename, person_id, data) VALUES (?, ?, ?)", (filename, person_id, data)) 58 | self.connection.commit() 59 | 60 | def iterator(self): 61 | with self.lock: 62 | cursor = self.connection.cursor() 63 | cursor.execute("SELECT filename, person_id FROM faces ORDER BY person_id ASC") 64 | rows = [ (filename, person_id) for filename, person_id, in cursor ] 65 | count = len(rows) 66 | 67 | def _generator(): 68 | for (filename, person_id) in rows: 69 | yield (filename, person_id, self.load(filename)) 70 | 71 | return GeneratorLen(_generator(), count) -------------------------------------------------------------------------------- /face3d/face.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2012 Bas Stottelaar, Jeroen Senden 3 | See the file LICENSE for copying permission. 4 | """ 5 | 6 | from matplotlib import pyplot 7 | from mpl_toolkits.mplot3d import Axes3D 8 | 9 | class Face(object): 10 | 11 | def __init__(self, abs_file): 12 | self.abs_file = abs_file 13 | self.compressed = False 14 | self.features = False 15 | self.key_points = {} 16 | self.reset() 17 | 18 | def reset(self): 19 | self.set_view(0, 0, self.abs_file.width, self.abs_file.height) 20 | 21 | def set_view(self, x_min, y_min, x_max, y_max): 22 | self.x_min = x_min 23 | self.y_min = y_min 24 | self.x_max = x_max 25 | self.y_max = y_max 26 | 27 | def center_at(self, x, y, delta_x, delta_y): 28 | # Translate and sanitize input 29 | x = int(x) + self.x_min 30 | y = int(y) + self.y_min 31 | delta_x = int(delta_x) 32 | delta_y = int(delta_y) 33 | 34 | # Save difference 35 | old_x_min = self.x_min 36 | old_y_min = self.y_min 37 | 38 | # Check values 39 | if delta_x * 2 > self.abs_file.width: 40 | raise ValueError("Delta x out of range") 41 | 42 | if delta_y * 2 > self.abs_file.height: 43 | raise ValueError("Delta y out of range") 44 | 45 | # X axis 46 | if x + delta_x > self.abs_file.width: 47 | self.x_min = self.abs_file.width - delta_x - delta_x 48 | self.x_max = self.abs_file.width 49 | elif x - delta_x < 0: 50 | self.x_min = 0 51 | self.x_max = delta_x + delta_x 52 | else: 53 | self.x_min = x - delta_x 54 | self.x_max = x + delta_x 55 | 56 | # Y axis 57 | if y + delta_y > self.abs_file.height: 58 | self.y_min = self.abs_file.height - delta_y - delta_y 59 | self.y_max = self.abs_file.height 60 | elif y - delta_y < 0: 61 | self.y_min = 0 62 | self.y_max = delta_y + delta_y 63 | else: 64 | self.y_min = y - delta_y 65 | self.y_max = y + delta_y 66 | 67 | # Translate each point 68 | for name, point in self.key_points.iteritems(): 69 | x, y = point 70 | self.key_points[name] = (x + (old_x_min - self.x_min), y + (old_y_min - self.y_min)) 71 | 72 | def add_key_point(name, x, y): 73 | self.key_points[name] = (x + self.x_min, y + self.y_min) 74 | 75 | @property 76 | def X(self): 77 | return self.abs_file.data['X'][range(self.y_min, self.y_max), :][:, range(self.x_min, self.x_max)] 78 | 79 | @property 80 | def Y(self): 81 | return self.abs_file.data['Y'][range(self.y_min, self.y_max), :][:, range(self.x_min, self.x_max)] 82 | 83 | @property 84 | def Z(self): 85 | return self.abs_file.data['Z'][range(self.y_min, self.y_max), :][:, range(self.x_min, self.x_max)] 86 | 87 | @property 88 | def width(self): 89 | return self.x_max - self.x_min 90 | 91 | @property 92 | def height(self): 93 | return self.y_max - self.y_min 94 | 95 | def plot_3d(self): 96 | figure = pyplot.figure() 97 | 98 | # Draw surface 99 | axis = Axes3D(figure) 100 | axis.plot_surface(X=self.X, Y=self.Y, Z=self.Z) 101 | 102 | return figure 103 | 104 | def compress(self): 105 | self.abs_file.data['X'] = self.X 106 | self.abs_file.data['Y'] = self.Y 107 | self.abs_file.data['Z'] = self.Z 108 | 109 | self.abs_file.col_size = self.width 110 | self.abs_file.row_size = self.height 111 | 112 | self.compressed = True 113 | self.reset() 114 | 115 | -------------------------------------------------------------------------------- /face3d/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2012 Bas Stottelaar, Jeroen Senden 3 | See the file LICENSE for copying permission. 4 | """ 5 | 6 | from matplotlib import pyplot 7 | from scipy import signal 8 | 9 | import numpy 10 | import scipy 11 | import math 12 | import os 13 | 14 | def max_xy(input, offset_x=0, offset_y=0): 15 | index = numpy.nanargmax(input) 16 | y, x = numpy.unravel_index(index, input.shape) 17 | return x + offset_x, y + offset_y 18 | 19 | def min_xy(input, offset_x=0, offset_y=0): 20 | index = numpy.nanargmin(input) 21 | y, x = numpy.unravel_index(index, input.shape) 22 | return x + offset_x, y + offset_y 23 | 24 | def find_peak_start(input, treshold=0.001, peak_length=15): 25 | input = numpy.abs(numpy.gradient(signal.bspline(input, 25))) 26 | 27 | # Control parameters 28 | input_treshold = numpy.nanmax(input) * treshold 29 | input_length = input.shape[0] 30 | recording = False 31 | start_x = 0 32 | stop_x = 0 33 | 34 | # Walk from start to end. When the current value exceeds threshold, 35 | # start recording. 36 | for i in range(0, input_length): 37 | if recording: 38 | if input[i] > treshold: 39 | stop_x = i 40 | 41 | if (stop_x - start_x) > peak_length: 42 | return start_x 43 | else: 44 | recording = False 45 | else: 46 | if input[i] > treshold: 47 | start_x = i 48 | recording = True 49 | 50 | # Nothing found 51 | return 0 52 | 53 | def find_peak_stop(input, *args): 54 | # Apply the start search but reverse array 55 | x = find_peak_start(input[::1], *args) 56 | 57 | # Reverse result 58 | return input.shape[0] - x 59 | 60 | def face(index=0, fit=True, N=67, K=12, interpolation=True): 61 | from absfile import AbsFile 62 | from face import Face 63 | import algorithms 64 | 65 | file = ['test_16/04371d164.abs', 'test_same/04203d350.abs', 'test_same/04203d352.abs', 'test_same/04203d354.abs', 'test_8/04316d216.abs', 'test_4/04374d193.abs'][index] 66 | face = Face(AbsFile(file)) 67 | 68 | if interpolation: 69 | algorithms.repair(face) 70 | 71 | algorithms.smooth(face) 72 | algorithms.crop(face) 73 | algorithms.zoom(face) 74 | algorithms.key_points(face) 75 | algorithms.rotate(face) 76 | 77 | if fit: 78 | algorithms.key_points(face) 79 | algorithms.fit(face) 80 | 81 | # Extract features 82 | algorithms.features_histogram(face, N, K) 83 | 84 | return face 85 | 86 | def evaluate_interpolation(output_file='interpolate.pdf'): 87 | f = face(5, interpolation=False) 88 | g = face(5) 89 | figure = pyplot.figure() 90 | 91 | subplot = pyplot.subplot(1, 2, 1) 92 | subplot.imshow(f.Z) 93 | 94 | subplot.xaxis.set_visible(False) 95 | subplot.yaxis.set_visible(False) 96 | 97 | subplot = pyplot.subplot(1, 2, 2) 98 | subplot.imshow(g.Z) 99 | 100 | subplot.xaxis.set_visible(False) 101 | subplot.yaxis.set_visible(False) 102 | 103 | figure.savefig(output_file, format='pdf', dpi=600, orientation='landscape', bbox_inches="tight") 104 | 105 | def evaluate_features(output_file='features.pdf'): 106 | import algorithms 107 | blue = (0.0, 0.0, 1.0, 1.0) 108 | g = face(1, True, 18, 12) 109 | h = face(2, True, 18, 12) 110 | f = face(3, True, 18, 12) 111 | k = face(0, True, 18, 12) 112 | 113 | figure = pyplot.figure() 114 | 115 | subplot = pyplot.subplot(1, 5, 1) 116 | 117 | subplot.imshow(g.Z) 118 | 119 | for xx in range(1, 8): 120 | v = int((xx) * (g.height / 8)) 121 | subplot.axhline(v, color=blue) 122 | 123 | subplot.set_xlabel('Different regions (N=8)') 124 | subplot.xaxis.set_visible(False) 125 | subplot.yaxis.set_visible(False) 126 | 127 | subplot = pyplot.subplot(1, 5, 2) 128 | subplot.imshow(g.features[1].reshape(g.features[2])) 129 | subplot.set_xlabel('Person 1a') 130 | subplot.xaxis.set_visible(False) 131 | subplot.yaxis.set_visible(False) 132 | 133 | subplot = pyplot.subplot(1, 5, 3) 134 | subplot.imshow(h.features[1].reshape(h.features[2])) 135 | subplot.set_xlabel('Person 1b') 136 | subplot.xaxis.set_visible(False) 137 | subplot.yaxis.set_visible(False) 138 | 139 | subplot = pyplot.subplot(1, 5, 4) 140 | subplot.imshow(f.features[1].reshape(f.features[2])) 141 | subplot.set_xlabel('Person 1c') 142 | subplot.xaxis.set_visible(False) 143 | subplot.yaxis.set_visible(False) 144 | 145 | subplot = pyplot.subplot(1, 5, 5) 146 | subplot.imshow(k.features[1].reshape(k.features[2])) 147 | subplot.set_xlabel('Person 2a') 148 | subplot.xaxis.set_visible(False) 149 | subplot.yaxis.set_visible(False) 150 | 151 | figure.savefig(output_file, format='pdf', dpi=600, orientation='landscape', bbox_inches="tight") 152 | 153 | def evaluate_feature_extraction(output_file='face.pdf', output2_file='surfature.pdf', output3_file='mouth.pdf'): 154 | import algorithms 155 | f = face(4, fit=False) 156 | g = face(4) 157 | h = face(1) 158 | grey = (0.7, 0.7, 0.7, 0.7) 159 | K, H, Pmax, Pmin = algorithms.surfature(f) 160 | 161 | ######## 162 | 163 | figure = pyplot.figure() 164 | 165 | subplot = pyplot.subplot(1, 1, 1) 166 | subplot.imshow(h.abs_file.data['Z']) 167 | 168 | subplot.xaxis.set_visible(False) 169 | subplot.yaxis.set_visible(False) 170 | 171 | figure.savefig("raw.pdf", format='pdf', dpi=600, orientation='landscape', bbox_inches="tight") 172 | 173 | ######## 174 | 175 | figure = pyplot.figure() 176 | 177 | subplot = pyplot.subplot(1, 1, 1) 178 | subplot.imshow(f.Z) 179 | 180 | for name, point in f.key_points.iteritems(): 181 | x, y = point 182 | subplot.plot(x, y, 'x', color='k') 183 | 184 | nose_x, nose_y = f.key_points['nose'] 185 | lip_x, lip_y = f.key_points['lip'] 186 | chin_x, chin_y = f.key_points['chin'] 187 | 188 | nose_left_x, nose_left_y = f.key_points['nose_left'] 189 | nose_right_x, nose_right_y = f.key_points['nose_right'] 190 | lip_left_x, lip_left_y = f.key_points['lip_left'] 191 | lip_right_x, lip_right_y = f.key_points['lip_right'] 192 | 193 | border_lip_left_x, border_lip_left_y = f.key_points['border_lip_left'] 194 | border_lip_right_x, border_lip_right_y = f.key_points['border_lip_right'] 195 | 196 | pyplot.plot([nose_left_x, nose_right_x], [nose_left_y, nose_right_y], color="K") 197 | pyplot.plot([lip_left_x, lip_right_x], [lip_left_y, lip_right_y], color="K") 198 | 199 | subplot.xaxis.set_visible(False) 200 | subplot.yaxis.set_visible(False) 201 | 202 | figure.savefig(output_file, format='pdf', dpi=600, orientation='landscape', bbox_inches="tight") 203 | 204 | ######## 205 | 206 | figure = pyplot.figure() 207 | 208 | subplot = pyplot.subplot(1, 1, 1) 209 | subplot.imshow(g.Z) 210 | 211 | subplot.xaxis.set_visible(False) 212 | subplot.yaxis.set_visible(False) 213 | 214 | figure.savefig("face2.pdf", format='pdf', dpi=600, orientation='landscape', bbox_inches="tight") 215 | 216 | ######## 217 | 218 | figure = pyplot.figure(figsize=(10, 5)) 219 | 220 | subplot = pyplot.subplot(1, 1, 1) 221 | #subplot.plot(K[:, nose_x]) 222 | a, = subplot.plot(H[:, nose_x] * 2) 223 | #subplot.plot(Pmin[:, nose_x]) 224 | b, = subplot.plot(Pmax[:, nose_x]) 225 | c, = subplot.plot(f.Z[:, nose_x] / 50) 226 | 227 | subplot.set_xlabel('Vertical Face Position') 228 | subplot.set_ylabel('Value') 229 | subplot.axvline(nose_y, color=grey) 230 | subplot.axvline(lip_y, color=grey) 231 | subplot.axvline(chin_y, color=grey) 232 | subplot.legend([a, b, c], ["Mean", "Pmax", "Original"]) 233 | 234 | figure.show() 235 | figure.savefig(output_file, format='pdf', dpi=600, orientation='landscape', bbox_inches="tight") 236 | 237 | ########## 238 | figure = pyplot.figure(figsize=(10, 5)) 239 | 240 | subplot = pyplot.subplot(1, 2, 1) 241 | 242 | #a, = subplot.plot(Pmax[(lip_y - 5):(lip_y + 5), :]) 243 | a, = subplot.plot(H[lip_y, :]) 244 | b, = subplot.plot(Pmax[lip_y, :] * 2) 245 | c, = subplot.plot(f.Z[lip_y, :] / 20) 246 | 247 | subplot.set_xlabel('Horizontal Face Position') 248 | subplot.set_ylabel('Value') 249 | subplot.axvline(lip_right_x, color=grey) 250 | subplot.axvline(lip_left_x, color=grey) 251 | subplot.axvline(border_lip_left_x, color=grey) 252 | subplot.axvline(border_lip_right_x, color=grey) 253 | subplot.legend([a, b, c], ["Mean", "Pmax", "Original"]) 254 | 255 | subplot = pyplot.subplot(1, 2, 2) 256 | 257 | a, = subplot.plot(numpy.nansum(H[(lip_y - 5):(lip_y + 5), :], axis=0)) 258 | b, = subplot.plot(numpy.nansum(Pmax[(lip_y - 5):(lip_y + 5), :], axis=0) * 2) 259 | c, = subplot.plot(numpy.nansum(f.Z[(lip_y - 5):(lip_y + 5), :], axis=0) / 100) 260 | 261 | subplot.set_xlabel('Horizontal Face Position (summed)') 262 | subplot.set_ylabel('Value') 263 | subplot.axvline(lip_right_x, color=grey) 264 | subplot.axvline(lip_left_x, color=grey) 265 | subplot.axvline(border_lip_left_x, color=grey) 266 | subplot.axvline(border_lip_right_x, color=grey) 267 | subplot.legend([a, b, c], ["Mean", "Pmax", "Original"]) 268 | 269 | figure.savefig(output_file, format='pdf', dpi=600, orientation='landscape', bbox_inches="tight") 270 | 271 | 272 | def evaluate_rotate(rotations=[-5.0, -2.5, -1.0, 1, 2.5, 5.0], index=4, output_file='rotations.pdf'): 273 | from scipy.ndimage import interpolation 274 | import algorithms 275 | 276 | original = face(index) 277 | other = face(1) 278 | faces = [] 279 | 280 | for rotation in rotations: 281 | f = face(index) 282 | 283 | f.abs_file.data['X'] = interpolation.rotate(f.abs_file.data['X'], rotation, mode='nearest', prefilter=False, reshape=False) 284 | f.abs_file.data['Y'] = interpolation.rotate(f.abs_file.data['Y'], rotation, mode='nearest', prefilter=False, reshape=False) 285 | f.abs_file.data['Z'] = interpolation.rotate(f.abs_file.data['Z'], rotation, mode='nearest', prefilter=False, reshape=False) 286 | 287 | algorithms.features_histogram(f) 288 | faces.append(f) 289 | 290 | pyplot.figure() 291 | 292 | subplot = pyplot.subplot(1, 2+len(rotations), 1) 293 | 294 | subplot.imshow(original.Z) 295 | subplot.title.set_text("Original") 296 | subplot.title.set_fontsize(10) 297 | subplot.xaxis.set_visible(False) 298 | subplot.yaxis.set_visible(False) 299 | 300 | for rotation, f, i in zip(rotations, faces, range(len(rotations))): 301 | subplot = pyplot.subplot(1, 2+len(rotations), 2 + i) 302 | subplot.imshow(f.Z) 303 | subplot.title.set_text("%.1f deg" % rotation) 304 | subplot.title.set_fontsize(10) 305 | subplot.xaxis.set_visible(False) 306 | subplot.yaxis.set_visible(False) 307 | 308 | subplot = pyplot.subplot(1, 2+len(rotations), len(rotations) + 2) 309 | subplot.imshow(other.Z) 310 | subplot.title.set_text("Other") 311 | subplot.title.set_fontsize(10) 312 | subplot.xaxis.set_visible(False) 313 | subplot.yaxis.set_visible(False) 314 | 315 | pyplot.savefig(output_file, format='pdf', dpi=600, orientation='landscape', bbox_inches="tight") 316 | 317 | return algorithms.similarity_matrix([original] + faces + [other], methods=[algorithms.distance_histogram_euclidean, algorithms.distance_histogram_city_block, algorithms.distance_histogram_correlation], normalizers=[False, False, False]) 318 | 319 | def chunks(l, n): 320 | for i in xrange(0, len(l), n): 321 | yield l[i:i+n] 322 | 323 | def raw_moment(data, iord, jord): 324 | nrows, ncols = data.shape 325 | y, x = numpy.mgrid[:nrows, :ncols] 326 | data = data * x**iord * y**jord 327 | 328 | return data.sum() 329 | 330 | def intertial_axis(data): 331 | """Calculate the x-mean, y-mean, and cov matrix of an image.""" 332 | 333 | data_sum = data.sum() 334 | m10 = raw_moment(data, 1, 0) 335 | m01 = raw_moment(data, 0, 1) 336 | x_bar = m10 / data_sum 337 | y_bar = m01 / data_sum 338 | u11 = (raw_moment(data, 1, 1) - x_bar * m01) / data_sum 339 | u20 = (raw_moment(data, 2, 0) - x_bar * m10) / data_sum 340 | u02 = (raw_moment(data, 0, 2) - y_bar * m01) / data_sum 341 | cov = numpy.array([[u20, u11], [u11, u02]]) 342 | 343 | return x_bar, y_bar, cov 344 | 345 | def generate_base_map(faces, func, output_file): 346 | # Make sure there is data 347 | if not faces or len(faces) == 0: 348 | print "Nothing to do" 349 | return 350 | 351 | # Calculate rows and columns 352 | columns = math.ceil(math.sqrt(len(faces))) 353 | rows = ((len(faces) - 1) / columns) + 1; 354 | figure = pyplot.figure() 355 | figure.subplots_adjust(top=0.85) 356 | index = 1 357 | 358 | for (file, person_id, face) in faces: 359 | # Advance to next plot 360 | subplot = figure.add_subplot(columns, rows, index, xticks=[], yticks=[]) 361 | index = index + 1 362 | 363 | # Plot face 364 | func(subplot, file, person_id, face, index) 365 | subplot.title.set_text(person_id) 366 | subplot.title.set_fontsize(10) 367 | subplot.xaxis.set_visible(False) 368 | subplot.yaxis.set_visible(False) 369 | 370 | # Save figure 371 | figure.savefig(output_file, format='pdf', dpi=600, orientation='landscape', bbox_inches="tight") 372 | 373 | def generate_feature_map(faces, output_file): 374 | def _func(subplot, file, person_id, face, index): 375 | subplot.imshow(face.features[1].reshape(face.features[2])) 376 | 377 | generate_base_map(faces, _func, output_file) 378 | 379 | def generate_depth_map(faces, output_file, key_points=False): 380 | def _func(subplot, file, person_id, face, index): 381 | subplot.imshow(face.Z) 382 | 383 | if key_points == True: 384 | for name, point in face.key_points.iteritems(): 385 | x, y = point 386 | subplot.plot(x, y, 'x', color='k') 387 | 388 | generate_base_map(faces, _func, output_file) 389 | 390 | def generate_similarity_matrix(matrix, faces, output_file): 391 | methods, rows, cols = matrix.shape 392 | output = [] 393 | 394 | # Iterate each method 395 | for i in range(methods): 396 | if cols > 0: 397 | table = [] 398 | table.append("

Method %d

%s" % (i, "\n".join([ "" % faces[j][1] for j in range(cols) ]))) 399 | 400 | for j in range(rows): 401 | table.append("%s" % (faces[j][1], "\n".join([ "" % (("%.2f" % matrix[(i, j, k)]) if not numpy.isnan(matrix[(i, j, k)]) else "—") for k in range(cols) ]))) 402 | 403 | table.append("
%s
%s
%s
") 404 | output.append("\n".join(table)) 405 | 406 | # Write table to file 407 | with open(output_file, "w") as f: 408 | f.write(""" 409 | 410 | 411 | Similarity Matrix 412 | 413 | 414 | %s 415 | 416 | 417 | """ % "\n".join(output)) 418 | 419 | def generate_roc_curve(rocs, output_file): 420 | grey = (0.7, 0.7, 0.7, 0.7) 421 | figure = pyplot.figure() 422 | titles = ["Euclidean", "City Block", "Correlation"] 423 | legends = [] 424 | plots = [] 425 | index = 0 426 | 427 | # Draw ROC line 428 | subplot = pyplot.subplot(1, 1, 1) 429 | subplot.plot([0, 1], [1, 0], color=grey) 430 | 431 | # Plot each line 432 | for roc, eer in rocs: 433 | plots.extend(subplot.plot(roc[0], roc[1])) 434 | subplot.plot(eer, 1 - eer, 'x', color='r') 435 | 436 | # Include EER in legend 437 | legends.append("%s (EER=%.2f%%)" % (titles[index], eer * 100)) 438 | index = index + 1 439 | 440 | # Axis and legend 441 | subplot.set_xlabel('False positives rate') 442 | subplot.set_ylabel('True positives rate') 443 | subplot.legend(plots, legends, loc=4) 444 | 445 | # Save figure 446 | figure.savefig(output_file, format='pdf', dpi=600, orientation='landscape', bbox_inches="tight") 447 | 448 | class GeneratorLen(object): 449 | def __init__(self, gen, length): 450 | self.gen = gen 451 | self.length = length 452 | 453 | def __len__(self): 454 | return self.length 455 | 456 | def __iter__(self): 457 | return self.gen 458 | 459 | --------------------------------------------------------------------------------