1
0
Fork 0
mirror of https://github.com/deepfakes/faceswap synced 2025-06-08 11:53:26 -04:00
faceswap/lib/face_filter.py
torzdf 66ed005ef3
Optimize Data Augmentation (#881)
* Move image utils to lib.image
* Add .pylintrc file
* Remove some cv2 pylint ignores
* TrainingData: Load images from disk in batches
* TrainingData: get_landmarks to batch
* TrainingData: transform and flip to batches
* TrainingData: Optimize color augmentation
* TrainingData: Optimize target and random_warp
* TrainingData - Convert _get_closest_match for batching
* TrainingData: Warp To Landmarks optimized
* Save models to threadpoolexecutor
* Move stack_images, Rename ImageManipulation. ImageAugmentation Docstrings
* Masks: Set dtype and threshold for lib.masks based on input face
* Docstrings and Documentation
2019-09-24 12:16:05 +01:00

166 lines
8 KiB
Python

#!/usr/bin python3
""" Face Filterer for extraction in faceswap.py """
import logging
from lib.vgg_face import VGGFace
from lib.image import read_image
from plugins.extract.pipeline import Extractor
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
def avg(arr):
""" Return an average """
return sum(arr) * 1.0 / len(arr)
class FaceFilter():
""" Face filter for extraction
NB: we take only first face, so the reference file should only contain one face. """
def __init__(self, reference_file_paths, nreference_file_paths, detector, aligner,
multiprocess=False, threshold=0.4):
logger.debug("Initializing %s: (reference_file_paths: %s, nreference_file_paths: %s, "
"detector: %s, aligner: %s, multiprocess: %s, threshold: %s)",
self.__class__.__name__, reference_file_paths, nreference_file_paths,
detector, aligner, multiprocess, threshold)
self.vgg_face = VGGFace()
self.filters = self.load_images(reference_file_paths, nreference_file_paths)
# TODO Revert face-filter to use the selected detector and aligner.
# Currently Tensorflow does not release vram after it has been allocated
# Whilst this vram can still be used, the pipeline for the extraction process can't see
# it so thinks there is not enough vram available.
# Either the pipeline will need to be changed to be re-usable by face-filter and extraction
# Or another vram measurement technique will need to be implemented to for when TF has
# already performed allocation. For now we force CPU detectors.
# self.align_faces(detector, aligner, multiprocess)
self.align_faces("cv2-dnn", "cv2-dnn", multiprocess)
self.get_filter_encodings()
self.threshold = threshold
logger.debug("Initialized %s", self.__class__.__name__)
@staticmethod
def load_images(reference_file_paths, nreference_file_paths):
""" Load the images """
retval = dict()
for fpath in reference_file_paths:
retval[fpath] = {"image": read_image(fpath, raise_error=True),
"type": "filter"}
for fpath in nreference_file_paths:
retval[fpath] = {"image": read_image(fpath, raise_error=True),
"type": "nfilter"}
logger.debug("Loaded filter images: %s", {k: v["type"] for k, v in retval.items()})
return retval
# Extraction pipeline
def align_faces(self, detector_name, aligner_name, multiprocess):
""" Use the requested detectors to retrieve landmarks for filter images """
extractor = Extractor(detector_name, aligner_name, multiprocess=multiprocess)
self.run_extractor(extractor)
del extractor
self.load_aligned_face()
def run_extractor(self, extractor):
""" Run extractor to get faces """
for _ in range(extractor.passes):
self.queue_images(extractor)
extractor.launch()
for faces in extractor.detected_faces():
filename = faces["filename"]
detected_faces = faces["detected_faces"]
if len(detected_faces) > 1:
logger.warning("Multiple faces found in %s file: '%s'. Using first detected "
"face.", self.filters[filename]["type"], filename)
self.filters[filename]["detected_face"] = detected_faces[0]
def queue_images(self, extractor):
""" queue images for detection and alignment """
in_queue = extractor.input_queue
for fname, img in self.filters.items():
logger.debug("Adding to filter queue: '%s' (%s)", fname, img["type"])
feed_dict = dict(filename=fname, image=img["image"])
if img.get("detected_faces", None):
feed_dict["detected_faces"] = img["detected_faces"]
logger.debug("Queueing filename: '%s' items: %s",
fname, list(feed_dict.keys()))
in_queue.put(feed_dict)
logger.debug("Sending EOF to filter queue")
in_queue.put("EOF")
def load_aligned_face(self):
""" Align the faces for vgg_face input """
for filename, face in self.filters.items():
logger.debug("Loading aligned face: '%s'", filename)
image = face["image"]
detected_face = face["detected_face"]
detected_face.load_aligned(image, size=224)
face["face"] = detected_face.aligned_face
del face["image"]
logger.debug("Loaded aligned face: ('%s', shape: %s)",
filename, face["face"].shape)
def get_filter_encodings(self):
""" Return filter face encodings from Keras VGG Face """
for filename, face in self.filters.items():
logger.debug("Getting encodings for: '%s'", filename)
encodings = self.vgg_face.predict(face["face"])
logger.debug("Filter Filename: %s, encoding shape: %s", filename, encodings.shape)
face["encoding"] = encodings
del face["face"]
def check(self, detected_face):
""" Check the extracted Face """
logger.trace("Checking face with FaceFilter")
distances = {"filter": list(), "nfilter": list()}
encodings = self.vgg_face.predict(detected_face.aligned_face)
for filt in self.filters.values():
similarity = self.vgg_face.find_cosine_similiarity(filt["encoding"], encodings)
distances[filt["type"]].append(similarity)
avgs = {key: avg(val) if val else None for key, val in distances.items()}
mins = {key: min(val) if val else None for key, val in distances.items()}
# Filter
if distances["filter"] and avgs["filter"] > self.threshold:
msg = "Rejecting filter face: {} > {}".format(round(avgs["filter"], 2), self.threshold)
retval = False
# nFilter no Filter
elif not distances["filter"] and avgs["nfilter"] < self.threshold:
msg = "Rejecting nFilter face: {} < {}".format(round(avgs["nfilter"], 2),
self.threshold)
retval = False
# Filter with nFilter
elif distances["filter"] and distances["nfilter"] and mins["filter"] > mins["nfilter"]:
msg = ("Rejecting face as distance from nfilter sample is smaller: (filter: {}, "
"nfilter: {})".format(round(mins["filter"], 2), round(mins["nfilter"], 2)))
retval = False
elif distances["filter"] and distances["nfilter"] and avgs["filter"] > avgs["nfilter"]:
msg = ("Rejecting face as average distance from nfilter sample is smaller: (filter: "
"{}, nfilter: {})".format(round(mins["filter"], 2), round(mins["nfilter"], 2)))
retval = False
elif distances["filter"] and distances["nfilter"]:
# k-nn classifier
var_k = min(5, min(len(distances["filter"]), len(distances["nfilter"])) + 1)
var_n = sum(list(map(lambda x: x[0],
list(sorted([(1, d) for d in distances["filter"]] +
[(0, d) for d in distances["nfilter"]],
key=lambda x: x[1]))[:var_k])))
ratio = var_n/var_k
if ratio < 0.5:
msg = ("Rejecting face as k-nearest neighbors classification is less than "
"0.5: {}".format(round(ratio, 2)))
retval = False
else:
msg = None
retval = True
else:
msg = None
retval = True
if msg:
logger.verbose(msg)
else:
logger.trace("Accepted face: (similarity: %s, threshold: %s)",
distances, self.threshold)
return retval