Use k-nn for face filtering
diff --git a/lib/FaceFilter.py b/lib/FaceFilter.py
index b8e3089183..b1d711f5b5 100644
--- a/lib/FaceFilter.py
+++ b/lib/FaceFilter.py
@@ -3,18 +3,50 @@
 import face_recognition
 # import face_recognition_models
 
+def avg(arr):
+  return sum(arr)*1.0/len(arr)
+
 class FaceFilter():
-    def __init__(self, reference_file_path, threshold = 0.6):
-        image = face_recognition.load_image_file(reference_file_path)
-        self.encoding = face_recognition.face_encodings(image)[0] # Note: we take only first face, so the reference file should only contain one face.
+    def __init__(self, reference_file_paths, nreference_file_paths, threshold = 0.6):
+        images = list(map(face_recognition.load_image_file, reference_file_paths))
+        nimages = list(map(face_recognition.load_image_file, nreference_file_paths))
+        # Note: we take only first face, so the reference file should only contain one face.
+        self.encodings = list(map(lambda im: face_recognition.face_encodings(im)[0], images))
+        self.nencodings = list(map(lambda im: face_recognition.face_encodings(im)[0], nimages))
         self.threshold = threshold
     
     def check(self, detected_face):
-        encodings = face_recognition.face_encodings(detected_face.image) # we could use detected landmarks, but I did not manage to do so. TODO The copy/paste below should help
+        # we could use detected landmarks, but I did not manage to do so. TODO The copy/paste below should help
+        encodings = face_recognition.face_encodings(detected_face.image)
         if encodings is not None and len(encodings) > 0:
-            score = face_recognition.face_distance([self.encoding], encodings[0])
-            print(score)
-            return score <= self.threshold
+            distances = list(face_recognition.face_distance(self.encodings, encodings[0]))
+            distance = avg(distances)
+            mindistance = min(distances)
+            maxdistance = max(distances)
+            if distance > self.threshold:
+                print("Distance above threshold: %f < %f" % (distance, self.threshold))
+                return False
+            if len(self.nencodings) > 0:
+              ndistances = list(face_recognition.face_distance(self.nencodings, encodings[0]))
+              ndistance = avg(ndistances)
+              nmindistance = min(ndistances)
+              nmaxdistance = max(ndistances)
+              if (mindistance > nmindistance):
+                  print("Distance to negative sample is smaller")
+                  return False
+              if (distance > ndistance):
+                  print("Average distance to negative sample is smaller")
+                  return False
+              # k-nn classifier
+              K=min(5, min(len(distances), len(ndistances)) + 1)
+              N=sum(list(map(lambda x: x[0],
+                    list(sorted([(1,d) for d in distances] + [(0,d) for d in ndistances],
+                                key=lambda x: x[1]))[:K])))
+              ratio = N/K
+              if (ratio < 0.5):
+                  print("K-nn is %.2f" % ratio)
+                  return False
+            return True
         else:
             print("No face encodings found")
             return False
diff --git a/lib/cli.py b/lib/cli.py
index b4b615a0fc..6b35676b22 100644
--- a/lib/cli.py
+++ b/lib/cli.py
@@ -40,6 +40,7 @@ def process_arguments(self, arguments):
         self.arguments = arguments
         print("Input Directory: {}".format(self.arguments.input_dir))
         print("Output Directory: {}".format(self.arguments.output_dir))
+        print("Filter: {}".format(self.arguments.filter))
         self.serializer = None
         if self.arguments.serializer is None and self.arguments.alignments_path is not None:
             ext = os.path.splitext(self.arguments.alignments_path)[-1]
@@ -159,10 +160,19 @@ def get_faces(self, image):
             self.verify_output = True
 
     def load_filter(self):
-        filter_file = self.arguments.filter
-        if Path(filter_file).exists():
-            print('Loading reference image for filtering')
-            return FaceFilter(filter_file)
+        nfilter_files = self.arguments.nfilter
+        if not isinstance(self.arguments.nfilter, list):
+            nfilter_files = [self.arguments.nfilter]
+        nfilter_files = list(filter(lambda fn: Path(fn).exists(), nfilter_files))
+
+        filter_files = self.arguments.filter
+        if not isinstance(self.arguments.filter, list):
+            filter_files = [self.arguments.filter]
+        filter_files = list(filter(lambda fn: Path(fn).exists(), filter_files))
+        
+        if filter_files:
+            print('Loading reference images for filtering: %s' % filter_files)
+            return FaceFilter(filter_files, nfilter_files, self.arguments.ref_threshold)
 
     # for now, we limit this class responsability to the read of files. images and faces are processed outside this class
     def process(self):
diff --git a/scripts/convert.py b/scripts/convert.py
index 6cc3aa4df0..97b2a8e63b 100644
--- a/scripts/convert.py
+++ b/scripts/convert.py
@@ -67,11 +67,27 @@ def add_optional_arguments(self, parser):
                             help="When used with --frame-ranges discards frames that are not processed instead of writing them out unchanged."
                             )
 
+        parser.add_argument('-l', '--ref_threshold',
+                            type=float,
+                            dest="ref_threshold",
+                            default=0.6,
+                            help="Threshold for positive face recognition"
+                            )
+
+        parser.add_argument('-n', '--nfilter',
+                            type=str,
+                            dest="nfilter",
+                            nargs='+',
+                            default="nfilter.jpg",
+                            help="Reference image for the persons you do not want to process. Should be a front portrait"
+                            )
+
         parser.add_argument('-f', '--filter',
                             type=str,
                             dest="filter",
+                            nargs="+",
                             default="filter.jpg",
-                            help="Reference image for the person you want to process. Should be a front portrait"
+                            help="Reference images for the person you want to process. Should be a front portrait"
                             )
 
         parser.add_argument('-b', '--blur-size',
diff --git a/scripts/extract.py b/scripts/extract.py
index d190fbbe93..561fc45cb8 100644
--- a/scripts/extract.py
+++ b/scripts/extract.py
@@ -26,9 +26,25 @@ def add_optional_arguments(self, parser):
                             default="hog",
                             help="Detector to use. 'cnn' detects much more angles but will be much more resource intensive and may fail on large files.")
 
+        parser.add_argument('-l', '--ref_threshold',
+                            type=float,
+                            dest="ref_threshold",
+                            default=0.6,
+                            help="Threshold for positive face recognition"
+                            )
+
+        parser.add_argument('-n', '--nfilter',
+                            type=str,
+                            dest="nfilter",
+                            nargs='+',
+                            default="nfilter.jpg",
+                            help="Reference image for the persons you do not want to process. Should be a front portrait"
+                            )
+
         parser.add_argument('-f', '--filter',
                             type=str,
                             dest="filter",
+                            nargs='+',
                             default="filter.jpg",
                             help="Reference image for the person you want to process. Should be a front portrait"
                             )
@@ -63,12 +79,13 @@ def process(self):
                     self.num_faces_detected += 1
                     self.faces_detected[os.path.basename(filename)] = faces
             else:
-                try:
-                    for filename in tqdm(self.read_directory()):
+                for filename in tqdm(self.read_directory()):
+                    try:
                         image = cv2.imread(filename)
                         self.faces_detected[os.path.basename(filename)] = self.handleImage(image, filename)
-                except Exception as e:
-                    print('Failed to extract from image: {}. Reason: {}'.format(filename, e))
+                    except Exception as e:
+                        print('Failed to extract from image: {}. Reason: {}'.format(filename, e))
+                        pass
         finally:
             self.write_alignments()
 
@@ -78,6 +95,8 @@ def processFiles(self, filename):
             return filename, self.handleImage(image, filename)
         except Exception as e:
             print('Failed to extract from image: {}. Reason: {}'.format(filename, e))
+            pass
+        return filename, []
 
     def handleImage(self, image, filename):
         count = 0

The current implementation of face detection uses a threshold for determining if a face matches the reference image. This commit allows the user to add multiple reference images and multiple negative reference images and then uses the old threshold in addition to K-nearest neighbours method to determine if some negative reference is more similar. This is useful if we have a video of multiple actors that look very similar.