mirror of
https://github.com/deepfakes/faceswap
synced 2025-06-07 10:43:27 -04:00
* Remove tensorflow_probability requirement * setup.py - fix progress bars * requirements.txt: Remove pre python 3.9 packages * update apple requirements.txt * update INSTALL.md * Remove python<3.9 code * setup.py - fix Windows Installer * typing: python3.9 compliant * Update pytest and readthedocs python versions * typing fixes * Python Version updates - Reduce max version to 3.10 - Default to 3.10 in installers - Remove incompatible 3.11 tests * Update dependencies * Downgrade imageio dep for Windows * typing: merge optional unions and fixes * Updates - min python version 3.10 - typing to python 3.10 spec - remove pre-tf2.10 code - Add conda tests * train: re-enable optimizer saving * Update dockerfiles * Update setup.py - Apple Conda deps to setup.py - Better Cuda + dependency handling * bugfix: Patch logging to prevent Autograph errors * Update dockerfiles * Setup.py - Setup.py - stdout to utf-8 * Add more OSes to github Actions * suppress mac-os end to end test
314 lines
11 KiB
Python
314 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
""" CV2 DNN landmarks extractor for faceswap.py
|
|
Adapted from: https://github.com/yinguobing/cnn-facial-landmark
|
|
MIT License
|
|
|
|
Copyright (c) 2017 Yin Guobing
|
|
|
|
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.
|
|
"""
|
|
from __future__ import annotations
|
|
import logging
|
|
import typing as T
|
|
|
|
import cv2
|
|
import numpy as np
|
|
|
|
from ._base import Aligner, AlignerBatch, BatchType
|
|
|
|
if T.TYPE_CHECKING:
|
|
from lib.align.detected_face import DetectedFace
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class Align(Aligner):
|
|
""" Perform transformation to align and get landmarks """
|
|
def __init__(self, **kwargs) -> None:
|
|
git_model_id = 1
|
|
model_filename = "cnn-facial-landmark_v1.pb"
|
|
super().__init__(git_model_id=git_model_id, model_filename=model_filename, **kwargs)
|
|
|
|
self.model: cv2.dnn.Net
|
|
self.name = "cv2-DNN Aligner"
|
|
self.input_size = 128
|
|
self.color_format = "RGB"
|
|
self.vram = 0 # Doesn't use GPU
|
|
self.vram_per_batch = 0
|
|
self.batchsize = 1
|
|
self.realign_centering = "legacy"
|
|
|
|
def init_model(self) -> None:
|
|
""" Initialize CV2 DNN Detector Model"""
|
|
self.model = cv2.dnn.readNetFromTensorflow(self.model_path) # pylint: disable=no-member
|
|
self.model.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU) # pylint: disable=no-member
|
|
|
|
def faces_to_feed(self, faces: np.ndarray) -> np.ndarray:
|
|
""" Convert a batch of face images from UINT8 (0-255) to fp32 (0.0-255.0)
|
|
|
|
Parameters
|
|
----------
|
|
faces: :class:`numpy.ndarray`
|
|
The batch of faces in UINT8 format
|
|
|
|
Returns
|
|
-------
|
|
class: `numpy.ndarray`
|
|
The batch of faces as fp32
|
|
"""
|
|
return faces.astype("float32").transpose((0, 3, 1, 2))
|
|
|
|
def process_input(self, batch: BatchType) -> None:
|
|
""" Compile the detected faces for prediction
|
|
|
|
Parameters
|
|
----------
|
|
batch: :class:`AlignerBatch`
|
|
The current batch to process input for
|
|
|
|
Returns
|
|
-------
|
|
:class:`AlignerBatch`
|
|
The batch item with the :attr:`feed` populated and any required :attr:`data` added
|
|
"""
|
|
assert isinstance(batch, AlignerBatch)
|
|
lfaces, roi, offsets = self.align_image(batch)
|
|
batch.feed = np.array(lfaces)[..., :3]
|
|
batch.data.append({"roi": roi, "offsets": offsets})
|
|
|
|
def _get_box_and_offset(self, face: DetectedFace) -> tuple[list[int], int]:
|
|
"""Obtain the bounding box and offset from a detected face.
|
|
|
|
|
|
Parameters
|
|
----------
|
|
face: :class:`~lib.align.DetectedFace`
|
|
The detected face object to obtain the bounding box and offset from
|
|
|
|
Returns
|
|
-------
|
|
box: list
|
|
The [left, top, right, bottom] bounding box
|
|
offset: int
|
|
The offset of the box (difference between half width vs height)
|
|
"""
|
|
|
|
box = T.cast(list[int], [face.left,
|
|
face.top,
|
|
face.right,
|
|
face.bottom])
|
|
diff_height_width = T.cast(int, face.height) - T.cast(int, face.width)
|
|
offset = int(abs(diff_height_width / 2))
|
|
return box, offset
|
|
|
|
def align_image(self, batch: AlignerBatch) -> tuple[list[np.ndarray],
|
|
list[list[int]],
|
|
list[tuple[int, int]]]:
|
|
""" Align the incoming image for prediction
|
|
|
|
Parameters
|
|
----------
|
|
batch: :class:`AlignerBatch`
|
|
The current batch to align the input for
|
|
|
|
Returns
|
|
-------
|
|
faces: list
|
|
List of feed faces for the aligner
|
|
rois: list
|
|
List of roi's for the faces
|
|
offsets: list
|
|
List of offsets for the faces
|
|
"""
|
|
logger.trace("Aligning image around center") # type:ignore
|
|
sizes = (self.input_size, self.input_size)
|
|
rois = []
|
|
faces = []
|
|
offsets = []
|
|
for det_face, image in zip(batch.detected_faces, batch.image):
|
|
box, offset_y = self._get_box_and_offset(det_face)
|
|
box_moved = self.move_box(box, (0, offset_y))
|
|
# Make box square.
|
|
roi = self.get_square_box(box_moved)
|
|
|
|
# Pad the image and adjust roi if face is outside of boundaries
|
|
image, offset = self.pad_image(roi, image)
|
|
face = image[roi[1] + abs(offset[1]): roi[3] + abs(offset[1]),
|
|
roi[0] + abs(offset[0]): roi[2] + abs(offset[0])]
|
|
interpolation = cv2.INTER_CUBIC if face.shape[0] < self.input_size else cv2.INTER_AREA
|
|
face = cv2.resize(face, dsize=sizes, interpolation=interpolation)
|
|
faces.append(face)
|
|
rois.append(roi)
|
|
offsets.append(offset)
|
|
return faces, rois, offsets
|
|
|
|
@classmethod
|
|
def move_box(cls,
|
|
box: list[int],
|
|
offset: tuple[int, int]) -> list[int]:
|
|
"""Move the box to direction specified by vector offset
|
|
|
|
Parameters
|
|
----------
|
|
box: list
|
|
The (`left`, `top`, `right`, `bottom`) box positions
|
|
offset: tuple
|
|
(x, y) offset to move the box
|
|
|
|
Returns
|
|
-------
|
|
list
|
|
The original box shifted by the offset
|
|
"""
|
|
left = box[0] + offset[0]
|
|
top = box[1] + offset[1]
|
|
right = box[2] + offset[0]
|
|
bottom = box[3] + offset[1]
|
|
return [left, top, right, bottom]
|
|
|
|
@staticmethod
|
|
def get_square_box(box: list[int]) -> list[int]:
|
|
"""Get a square box out of the given box, by expanding it.
|
|
|
|
Parameters
|
|
----------
|
|
box: list
|
|
The (`left`, `top`, `right`, `bottom`) box positions
|
|
|
|
Returns
|
|
-------
|
|
list
|
|
The original box but made square
|
|
"""
|
|
left = box[0]
|
|
top = box[1]
|
|
right = box[2]
|
|
bottom = box[3]
|
|
|
|
box_width = right - left
|
|
box_height = bottom - top
|
|
|
|
# Check if box is already a square. If not, make it a square.
|
|
diff = box_height - box_width
|
|
delta = int(abs(diff) / 2)
|
|
|
|
if diff == 0: # Already a square.
|
|
return box
|
|
if diff > 0: # Height > width, a slim box.
|
|
left -= delta
|
|
right += delta
|
|
if diff % 2 == 1:
|
|
right += 1
|
|
else: # Width > height, a short box.
|
|
top -= delta
|
|
bottom += delta
|
|
if diff % 2 == 1:
|
|
bottom += 1
|
|
|
|
# Make sure box is always square.
|
|
assert ((right - left) == (bottom - top)), 'Box is not square.'
|
|
|
|
return [left, top, right, bottom]
|
|
|
|
@classmethod
|
|
def pad_image(cls, box: list[int], image: np.ndarray) -> tuple[np.ndarray, tuple[int, int]]:
|
|
"""Pad image if face-box falls outside of boundaries
|
|
|
|
Parameters
|
|
----------
|
|
box: list
|
|
The (`left`, `top`, `right`, `bottom`) roi box positions
|
|
image: :class:`numpy.ndarray`
|
|
The image to be padded
|
|
|
|
Returns
|
|
-------
|
|
:class:`numpy.ndarray`
|
|
The padded image
|
|
"""
|
|
height, width = image.shape[:2]
|
|
pad_l = 1 - box[0] if box[0] < 0 else 0
|
|
pad_t = 1 - box[1] if box[1] < 0 else 0
|
|
pad_r = box[2] - width if box[2] > width else 0
|
|
pad_b = box[3] - height if box[3] > height else 0
|
|
logger.trace("Padding: (l: %s, t: %s, r: %s, b: %s)", # type:ignore
|
|
pad_l, pad_t, pad_r, pad_b)
|
|
padded_image = cv2.copyMakeBorder(image.copy(),
|
|
pad_t,
|
|
pad_b,
|
|
pad_l,
|
|
pad_r,
|
|
cv2.BORDER_CONSTANT,
|
|
value=(0, 0, 0))
|
|
offsets = (pad_l - pad_r, pad_t - pad_b)
|
|
logger.trace("image_shape: %s, Padded shape: %s, box: %s, offsets: %s", # type:ignore
|
|
image.shape, padded_image.shape, box, offsets)
|
|
return padded_image, offsets
|
|
|
|
def predict(self, feed: np.ndarray) -> np.ndarray:
|
|
""" Predict the 68 point landmarks
|
|
|
|
Parameters
|
|
----------
|
|
feed: :class:`numpy.ndarray`
|
|
The batch to feed into the aligner
|
|
|
|
Returns
|
|
-------
|
|
:class:`numpy.ndarray`
|
|
The predictions from the aligner
|
|
"""
|
|
assert isinstance(self.model, cv2.dnn.Net)
|
|
self.model.setInput(feed)
|
|
retval = self.model.forward()
|
|
return retval
|
|
|
|
def process_output(self, batch: BatchType) -> None:
|
|
""" Process the output from the model
|
|
|
|
Parameters
|
|
----------
|
|
batch: :class:`AlignerBatch`
|
|
The current batch from the model with :attr:`predictions` populated
|
|
"""
|
|
assert isinstance(batch, AlignerBatch)
|
|
self.get_pts_from_predict(batch)
|
|
|
|
def get_pts_from_predict(self, batch: AlignerBatch):
|
|
""" Get points from predictor and populates the :attr:`landmarks` property
|
|
|
|
Parameters
|
|
----------
|
|
batch: :class:`AlignerBatch`
|
|
The current batch from the model with :attr:`predictions` populated
|
|
"""
|
|
landmarks = []
|
|
if batch.second_pass:
|
|
batch.landmarks = batch.prediction.reshape(self.batchsize, -1, 2) * self.input_size
|
|
else:
|
|
for prediction, roi, offset in zip(batch.prediction,
|
|
batch.data[0]["roi"],
|
|
batch.data[0]["offsets"]):
|
|
points = np.reshape(prediction, (-1, 2))
|
|
points *= (roi[2] - roi[0])
|
|
points[:, 0] += (roi[0] - offset[0])
|
|
points[:, 1] += (roi[1] - offset[1])
|
|
landmarks.append(points)
|
|
batch.landmarks = np.array(landmarks)
|
|
logger.trace("Predicted Landmarks: %s", batch.landmarks) # type:ignore
|