mirror of
https://github.com/deepfakes/faceswap.git
synced 2026-10-23 10:22:29 +08:00
252 lines
9.8 KiB
Python
252 lines
9.8 KiB
Python
#!/usr/bin/env python3
|
|
""" Base class for Face Aligner plugins
|
|
Plugins should inherit from this class
|
|
|
|
See the override methods for which methods are
|
|
required.
|
|
|
|
The plugin will receive a dict containing:
|
|
{"filename": <filename of source frame>,
|
|
"image": <source image>,
|
|
"detected_faces": <list of bounding box dicts as defined in lib/plugins/extract/detect/_base>}
|
|
|
|
For each source item, the plugin must pass a dict to finalize containing:
|
|
{"filename": <filename of source frame>,
|
|
"image": <source image>,
|
|
"detected_faces": <list of bounding box dicts as defined in lib/plugins/extract/detect/_base>,
|
|
"landmarks": <list of landmarks>}
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
import traceback
|
|
|
|
from io import StringIO
|
|
|
|
import cv2
|
|
|
|
from lib.aligner import Extract
|
|
from lib.gpu_stats import GPUStats
|
|
from lib.utils import GetModel
|
|
|
|
logger = logging.getLogger(__name__) # pylint:disable=invalid-name
|
|
|
|
|
|
class Aligner():
|
|
""" Landmarks Aligner Object """
|
|
def __init__(self, loglevel, configfile=None, normalize_method=None,
|
|
git_model_id=None, model_filename=None, colorspace="BGR", input_size=256):
|
|
logger.debug("Initializing %s: (loglevel: %s, configfile: %s, normalize_method: %s, "
|
|
"git_model_id: %s, model_filename: '%s', colorspace: '%s'. input_size: %s)",
|
|
self.__class__.__name__, loglevel, configfile, normalize_method, git_model_id,
|
|
model_filename, colorspace, input_size)
|
|
self.loglevel = loglevel
|
|
self.normalize_method = normalize_method
|
|
self.colorspace = colorspace.upper()
|
|
self.input_size = input_size
|
|
self.extract = Extract()
|
|
self.init = None
|
|
self.error = None
|
|
|
|
# The input and output queues for the plugin.
|
|
# See lib.queue_manager.QueueManager for getting queues
|
|
self.queues = {"in": None, "out": None}
|
|
|
|
# Get model if required
|
|
self.model_path = self.get_model(git_model_id, model_filename)
|
|
|
|
# Approximate VRAM required for aligner. Used to calculate
|
|
# how many parallel processes / batches can be run.
|
|
# Be conservative to avoid OOM.
|
|
self.vram = None
|
|
|
|
# Set to true if the plugin supports PlaidML
|
|
self.supports_plaidml = False
|
|
|
|
logger.debug("Initialized %s", self.__class__.__name__)
|
|
|
|
# <<< OVERRIDE METHODS >>> #
|
|
# These methods must be overriden when creating a plugin
|
|
def initialize(self, *args, **kwargs):
|
|
""" Inititalize the aligner
|
|
Tasks to be run before any alignments are performed.
|
|
Override for specific detector """
|
|
logger.debug("_base initialize %s: (PID: %s, args: %s, kwargs: %s)",
|
|
self.__class__.__name__, os.getpid(), args, kwargs)
|
|
self.init = kwargs["event"]
|
|
self.error = kwargs["error"]
|
|
self.queues["in"] = kwargs["in_queue"]
|
|
self.queues["out"] = kwargs["out_queue"]
|
|
|
|
def align_image(self, detected_face, image):
|
|
""" Align the incoming image for feeding into aligner
|
|
Override for aligner specific processing """
|
|
raise NotImplementedError
|
|
|
|
def predict_landmarks(self, feed_dict):
|
|
""" Predict the 68 point landmarks
|
|
Override for aligner specific landmark prediction """
|
|
raise NotImplementedError
|
|
|
|
# <<< GET MODEL >>> #
|
|
@staticmethod
|
|
def get_model(git_model_id, model_filename):
|
|
""" Check if model is available, if not, download and unzip it """
|
|
if model_filename is None:
|
|
logger.debug("No model_filename specified. Returning None")
|
|
return None
|
|
if git_model_id is None:
|
|
logger.debug("No git_model_id specified. Returning None")
|
|
return None
|
|
cache_path = os.path.join(os.path.dirname(__file__), ".cache")
|
|
model = GetModel(model_filename, cache_path, git_model_id)
|
|
return model.model_path
|
|
|
|
# <<< ALIGNMENT WRAPPER >>> #
|
|
def run(self, *args, **kwargs):
|
|
""" Parent align process.
|
|
This should always be called as the entry point so exceptions
|
|
are passed back to parent.
|
|
Do not override """
|
|
try:
|
|
self.align(*args, **kwargs)
|
|
except Exception: # pylint:disable=broad-except
|
|
logger.error("Caught exception in child process: %s", os.getpid())
|
|
# Display traceback if in initialization stage
|
|
if not self.init.is_set():
|
|
logger.exception("Traceback:")
|
|
tb_buffer = StringIO()
|
|
traceback.print_exc(file=tb_buffer)
|
|
exception = {"exception": (os.getpid(), tb_buffer)}
|
|
self.queues["out"].put(exception)
|
|
exit(1)
|
|
|
|
def align(self, *args, **kwargs):
|
|
""" Process landmarks """
|
|
if not self.init:
|
|
self.initialize(*args, **kwargs)
|
|
logger.debug("Launching Align: (args: %s kwargs: %s)", args, kwargs)
|
|
|
|
for item in self.get_item():
|
|
if item == "EOF":
|
|
self.finalize(item)
|
|
break
|
|
image = self.convert_color(item["image"])
|
|
|
|
logger.trace("Aligning faces")
|
|
try:
|
|
item["landmarks"] = self.process_landmarks(image, item["detected_faces"])
|
|
logger.trace("Aligned faces: %s", item["landmarks"])
|
|
except ValueError as err:
|
|
logger.warning("Image '%s' could not be processed. This may be due to corrupted "
|
|
"data: %s", item["filename"], str(err))
|
|
item["detected_faces"] = list()
|
|
item["landmarks"] = list()
|
|
# UNCOMMENT THIS CODE BLOCK TO PRINT TRACEBACK ERRORS
|
|
# import sys
|
|
# exc_info = sys.exc_info()
|
|
# traceback.print_exception(*exc_info)
|
|
self.finalize(item)
|
|
logger.debug("Completed Align")
|
|
|
|
def convert_color(self, image):
|
|
""" Convert the image to the correct colorspace """
|
|
logger.trace("Converting image to colorspace: %s", self.colorspace)
|
|
if self.colorspace == "RGB":
|
|
cvt_image = image[:, :, ::-1].copy()
|
|
elif self.colorspace == "GRAY":
|
|
cvt_image = cv2.cvtColor(image.copy(), cv2.COLOR_BGR2GRAY) # pylint:disable=no-member
|
|
else:
|
|
cvt_image = image.copy()
|
|
return cvt_image
|
|
|
|
def process_landmarks(self, image, detected_faces):
|
|
""" Align image and process landmarks """
|
|
logger.trace("Processing landmarks")
|
|
retval = list()
|
|
for detected_face in detected_faces:
|
|
feed_dict = self.align_image(detected_face, image)
|
|
self.normalize_face(feed_dict)
|
|
landmarks = self.predict_landmarks(feed_dict)
|
|
retval.append(landmarks)
|
|
logger.trace("Processed landmarks: %s", retval)
|
|
return retval
|
|
|
|
# <<< FACE NORMALIZATION METHODS >>> #
|
|
def normalize_face(self, feed_dict):
|
|
""" Normalize the face for feeding into model """
|
|
if self.normalize_method is None:
|
|
return
|
|
logger.trace("Normalizing face")
|
|
meth = getattr(self, "normalize_{}".format(self.normalize_method.lower()))
|
|
feed_dict["image"] = meth(feed_dict["image"])
|
|
logger.trace("Normalized face")
|
|
|
|
@staticmethod
|
|
def normalize_mean(face):
|
|
""" Normalize Face to the Mean """
|
|
face = face / 255.0
|
|
for chan in range(3):
|
|
layer = face[:, :, chan]
|
|
layer = (layer - layer.min()) / (layer.max() - layer.min())
|
|
face[:, :, chan] = layer
|
|
return face * 255.0
|
|
|
|
@staticmethod
|
|
def normalize_hist(face):
|
|
""" Equalize the RGB histogram channels """
|
|
for chan in range(3):
|
|
face[:, :, chan] = cv2.equalizeHist(face[:, :, chan]) # pylint: disable=no-member
|
|
return face
|
|
|
|
@staticmethod
|
|
def normalize_clahe(face):
|
|
""" Perform Contrast Limited Adaptive Histogram Equalization """
|
|
clahe = cv2.createCLAHE(clipLimit=2.0, # pylint: disable=no-member
|
|
tileGridSize=(4, 4))
|
|
for chan in range(3):
|
|
face[:, :, chan] = clahe.apply(face[:, :, chan])
|
|
return face
|
|
|
|
# <<< FINALIZE METHODS >>> #
|
|
def finalize(self, output):
|
|
""" This should be called as the final task of each plugin
|
|
aligns faces and puts to the out queue """
|
|
if output == "EOF":
|
|
logger.trace("Item out: %s", output)
|
|
self.queues["out"].put("EOF")
|
|
return
|
|
logger.trace("Item out: %s", {key: val
|
|
for key, val in output.items()
|
|
if key != "image"})
|
|
self.queues["out"].put((output))
|
|
|
|
# <<< MISC METHODS >>> #
|
|
def get_vram_free(self):
|
|
""" Return free and total VRAM on card with most VRAM free"""
|
|
stats = GPUStats()
|
|
vram = stats.get_card_most_free(supports_plaidml=self.supports_plaidml)
|
|
logger.verbose("Using device %s with %sMB free of %sMB",
|
|
vram["device"],
|
|
int(vram["free"]),
|
|
int(vram["total"]))
|
|
return int(vram["card_id"]), int(vram["free"]), int(vram["total"])
|
|
|
|
def get_item(self):
|
|
""" Yield one item from the queue """
|
|
while True:
|
|
item = self.queues["in"].get()
|
|
if isinstance(item, dict):
|
|
logger.trace("Item in: %s", {key: val
|
|
for key, val in item.items()
|
|
if key != "image"})
|
|
# Pass Detector failures straight out and quit
|
|
if item.get("exception", None):
|
|
self.queues["out"].put(item)
|
|
exit(1)
|
|
else:
|
|
logger.trace("Item in: %s", item)
|
|
yield item
|
|
if item == "EOF":
|
|
break
|