mirror of
https://github.com/deepfakes/faceswap
synced 2025-06-07 10:43:27 -04:00
lib.align.detected_face: Split Mask objects to own aligned_mask module
This commit is contained in:
parent
2bad105dc8
commit
96528ee3e8
6 changed files with 643 additions and 610 deletions
|
@ -30,6 +30,27 @@ Handles aligned faces and corresponding pose estimates
|
|||
:show-inheritance:
|
||||
|
||||
|
||||
aligned\_mask module
|
||||
====================
|
||||
|
||||
Handles aligned storage and retrieval of Faceswap generated masks
|
||||
|
||||
.. rubric:: Module Summary
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
~lib.align.aligned_mask.BlurMask
|
||||
~lib.align.aligned_mask.Mask
|
||||
|
||||
.. rubric:: Module
|
||||
|
||||
.. automodule:: lib.align.aligned_mask
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
alignments module
|
||||
=================
|
||||
|
||||
|
@ -71,9 +92,7 @@ Handles detected face objects and their associated masks.
|
|||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
~lib.align.detected_face.BlurMask
|
||||
~lib.align.detected_face.DetectedFace
|
||||
~lib.align.detected_face.Mask
|
||||
~lib.align.detected_face.update_legacy_png_header
|
||||
|
||||
.. rubric:: Module
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
associated objects. """
|
||||
from .aligned_face import (AlignedFace, get_adjusted_center, get_matrix_scaling,
|
||||
get_centered_size, transform_image)
|
||||
from .aligned_mask import BlurMask, LandmarksMask, Mask
|
||||
from .alignments import Alignments
|
||||
from .constants import CenteringType, EXTRACT_RATIOS, LANDMARK_PARTS, LandmarkType
|
||||
from .detected_face import BlurMask, DetectedFace, Mask, update_legacy_png_header
|
||||
from .detected_face import DetectedFace, update_legacy_png_header
|
||||
|
|
599
lib/align/aligned_mask.py
Normal file
599
lib/align/aligned_mask.py
Normal file
|
@ -0,0 +1,599 @@
|
|||
#!/usr/bin python3
|
||||
""" Handles retrieval and storage of Faceswap aligned masks """
|
||||
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
import typing as T
|
||||
|
||||
from zlib import compress, decompress
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from lib.logger import parse_class_init
|
||||
|
||||
from .alignments import MaskAlignmentsFileDict
|
||||
from . import get_adjusted_center, get_centered_size
|
||||
|
||||
if T.TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
from .aligned_face import CenteringType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Mask():
|
||||
""" Face Mask information and convenience methods
|
||||
|
||||
Holds a Faceswap mask as generated from :mod:`plugins.extract.mask` and the information
|
||||
required to transform it to its original frame.
|
||||
|
||||
Holds convenience methods to handle the warping, storing and retrieval of the mask.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
storage_size: int, optional
|
||||
The size (in pixels) that the mask should be stored at. Default: 128.
|
||||
storage_centering, str (optional):
|
||||
The centering to store the mask at. One of `"legacy"`, `"face"`, `"head"`.
|
||||
Default: `"face"`
|
||||
|
||||
Attributes
|
||||
----------
|
||||
stored_size: int
|
||||
The size, in pixels, of the stored mask across its height and width.
|
||||
stored_centering: str
|
||||
The centering that the mask is stored at. One of `"legacy"`, `"face"`, `"head"`
|
||||
"""
|
||||
def __init__(self,
|
||||
storage_size: int = 128,
|
||||
storage_centering: CenteringType = "face") -> None:
|
||||
logger.trace(parse_class_init(locals())) # type:ignore[attr-defined]
|
||||
self.stored_size = storage_size
|
||||
self.stored_centering = storage_centering
|
||||
|
||||
self._mask: bytes | None = None
|
||||
self._affine_matrix: np.ndarray | None = None
|
||||
self._interpolator: int | None = None
|
||||
|
||||
self._blur_type: T.Literal["gaussian", "normalized"] | None = None
|
||||
self._blur_passes: int = 0
|
||||
self._blur_kernel: float | int = 0
|
||||
self._threshold = 0.0
|
||||
self._dilation: tuple[T.Literal["erode", "dilate"], np.ndarray | None] = ("erode", None)
|
||||
self._sub_crop_size = 0
|
||||
self._sub_crop_slices: dict[T.Literal["in", "out"], list[slice]] = {}
|
||||
|
||||
self.set_blur_and_threshold()
|
||||
logger.trace("Initialized: %s", self.__class__.__name__) # type:ignore[attr-defined]
|
||||
|
||||
@property
|
||||
def mask(self) -> np.ndarray:
|
||||
""" :class:`numpy.ndarray`: The mask at the size of :attr:`stored_size` with any requested
|
||||
blurring, threshold amount and centering applied."""
|
||||
mask = self.stored_mask
|
||||
if self._dilation[-1] is not None or self._threshold != 0.0 or self._blur_kernel != 0:
|
||||
mask = mask.copy()
|
||||
self._dilate_mask(mask)
|
||||
if self._threshold != 0.0:
|
||||
mask[mask < self._threshold] = 0.0
|
||||
mask[mask > 255.0 - self._threshold] = 255.0
|
||||
if self._blur_kernel != 0 and self._blur_type is not None:
|
||||
mask = BlurMask(self._blur_type,
|
||||
mask,
|
||||
self._blur_kernel,
|
||||
passes=self._blur_passes).blurred
|
||||
if self._sub_crop_size: # Crop the mask to the given centering
|
||||
out = np.zeros((self._sub_crop_size, self._sub_crop_size, 1), dtype=mask.dtype)
|
||||
slice_in, slice_out = self._sub_crop_slices["in"], self._sub_crop_slices["out"]
|
||||
out[slice_out[0], slice_out[1], :] = mask[slice_in[0], slice_in[1], :]
|
||||
mask = out
|
||||
logger.trace("mask shape: %s", mask.shape) # type:ignore[attr-defined]
|
||||
return mask
|
||||
|
||||
@property
|
||||
def stored_mask(self) -> np.ndarray:
|
||||
""" :class:`numpy.ndarray`: The mask at the size of :attr:`stored_size` as it is stored
|
||||
(i.e. with no blurring/centering applied). """
|
||||
assert self._mask is not None
|
||||
dims = (self.stored_size, self.stored_size, 1)
|
||||
mask = np.frombuffer(decompress(self._mask), dtype="uint8").reshape(dims)
|
||||
logger.trace("stored mask shape: %s", mask.shape) # type:ignore[attr-defined]
|
||||
return mask
|
||||
|
||||
@property
|
||||
def original_roi(self) -> np.ndarray:
|
||||
""" :class: `numpy.ndarray`: The original region of interest of the mask in the
|
||||
source frame. """
|
||||
points = np.array([[0, 0],
|
||||
[0, self.stored_size - 1],
|
||||
[self.stored_size - 1, self.stored_size - 1],
|
||||
[self.stored_size - 1, 0]], np.int32).reshape((-1, 1, 2))
|
||||
matrix = cv2.invertAffineTransform(self.affine_matrix)
|
||||
roi = cv2.transform(points, matrix).reshape((4, 2))
|
||||
logger.trace("Returning: %s", roi) # type:ignore[attr-defined]
|
||||
return roi
|
||||
|
||||
@property
|
||||
def affine_matrix(self) -> np.ndarray:
|
||||
""" :class: `numpy.ndarray`: The affine matrix to transpose the mask to a full frame. """
|
||||
assert self._affine_matrix is not None
|
||||
return self._affine_matrix
|
||||
|
||||
@property
|
||||
def interpolator(self) -> int:
|
||||
""" int: The cv2 interpolator required to transpose the mask to a full frame. """
|
||||
assert self._interpolator is not None
|
||||
return self._interpolator
|
||||
|
||||
def _dilate_mask(self, mask: np.ndarray) -> None:
|
||||
""" Erode/Dilate the mask. The action is performed in-place on the given mask.
|
||||
|
||||
No action is performed if a dilation amount has not been set
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mask: :class:`numpy.ndarray`
|
||||
The mask to be eroded/dilated
|
||||
"""
|
||||
if self._dilation[-1] is None:
|
||||
return
|
||||
|
||||
func = cv2.erode if self._dilation[0] == "erode" else cv2.dilate
|
||||
func(mask, self._dilation[-1], dst=mask, iterations=1)
|
||||
|
||||
def get_full_frame_mask(self, width: int, height: int) -> np.ndarray:
|
||||
""" Return the stored mask in a full size frame of the given dimensions
|
||||
|
||||
Parameters
|
||||
----------
|
||||
width: int
|
||||
The width of the original frame that the mask was extracted from
|
||||
height: int
|
||||
The height of the original frame that the mask was extracted from
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`numpy.ndarray`: The mask affined to the original full frame of the given dimensions
|
||||
"""
|
||||
frame = np.zeros((width, height, 1), dtype="uint8")
|
||||
mask = cv2.warpAffine(self.mask,
|
||||
self.affine_matrix,
|
||||
(width, height),
|
||||
frame,
|
||||
flags=cv2.WARP_INVERSE_MAP | self.interpolator,
|
||||
borderMode=cv2.BORDER_CONSTANT)
|
||||
logger.trace("mask shape: %s, mask dtype: %s, mask min: %s, " # type:ignore[attr-defined]
|
||||
"mask max: %s", mask.shape, mask.dtype, mask.min(), mask.max())
|
||||
return mask
|
||||
|
||||
def add(self, mask: np.ndarray, affine_matrix: np.ndarray, interpolator: int) -> None:
|
||||
""" Add a Faceswap mask to this :class:`Mask`.
|
||||
|
||||
The mask should be the original output from :mod:`plugins.extract.mask`
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mask: :class:`numpy.ndarray`
|
||||
The mask that is to be added as output from :mod:`plugins.extract.mask`
|
||||
It should be in the range 0.0 - 1.0 ideally with a ``dtype`` of ``float32``
|
||||
affine_matrix: :class:`numpy.ndarray`
|
||||
The transformation matrix required to transform the mask to the original frame.
|
||||
interpolator, int:
|
||||
The CV2 interpolator required to transform this mask to it's original frame
|
||||
"""
|
||||
logger.trace("mask shape: %s, mask dtype: %s, mask min: %s, " # type:ignore[attr-defined]
|
||||
"mask max: %s, affine_matrix: %s, interpolator: %s)",
|
||||
mask.shape, mask.dtype, mask.min(), affine_matrix, mask.max(), interpolator)
|
||||
self._affine_matrix = self._adjust_affine_matrix(mask.shape[0], affine_matrix)
|
||||
self._interpolator = interpolator
|
||||
self.replace_mask(mask)
|
||||
|
||||
def replace_mask(self, mask: np.ndarray) -> None:
|
||||
""" Replace the existing :attr:`_mask` with the given mask.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mask: :class:`numpy.ndarray`
|
||||
The mask that is to be added as output from :mod:`plugins.extract.mask`.
|
||||
It should be in the range 0.0 - 1.0 ideally with a ``dtype`` of ``float32``
|
||||
"""
|
||||
mask = (cv2.resize(mask * 255.0,
|
||||
(self.stored_size, self.stored_size),
|
||||
interpolation=cv2.INTER_AREA)).astype("uint8")
|
||||
self._mask = compress(mask.tobytes())
|
||||
|
||||
def set_dilation(self, amount: float) -> None:
|
||||
""" Set the internal dilation object for returned masks
|
||||
|
||||
Parameters
|
||||
----------
|
||||
amount: float
|
||||
The amount of erosion/dilation to apply as a percentage of the total mask size.
|
||||
Negative values erode the mask. Positive values dilate the mask
|
||||
"""
|
||||
if amount == 0:
|
||||
self._dilation = ("erode", None)
|
||||
return
|
||||
|
||||
action: T.Literal["erode", "dilate"] = "erode" if amount < 0 else "dilate"
|
||||
kernel = int(round(self.stored_size * abs(amount / 100.), 0))
|
||||
self._dilation = (action, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel, kernel)))
|
||||
|
||||
logger.trace("action: '%s', amount: %s, kernel: %s, ", # type:ignore[attr-defined]
|
||||
action, amount, kernel)
|
||||
|
||||
def set_blur_and_threshold(self,
|
||||
blur_kernel: int = 0,
|
||||
blur_type: T.Literal["gaussian", "normalized"] | None = "gaussian",
|
||||
blur_passes: int = 1,
|
||||
threshold: int = 0) -> None:
|
||||
""" Set the internal blur kernel and threshold amount for returned masks
|
||||
|
||||
Parameters
|
||||
----------
|
||||
blur_kernel: int, optional
|
||||
The kernel size, in pixels to apply gaussian blurring to the mask. Set to 0 for no
|
||||
blurring. Should be odd, if an even number is passed in (outside of 0) then it is
|
||||
rounded up to the next odd number. Default: 0
|
||||
blur_type: ["gaussian", "normalized"], optional
|
||||
The blur type to use. ``gaussian`` or ``normalized`` box filter. Default: ``gaussian``
|
||||
blur_passes: int, optional
|
||||
The number of passed to perform when blurring. Default: 1
|
||||
threshold: int, optional
|
||||
The threshold amount to minimize/maximize mask values to 0 and 100. Percentage value.
|
||||
Default: 0
|
||||
"""
|
||||
logger.trace("blur_kernel: %s, blur_type: %s, " # type:ignore[attr-defined]
|
||||
"blur_passes: %s, threshold: %s",
|
||||
blur_kernel, blur_type, blur_passes, threshold)
|
||||
if blur_type is not None:
|
||||
blur_kernel += 0 if blur_kernel == 0 or blur_kernel % 2 == 1 else 1
|
||||
self._blur_kernel = blur_kernel
|
||||
self._blur_type = blur_type
|
||||
self._blur_passes = blur_passes
|
||||
self._threshold = (threshold / 100.0) * 255.0
|
||||
|
||||
def set_sub_crop(self,
|
||||
source_offset: np.ndarray,
|
||||
target_offset: np.ndarray,
|
||||
centering: CenteringType,
|
||||
coverage_ratio: float = 1.0) -> None:
|
||||
""" Set the internal crop area of the mask to be returned.
|
||||
|
||||
This impacts the returned mask from :attr:`mask` if the requested mask is required for
|
||||
different face centering than what has been stored.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
source_offset: :class:`numpy.ndarray`
|
||||
The (x, y) offset for the mask at its stored centering
|
||||
target_offset: :class:`numpy.ndarray`
|
||||
The (x, y) offset for the mask at the requested target centering
|
||||
centering: str
|
||||
The centering to set the sub crop area for. One of `"legacy"`, `"face"`. `"head"`
|
||||
coverage_ratio: float, optional
|
||||
The coverage ratio to be applied to the target image. ``None`` for default (1.0).
|
||||
Default: ``None``
|
||||
"""
|
||||
if centering == self.stored_centering and coverage_ratio == 1.0:
|
||||
return
|
||||
|
||||
center = get_adjusted_center(self.stored_size,
|
||||
source_offset,
|
||||
target_offset,
|
||||
self.stored_centering)
|
||||
crop_size = get_centered_size(self.stored_centering,
|
||||
centering,
|
||||
self.stored_size,
|
||||
coverage_ratio=coverage_ratio)
|
||||
roi = np.array([center - crop_size // 2, center + crop_size // 2]).ravel()
|
||||
|
||||
self._sub_crop_size = crop_size
|
||||
self._sub_crop_slices["in"] = [slice(max(roi[1], 0), max(roi[3], 0)),
|
||||
slice(max(roi[0], 0), max(roi[2], 0))]
|
||||
self._sub_crop_slices["out"] = [
|
||||
slice(max(roi[1] * -1, 0),
|
||||
crop_size - min(crop_size, max(0, roi[3] - self.stored_size))),
|
||||
slice(max(roi[0] * -1, 0),
|
||||
crop_size - min(crop_size, max(0, roi[2] - self.stored_size)))]
|
||||
|
||||
logger.trace("src_size: %s, coverage_ratio: %s, " # type:ignore[attr-defined]
|
||||
"sub_crop_size: %s, sub_crop_slices: %s",
|
||||
roi, coverage_ratio, self._sub_crop_size, self._sub_crop_slices)
|
||||
|
||||
def _adjust_affine_matrix(self, mask_size: int, affine_matrix: np.ndarray) -> np.ndarray:
|
||||
""" Adjust the affine matrix for the mask's storage size
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mask_size: int
|
||||
The original size of the mask.
|
||||
affine_matrix: :class:`numpy.ndarray`
|
||||
The affine matrix to transform the mask at original size to the parent frame.
|
||||
|
||||
Returns
|
||||
-------
|
||||
affine_matrix: :class:`numpy,ndarray`
|
||||
The affine matrix adjusted for the mask at its stored dimensions.
|
||||
"""
|
||||
zoom = self.stored_size / mask_size
|
||||
zoom_mat = np.array([[zoom, 0, 0.], [0, zoom, 0.]])
|
||||
adjust_mat = np.dot(zoom_mat, np.concatenate((affine_matrix, np.array([[0., 0., 1.]]))))
|
||||
logger.trace("storage_size: %s, mask_size: %s, zoom: %s, " # type:ignore[attr-defined]
|
||||
"original matrix: %s, adjusted_matrix: %s", self.stored_size, mask_size, zoom,
|
||||
affine_matrix.shape, adjust_mat.shape)
|
||||
return adjust_mat
|
||||
|
||||
def to_dict(self, is_png=False) -> MaskAlignmentsFileDict:
|
||||
""" Convert the mask to a dictionary for saving to an alignments file
|
||||
|
||||
Parameters
|
||||
----------
|
||||
is_png: bool
|
||||
``True`` if the dictionary is being created for storage in a png header otherwise
|
||||
``False``. Default: ``False``
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict:
|
||||
The :class:`Mask` for saving to an alignments file. Contains the keys ``mask``,
|
||||
``affine_matrix``, ``interpolator``, ``stored_size``, ``stored_centering``
|
||||
"""
|
||||
assert self._mask is not None
|
||||
affine_matrix = self.affine_matrix.tolist() if is_png else self.affine_matrix
|
||||
retval = MaskAlignmentsFileDict(mask=self._mask,
|
||||
affine_matrix=affine_matrix,
|
||||
interpolator=self.interpolator,
|
||||
stored_size=self.stored_size,
|
||||
stored_centering=self.stored_centering)
|
||||
logger.trace({k: v if k != "mask" else type(v) # type:ignore[attr-defined]
|
||||
for k, v in retval.items()})
|
||||
return retval
|
||||
|
||||
def to_png_meta(self) -> MaskAlignmentsFileDict:
|
||||
""" Convert the mask to a dictionary supported by png itxt headers.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict:
|
||||
The :class:`Mask` for saving to an alignments file. Contains the keys ``mask``,
|
||||
``affine_matrix``, ``interpolator``, ``stored_size``, ``stored_centering``
|
||||
"""
|
||||
return self.to_dict(is_png=True)
|
||||
|
||||
def from_dict(self, mask_dict: MaskAlignmentsFileDict) -> None:
|
||||
""" Populates the :class:`Mask` from a dictionary loaded from an alignments file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mask_dict: dict
|
||||
A dictionary stored in an alignments file containing the keys ``mask``,
|
||||
``affine_matrix``, ``interpolator``, ``stored_size``, ``stored_centering``
|
||||
"""
|
||||
self._mask = mask_dict["mask"]
|
||||
affine_matrix = mask_dict["affine_matrix"]
|
||||
self._affine_matrix = (affine_matrix if isinstance(affine_matrix, np.ndarray)
|
||||
else np.array(affine_matrix, dtype="float64"))
|
||||
self._interpolator = mask_dict["interpolator"]
|
||||
self.stored_size = mask_dict["stored_size"]
|
||||
centering = mask_dict.get("stored_centering")
|
||||
self.stored_centering = "face" if centering is None else centering
|
||||
logger.trace({k: v if k != "mask" else type(v) # type:ignore[attr-defined]
|
||||
for k, v in mask_dict.items()})
|
||||
|
||||
|
||||
class LandmarksMask(Mask):
|
||||
""" Create a single channel mask from aligned landmark points.
|
||||
|
||||
Landmarks masks are created on the fly, so the stored centering and size should be the same as
|
||||
the aligned face that the mask will be applied to. As the masks are created on the fly, blur +
|
||||
dilation is applied to the mask at creation (prior to compression) rather than after
|
||||
decompression when requested.
|
||||
|
||||
Note
|
||||
----
|
||||
Threshold is not used for Landmarks mask as the mask is binary
|
||||
|
||||
Parameters
|
||||
----------
|
||||
points: list
|
||||
A list of landmark points that correspond to the given storage_size to create
|
||||
the mask. Each item in the list should be a :class:`numpy.ndarray` that a filled
|
||||
convex polygon will be created from
|
||||
storage_size: int, optional
|
||||
The size (in pixels) that the compressed mask should be stored at. Default: 128.
|
||||
storage_centering, str (optional):
|
||||
The centering to store the mask at. One of `"legacy"`, `"face"`, `"head"`.
|
||||
Default: `"face"`
|
||||
dilation: float, optional
|
||||
The amount of dilation to apply to the mask. as a percentage of the mask size. Default: 0.0
|
||||
"""
|
||||
def __init__(self,
|
||||
points: list[np.ndarray],
|
||||
storage_size: int = 128,
|
||||
storage_centering: CenteringType = "face",
|
||||
dilation: float = 0.0) -> None:
|
||||
super().__init__(storage_size=storage_size, storage_centering=storage_centering)
|
||||
self._points = points
|
||||
self.set_dilation(dilation)
|
||||
|
||||
@property
|
||||
def mask(self) -> np.ndarray:
|
||||
""" :class:`numpy.ndarray`: Overrides the default mask property, creating the processed
|
||||
mask at first call and compressing it. The decompressed mask is returned from this
|
||||
property. """
|
||||
return self.stored_mask
|
||||
|
||||
def generate_mask(self, affine_matrix: np.ndarray, interpolator: int) -> None:
|
||||
""" Generate the mask.
|
||||
|
||||
Creates the mask applying any requested dilation and blurring and assigns compressed mask
|
||||
to :attr:`_mask`
|
||||
|
||||
Parameters
|
||||
----------
|
||||
affine_matrix: :class:`numpy.ndarray`
|
||||
The transformation matrix required to transform the mask to the original frame.
|
||||
interpolator, int:
|
||||
The CV2 interpolator required to transform this mask to it's original frame
|
||||
"""
|
||||
mask = np.zeros((self.stored_size, self.stored_size, 1), dtype="float32")
|
||||
for landmarks in self._points:
|
||||
lms = np.rint(landmarks).astype("int")
|
||||
cv2.fillConvexPoly(mask, cv2.convexHull(lms), [1.0], lineType=cv2.LINE_AA)
|
||||
if self._dilation[-1] is not None:
|
||||
self._dilate_mask(mask)
|
||||
if self._blur_kernel != 0 and self._blur_type is not None:
|
||||
mask = BlurMask(self._blur_type,
|
||||
mask,
|
||||
self._blur_kernel,
|
||||
passes=self._blur_passes).blurred
|
||||
logger.trace("mask: (shape: %s, dtype: %s)", # type:ignore[attr-defined]
|
||||
mask.shape, mask.dtype)
|
||||
self.add(mask, affine_matrix, interpolator)
|
||||
|
||||
|
||||
class BlurMask():
|
||||
""" Factory class to return the correct blur object for requested blur type.
|
||||
|
||||
Works for square images only. Currently supports Gaussian and Normalized Box Filters.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
blur_type: ["gaussian", "normalized"]
|
||||
The type of blur to use
|
||||
mask: :class:`numpy.ndarray`
|
||||
The mask to apply the blur to
|
||||
kernel: int or float
|
||||
Either the kernel size (in pixels) or the size of the kernel as a ratio of mask size
|
||||
is_ratio: bool, optional
|
||||
Whether the given :attr:`kernel` parameter is a ratio or not. If ``True`` then the
|
||||
actual kernel size will be calculated from the given ratio and the mask size. If
|
||||
``False`` then the kernel size will be set directly from the :attr:`kernel` parameter.
|
||||
Default: ``False``
|
||||
passes: int, optional
|
||||
The number of passes to perform when blurring. Default: ``1``
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> print(mask.shape)
|
||||
(128, 128, 1)
|
||||
>>> new_mask = BlurMask("gaussian", mask, 3, is_ratio=False, passes=1).blurred
|
||||
>>> print(new_mask.shape)
|
||||
(128, 128, 1)
|
||||
"""
|
||||
def __init__(self,
|
||||
blur_type: T.Literal["gaussian", "normalized"],
|
||||
mask: np.ndarray,
|
||||
kernel: int | float,
|
||||
is_ratio: bool = False,
|
||||
passes: int = 1) -> None:
|
||||
logger.trace(parse_class_init(locals())) # type:ignore[attr-defined]
|
||||
self._blur_type = blur_type
|
||||
self._mask = mask
|
||||
self._passes = passes
|
||||
kernel_size = self._get_kernel_size(kernel, is_ratio)
|
||||
self._kernel_size = self._get_kernel_tuple(kernel_size)
|
||||
logger.trace("Initialized %s", self.__class__.__name__) # type:ignore[attr-defined]
|
||||
|
||||
@property
|
||||
def blurred(self) -> np.ndarray:
|
||||
""" :class:`numpy.ndarray`: The final mask with blurring applied. """
|
||||
func = self._func_mapping[self._blur_type]
|
||||
kwargs = self._get_kwargs()
|
||||
blurred = self._mask
|
||||
for i in range(self._passes):
|
||||
assert isinstance(kwargs["ksize"], tuple)
|
||||
ksize = int(kwargs["ksize"][0])
|
||||
logger.trace("Pass: %s, kernel_size: %s", # type:ignore[attr-defined]
|
||||
i + 1, (ksize, ksize))
|
||||
blurred = func(blurred, **kwargs)
|
||||
ksize = int(round(ksize * self._multipass_factor))
|
||||
kwargs["ksize"] = self._get_kernel_tuple(ksize)
|
||||
blurred = blurred[..., None]
|
||||
logger.trace("Returning blurred mask. Shape: %s", # type:ignore[attr-defined]
|
||||
blurred.shape)
|
||||
return blurred
|
||||
|
||||
@property
|
||||
def _multipass_factor(self) -> float:
|
||||
""" For multiple passes the kernel must be scaled down. This value is
|
||||
different for box filter and gaussian """
|
||||
factor = {"gaussian": 0.8, "normalized": 0.5}
|
||||
return factor[self._blur_type]
|
||||
|
||||
@property
|
||||
def _sigma(self) -> T.Literal[0]:
|
||||
""" int: The Sigma for Gaussian Blur. Returns 0 to force calculation from kernel size. """
|
||||
return 0
|
||||
|
||||
@property
|
||||
def _func_mapping(self) -> dict[T.Literal["gaussian", "normalized"], Callable]:
|
||||
""" dict: :attr:`_blur_type` mapped to cv2 Function name. """
|
||||
return {"gaussian": cv2.GaussianBlur, "normalized": cv2.blur}
|
||||
|
||||
@property
|
||||
def _kwarg_requirements(self) -> dict[T.Literal["gaussian", "normalized"], list[str]]:
|
||||
""" dict: :attr:`_blur_type` mapped to cv2 Function required keyword arguments. """
|
||||
return {"gaussian": ['ksize', 'sigmaX'], "normalized": ['ksize']}
|
||||
|
||||
@property
|
||||
def _kwarg_mapping(self) -> dict[str, int | tuple[int, int]]:
|
||||
""" dict: cv2 function keyword arguments mapped to their parameters. """
|
||||
return {"ksize": self._kernel_size, "sigmaX": self._sigma}
|
||||
|
||||
def _get_kernel_size(self, kernel: int | float, is_ratio: bool) -> int:
|
||||
""" Set the kernel size to absolute value.
|
||||
|
||||
If :attr:`is_ratio` is ``True`` then the kernel size is calculated from the given ratio and
|
||||
the :attr:`_mask` size, otherwise the given kernel size is just returned.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
kernel: int or float
|
||||
Either the kernel size (in pixels) or the size of the kernel as a ratio of mask size
|
||||
is_ratio: bool, optional
|
||||
Whether the given :attr:`kernel` parameter is a ratio or not. If ``True`` then the
|
||||
actual kernel size will be calculated from the given ratio and the mask size. If
|
||||
``False`` then the kernel size will be set directly from the :attr:`kernel` parameter.
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The size (in pixels) of the blur kernel
|
||||
"""
|
||||
if not is_ratio:
|
||||
return int(kernel)
|
||||
|
||||
mask_diameter = np.sqrt(np.sum(self._mask))
|
||||
radius = round(max(1., mask_diameter * kernel / 100.))
|
||||
kernel_size = int(radius * 2 + 1)
|
||||
logger.trace("kernel_size: %s", kernel_size) # type:ignore[attr-defined]
|
||||
return kernel_size
|
||||
|
||||
@staticmethod
|
||||
def _get_kernel_tuple(kernel_size: int) -> tuple[int, int]:
|
||||
""" Make sure kernel_size is odd and return it as a tuple.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
kernel_size: int
|
||||
The size in pixels of the blur kernel
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple
|
||||
The kernel size as a tuple of ('int', 'int')
|
||||
"""
|
||||
kernel_size += 1 if kernel_size % 2 == 0 else 0
|
||||
retval = (kernel_size, kernel_size)
|
||||
logger.trace(retval) # type:ignore[attr-defined]
|
||||
return retval
|
||||
|
||||
def _get_kwargs(self) -> dict[str, int | tuple[int, int]]:
|
||||
""" dict: the valid keyword arguments for the requested :attr:`_blur_type` """
|
||||
retval = {kword: self._kwarg_mapping[kword]
|
||||
for kword in self._kwarg_requirements[self._blur_type]}
|
||||
logger.trace("BlurMask kwargs: %s", retval) # type:ignore[attr-defined]
|
||||
return retval
|
|
@ -8,17 +8,18 @@ import typing as T
|
|||
from hashlib import sha1
|
||||
from zlib import compress, decompress
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from lib.image import encode_image, read_image
|
||||
from lib.logger import parse_class_init
|
||||
from lib.utils import FaceswapError
|
||||
from .alignments import (Alignments, AlignmentFileDict, MaskAlignmentsFileDict,
|
||||
PNGHeaderAlignmentsDict, PNGHeaderDict, PNGHeaderSourceDict)
|
||||
from . import AlignedFace, get_adjusted_center, get_centered_size, LANDMARK_PARTS
|
||||
from .alignments import (Alignments, AlignmentFileDict, PNGHeaderAlignmentsDict,
|
||||
PNGHeaderDict, PNGHeaderSourceDict)
|
||||
from .aligned_face import AlignedFace
|
||||
from .aligned_mask import LandmarksMask, Mask
|
||||
from .constants import LANDMARK_PARTS
|
||||
|
||||
if T.TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
from .aligned_face import CenteringType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -53,7 +54,7 @@ class DetectedFace():
|
|||
of 68 `(x, y)` ``tuples`` with each of the landmark co-ordinates.
|
||||
mask: dict
|
||||
The generated mask(s) for the face as generated in :mod:`plugins.extract.mask`. Must be a
|
||||
dict of {**name** (`str`): :class:`Mask`}.
|
||||
dict of {**name** (`str`): :class:`~lib.align.aligned_mask.Mask`}.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
|
@ -77,7 +78,7 @@ class DetectedFace():
|
|||
The 68 point landmarks as discovered in :mod:`plugins.extract.align`.
|
||||
mask: dict
|
||||
The generated mask(s) for the face as generated in :mod:`plugins.extract.mask`. Is a
|
||||
dict of {**name** (`str`): :class:`Mask`}.
|
||||
dict of {**name** (`str`): :class:`~lib.align.aligned_mask.Mask`}.
|
||||
"""
|
||||
def __init__(self,
|
||||
image: np.ndarray | None = None,
|
||||
|
@ -86,13 +87,9 @@ class DetectedFace():
|
|||
top: int | None = None,
|
||||
height: int | None = None,
|
||||
landmarks_xy: np.ndarray | None = None,
|
||||
mask: dict[str, "Mask"] | None = None,
|
||||
mask: dict[str, Mask] | None = None,
|
||||
filename: str | None = None) -> None:
|
||||
logger.trace("Initializing %s: (image: %s, left: %s, " # type:ignore[attr-defined]
|
||||
"width: %s, top: %s, height: %s, landmarks_xy: %s, mask: %s, filename: %s)",
|
||||
self.__class__.__name__,
|
||||
image.shape if image is not None and image.any() else image, left, width, top,
|
||||
height, landmarks_xy, mask, filename)
|
||||
logger.trace(parse_class_init(locals())) # type:ignore[attr-defined]
|
||||
self.image = image
|
||||
self.left = left
|
||||
self.width = width
|
||||
|
@ -143,7 +140,7 @@ class DetectedFace():
|
|||
interpolator: int,
|
||||
storage_size: int = 128,
|
||||
storage_centering: CenteringType = "face") -> None:
|
||||
""" Add a :class:`Mask` to this detected face
|
||||
""" Add a :class:`~lib.align.aligned_mask.Mask` to this detected face
|
||||
|
||||
The mask should be the original output from :mod:`plugins.extract.mask`
|
||||
If a mask with this name already exists it will be overwritten by the given
|
||||
|
@ -211,7 +208,7 @@ class DetectedFace():
|
|||
area: T.Literal["eye", "face", "mouth"],
|
||||
blur_kernel: int,
|
||||
dilation: float) -> np.ndarray:
|
||||
""" Add a :class:`LandmarksMask` to this detected face
|
||||
""" Add a :class:`L~lib.align.aligned_mask.LandmarksMask` to this detected face
|
||||
|
||||
Landmark based masks are generated from face Aligned Face landmark points. An aligned
|
||||
face must be loaded. As the data is coming from the already aligned face, no further mask
|
||||
|
@ -273,8 +270,8 @@ class DetectedFace():
|
|||
A list of training mask. Must be all be uint-8 3D arrays of the same size in
|
||||
0-255 range
|
||||
delete_masks: bool, optional
|
||||
``True`` to delete any of the :class:`Mask` objects owned by this detected face. Use to
|
||||
free up unrequired memory usage. Default: ``False``
|
||||
``True`` to delete any of the :class:`~lib.align.aligned_mask.Mask` objects owned by
|
||||
this detected face. Use to free up unrequired memory usage. Default: ``False``
|
||||
"""
|
||||
if delete_masks:
|
||||
del self.mask
|
||||
|
@ -496,588 +493,6 @@ class DetectedFace():
|
|||
is_legacy=is_aligned and is_legacy)
|
||||
|
||||
|
||||
class Mask():
|
||||
""" Face Mask information and convenience methods
|
||||
|
||||
Holds a Faceswap mask as generated from :mod:`plugins.extract.mask` and the information
|
||||
required to transform it to its original frame.
|
||||
|
||||
Holds convenience methods to handle the warping, storing and retrieval of the mask.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
storage_size: int, optional
|
||||
The size (in pixels) that the mask should be stored at. Default: 128.
|
||||
storage_centering, str (optional):
|
||||
The centering to store the mask at. One of `"legacy"`, `"face"`, `"head"`.
|
||||
Default: `"face"`
|
||||
|
||||
Attributes
|
||||
----------
|
||||
stored_size: int
|
||||
The size, in pixels, of the stored mask across its height and width.
|
||||
stored_centering: str
|
||||
The centering that the mask is stored at. One of `"legacy"`, `"face"`, `"head"`
|
||||
"""
|
||||
def __init__(self,
|
||||
storage_size: int = 128,
|
||||
storage_centering: CenteringType = "face") -> None:
|
||||
logger.trace("Initializing: %s (storage_size: %s, " # type:ignore[attr-defined]
|
||||
"storage_centering: %s)",
|
||||
self.__class__.__name__, storage_size, storage_centering)
|
||||
self.stored_size = storage_size
|
||||
self.stored_centering = storage_centering
|
||||
|
||||
self._mask: bytes | None = None
|
||||
self._affine_matrix: np.ndarray | None = None
|
||||
self._interpolator: int | None = None
|
||||
|
||||
self._blur_type: T.Literal["gaussian", "normalized"] | None = None
|
||||
self._blur_passes: int = 0
|
||||
self._blur_kernel: float | int = 0
|
||||
self._threshold = 0.0
|
||||
self._dilation: tuple[T.Literal["erode", "dilate"], np.ndarray | None] = ("erode", None)
|
||||
self._sub_crop_size = 0
|
||||
self._sub_crop_slices: dict[T.Literal["in", "out"], list[slice]] = {}
|
||||
|
||||
self.set_blur_and_threshold()
|
||||
logger.trace("Initialized: %s", self.__class__.__name__) # type:ignore[attr-defined]
|
||||
|
||||
@property
|
||||
def mask(self) -> np.ndarray:
|
||||
""" :class:`numpy.ndarray`: The mask at the size of :attr:`stored_size` with any requested
|
||||
blurring, threshold amount and centering applied."""
|
||||
mask = self.stored_mask
|
||||
if self._dilation[-1] is not None or self._threshold != 0.0 or self._blur_kernel != 0:
|
||||
mask = mask.copy()
|
||||
self._dilate_mask(mask)
|
||||
if self._threshold != 0.0:
|
||||
mask[mask < self._threshold] = 0.0
|
||||
mask[mask > 255.0 - self._threshold] = 255.0
|
||||
if self._blur_kernel != 0 and self._blur_type is not None:
|
||||
mask = BlurMask(self._blur_type,
|
||||
mask,
|
||||
self._blur_kernel,
|
||||
passes=self._blur_passes).blurred
|
||||
if self._sub_crop_size: # Crop the mask to the given centering
|
||||
out = np.zeros((self._sub_crop_size, self._sub_crop_size, 1), dtype=mask.dtype)
|
||||
slice_in, slice_out = self._sub_crop_slices["in"], self._sub_crop_slices["out"]
|
||||
out[slice_out[0], slice_out[1], :] = mask[slice_in[0], slice_in[1], :]
|
||||
mask = out
|
||||
logger.trace("mask shape: %s", mask.shape) # type:ignore[attr-defined]
|
||||
return mask
|
||||
|
||||
@property
|
||||
def stored_mask(self) -> np.ndarray:
|
||||
""" :class:`numpy.ndarray`: The mask at the size of :attr:`stored_size` as it is stored
|
||||
(i.e. with no blurring/centering applied). """
|
||||
assert self._mask is not None
|
||||
dims = (self.stored_size, self.stored_size, 1)
|
||||
mask = np.frombuffer(decompress(self._mask), dtype="uint8").reshape(dims)
|
||||
logger.trace("stored mask shape: %s", mask.shape) # type:ignore[attr-defined]
|
||||
return mask
|
||||
|
||||
@property
|
||||
def original_roi(self) -> np.ndarray:
|
||||
""" :class: `numpy.ndarray`: The original region of interest of the mask in the
|
||||
source frame. """
|
||||
points = np.array([[0, 0],
|
||||
[0, self.stored_size - 1],
|
||||
[self.stored_size - 1, self.stored_size - 1],
|
||||
[self.stored_size - 1, 0]], np.int32).reshape((-1, 1, 2))
|
||||
matrix = cv2.invertAffineTransform(self.affine_matrix)
|
||||
roi = cv2.transform(points, matrix).reshape((4, 2))
|
||||
logger.trace("Returning: %s", roi) # type:ignore[attr-defined]
|
||||
return roi
|
||||
|
||||
@property
|
||||
def affine_matrix(self) -> np.ndarray:
|
||||
""" :class: `numpy.ndarray`: The affine matrix to transpose the mask to a full frame. """
|
||||
assert self._affine_matrix is not None
|
||||
return self._affine_matrix
|
||||
|
||||
@property
|
||||
def interpolator(self) -> int:
|
||||
""" int: The cv2 interpolator required to transpose the mask to a full frame. """
|
||||
assert self._interpolator is not None
|
||||
return self._interpolator
|
||||
|
||||
def _dilate_mask(self, mask: np.ndarray) -> None:
|
||||
""" Erode/Dilate the mask. The action is performed in-place on the given mask.
|
||||
|
||||
No action is performed if a dilation amount has not been set
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mask: :class:`numpy.ndarray`
|
||||
The mask to be eroded/dilated
|
||||
"""
|
||||
if self._dilation[-1] is None:
|
||||
return
|
||||
|
||||
func = cv2.erode if self._dilation[0] == "erode" else cv2.dilate
|
||||
func(mask, self._dilation[-1], dst=mask, iterations=1)
|
||||
|
||||
def get_full_frame_mask(self, width: int, height: int) -> np.ndarray:
|
||||
""" Return the stored mask in a full size frame of the given dimensions
|
||||
|
||||
Parameters
|
||||
----------
|
||||
width: int
|
||||
The width of the original frame that the mask was extracted from
|
||||
height: int
|
||||
The height of the original frame that the mask was extracted from
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`numpy.ndarray`: The mask affined to the original full frame of the given dimensions
|
||||
"""
|
||||
frame = np.zeros((width, height, 1), dtype="uint8")
|
||||
mask = cv2.warpAffine(self.mask,
|
||||
self.affine_matrix,
|
||||
(width, height),
|
||||
frame,
|
||||
flags=cv2.WARP_INVERSE_MAP | self.interpolator,
|
||||
borderMode=cv2.BORDER_CONSTANT)
|
||||
logger.trace("mask shape: %s, mask dtype: %s, mask min: %s, " # type:ignore[attr-defined]
|
||||
"mask max: %s", mask.shape, mask.dtype, mask.min(), mask.max())
|
||||
return mask
|
||||
|
||||
def add(self, mask: np.ndarray, affine_matrix: np.ndarray, interpolator: int) -> None:
|
||||
""" Add a Faceswap mask to this :class:`Mask`.
|
||||
|
||||
The mask should be the original output from :mod:`plugins.extract.mask`
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mask: :class:`numpy.ndarray`
|
||||
The mask that is to be added as output from :mod:`plugins.extract.mask`
|
||||
It should be in the range 0.0 - 1.0 ideally with a ``dtype`` of ``float32``
|
||||
affine_matrix: :class:`numpy.ndarray`
|
||||
The transformation matrix required to transform the mask to the original frame.
|
||||
interpolator, int:
|
||||
The CV2 interpolator required to transform this mask to it's original frame
|
||||
"""
|
||||
logger.trace("mask shape: %s, mask dtype: %s, mask min: %s, " # type:ignore[attr-defined]
|
||||
"mask max: %s, affine_matrix: %s, interpolator: %s)",
|
||||
mask.shape, mask.dtype, mask.min(), affine_matrix, mask.max(), interpolator)
|
||||
self._affine_matrix = self._adjust_affine_matrix(mask.shape[0], affine_matrix)
|
||||
self._interpolator = interpolator
|
||||
self.replace_mask(mask)
|
||||
|
||||
def replace_mask(self, mask: np.ndarray) -> None:
|
||||
""" Replace the existing :attr:`_mask` with the given mask.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mask: :class:`numpy.ndarray`
|
||||
The mask that is to be added as output from :mod:`plugins.extract.mask`.
|
||||
It should be in the range 0.0 - 1.0 ideally with a ``dtype`` of ``float32``
|
||||
"""
|
||||
mask = (cv2.resize(mask * 255.0,
|
||||
(self.stored_size, self.stored_size),
|
||||
interpolation=cv2.INTER_AREA)).astype("uint8")
|
||||
self._mask = compress(mask.tobytes())
|
||||
|
||||
def set_dilation(self, amount: float) -> None:
|
||||
""" Set the internal dilation object for returned masks
|
||||
|
||||
Parameters
|
||||
----------
|
||||
amount: float
|
||||
The amount of erosion/dilation to apply as a percentage of the total mask size.
|
||||
Negative values erode the mask. Positive values dilate the mask
|
||||
"""
|
||||
if amount == 0:
|
||||
self._dilation = ("erode", None)
|
||||
return
|
||||
|
||||
action: T.Literal["erode", "dilate"] = "erode" if amount < 0 else "dilate"
|
||||
kernel = int(round(self.stored_size * abs(amount / 100.), 0))
|
||||
self._dilation = (action, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel, kernel)))
|
||||
|
||||
logger.trace("action: '%s', amount: %s, kernel: %s, ", # type:ignore[attr-defined]
|
||||
action, amount, kernel)
|
||||
|
||||
def set_blur_and_threshold(self,
|
||||
blur_kernel: int = 0,
|
||||
blur_type: T.Literal["gaussian", "normalized"] | None = "gaussian",
|
||||
blur_passes: int = 1,
|
||||
threshold: int = 0) -> None:
|
||||
""" Set the internal blur kernel and threshold amount for returned masks
|
||||
|
||||
Parameters
|
||||
----------
|
||||
blur_kernel: int, optional
|
||||
The kernel size, in pixels to apply gaussian blurring to the mask. Set to 0 for no
|
||||
blurring. Should be odd, if an even number is passed in (outside of 0) then it is
|
||||
rounded up to the next odd number. Default: 0
|
||||
blur_type: ["gaussian", "normalized"], optional
|
||||
The blur type to use. ``gaussian`` or ``normalized`` box filter. Default: ``gaussian``
|
||||
blur_passes: int, optional
|
||||
The number of passed to perform when blurring. Default: 1
|
||||
threshold: int, optional
|
||||
The threshold amount to minimize/maximize mask values to 0 and 100. Percentage value.
|
||||
Default: 0
|
||||
"""
|
||||
logger.trace("blur_kernel: %s, blur_type: %s, " # type:ignore[attr-defined]
|
||||
"blur_passes: %s, threshold: %s",
|
||||
blur_kernel, blur_type, blur_passes, threshold)
|
||||
if blur_type is not None:
|
||||
blur_kernel += 0 if blur_kernel == 0 or blur_kernel % 2 == 1 else 1
|
||||
self._blur_kernel = blur_kernel
|
||||
self._blur_type = blur_type
|
||||
self._blur_passes = blur_passes
|
||||
self._threshold = (threshold / 100.0) * 255.0
|
||||
|
||||
def set_sub_crop(self,
|
||||
source_offset: np.ndarray,
|
||||
target_offset: np.ndarray,
|
||||
centering: CenteringType,
|
||||
coverage_ratio: float = 1.0) -> None:
|
||||
""" Set the internal crop area of the mask to be returned.
|
||||
|
||||
This impacts the returned mask from :attr:`mask` if the requested mask is required for
|
||||
different face centering than what has been stored.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
source_offset: :class:`numpy.ndarray`
|
||||
The (x, y) offset for the mask at its stored centering
|
||||
target_offset: :class:`numpy.ndarray`
|
||||
The (x, y) offset for the mask at the requested target centering
|
||||
centering: str
|
||||
The centering to set the sub crop area for. One of `"legacy"`, `"face"`. `"head"`
|
||||
coverage_ratio: float, optional
|
||||
The coverage ratio to be applied to the target image. ``None`` for default (1.0).
|
||||
Default: ``None``
|
||||
"""
|
||||
if centering == self.stored_centering and coverage_ratio == 1.0:
|
||||
return
|
||||
|
||||
center = get_adjusted_center(self.stored_size,
|
||||
source_offset,
|
||||
target_offset,
|
||||
self.stored_centering)
|
||||
crop_size = get_centered_size(self.stored_centering,
|
||||
centering,
|
||||
self.stored_size,
|
||||
coverage_ratio=coverage_ratio)
|
||||
roi = np.array([center - crop_size // 2, center + crop_size // 2]).ravel()
|
||||
|
||||
self._sub_crop_size = crop_size
|
||||
self._sub_crop_slices["in"] = [slice(max(roi[1], 0), max(roi[3], 0)),
|
||||
slice(max(roi[0], 0), max(roi[2], 0))]
|
||||
self._sub_crop_slices["out"] = [
|
||||
slice(max(roi[1] * -1, 0),
|
||||
crop_size - min(crop_size, max(0, roi[3] - self.stored_size))),
|
||||
slice(max(roi[0] * -1, 0),
|
||||
crop_size - min(crop_size, max(0, roi[2] - self.stored_size)))]
|
||||
|
||||
logger.trace("src_size: %s, coverage_ratio: %s, " # type:ignore[attr-defined]
|
||||
"sub_crop_size: %s, sub_crop_slices: %s",
|
||||
roi, coverage_ratio, self._sub_crop_size, self._sub_crop_slices)
|
||||
|
||||
def _adjust_affine_matrix(self, mask_size: int, affine_matrix: np.ndarray) -> np.ndarray:
|
||||
""" Adjust the affine matrix for the mask's storage size
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mask_size: int
|
||||
The original size of the mask.
|
||||
affine_matrix: :class:`numpy.ndarray`
|
||||
The affine matrix to transform the mask at original size to the parent frame.
|
||||
|
||||
Returns
|
||||
-------
|
||||
affine_matrix: :class:`numpy,ndarray`
|
||||
The affine matrix adjusted for the mask at its stored dimensions.
|
||||
"""
|
||||
zoom = self.stored_size / mask_size
|
||||
zoom_mat = np.array([[zoom, 0, 0.], [0, zoom, 0.]])
|
||||
adjust_mat = np.dot(zoom_mat, np.concatenate((affine_matrix, np.array([[0., 0., 1.]]))))
|
||||
logger.trace("storage_size: %s, mask_size: %s, zoom: %s, " # type:ignore[attr-defined]
|
||||
"original matrix: %s, adjusted_matrix: %s", self.stored_size, mask_size, zoom,
|
||||
affine_matrix.shape, adjust_mat.shape)
|
||||
return adjust_mat
|
||||
|
||||
def to_dict(self, is_png=False) -> MaskAlignmentsFileDict:
|
||||
""" Convert the mask to a dictionary for saving to an alignments file
|
||||
|
||||
Parameters
|
||||
----------
|
||||
is_png: bool
|
||||
``True`` if the dictionary is being created for storage in a png header otherwise
|
||||
``False``. Default: ``False``
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict:
|
||||
The :class:`Mask` for saving to an alignments file. Contains the keys ``mask``,
|
||||
``affine_matrix``, ``interpolator``, ``stored_size``, ``stored_centering``
|
||||
"""
|
||||
assert self._mask is not None
|
||||
affine_matrix = self.affine_matrix.tolist() if is_png else self.affine_matrix
|
||||
retval = MaskAlignmentsFileDict(mask=self._mask,
|
||||
affine_matrix=affine_matrix,
|
||||
interpolator=self.interpolator,
|
||||
stored_size=self.stored_size,
|
||||
stored_centering=self.stored_centering)
|
||||
logger.trace({k: v if k != "mask" else type(v) # type:ignore[attr-defined]
|
||||
for k, v in retval.items()})
|
||||
return retval
|
||||
|
||||
def to_png_meta(self) -> MaskAlignmentsFileDict:
|
||||
""" Convert the mask to a dictionary supported by png itxt headers.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict:
|
||||
The :class:`Mask` for saving to an alignments file. Contains the keys ``mask``,
|
||||
``affine_matrix``, ``interpolator``, ``stored_size``, ``stored_centering``
|
||||
"""
|
||||
return self.to_dict(is_png=True)
|
||||
|
||||
def from_dict(self, mask_dict: MaskAlignmentsFileDict) -> None:
|
||||
""" Populates the :class:`Mask` from a dictionary loaded from an alignments file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mask_dict: dict
|
||||
A dictionary stored in an alignments file containing the keys ``mask``,
|
||||
``affine_matrix``, ``interpolator``, ``stored_size``, ``stored_centering``
|
||||
"""
|
||||
self._mask = mask_dict["mask"]
|
||||
affine_matrix = mask_dict["affine_matrix"]
|
||||
self._affine_matrix = (affine_matrix if isinstance(affine_matrix, np.ndarray)
|
||||
else np.array(affine_matrix, dtype="float64"))
|
||||
self._interpolator = mask_dict["interpolator"]
|
||||
self.stored_size = mask_dict["stored_size"]
|
||||
centering = mask_dict.get("stored_centering")
|
||||
self.stored_centering = "face" if centering is None else centering
|
||||
logger.trace({k: v if k != "mask" else type(v) # type:ignore[attr-defined]
|
||||
for k, v in mask_dict.items()})
|
||||
|
||||
|
||||
class LandmarksMask(Mask):
|
||||
""" Create a single channel mask from aligned landmark points.
|
||||
|
||||
Landmarks masks are created on the fly, so the stored centering and size should be the same as
|
||||
the aligned face that the mask will be applied to. As the masks are created on the fly, blur +
|
||||
dilation is applied to the mask at creation (prior to compression) rather than after
|
||||
decompression when requested.
|
||||
|
||||
Note
|
||||
----
|
||||
Threshold is not used for Landmarks mask as the mask is binary
|
||||
|
||||
Parameters
|
||||
----------
|
||||
points: list
|
||||
A list of landmark points that correspond to the given storage_size to create
|
||||
the mask. Each item in the list should be a :class:`numpy.ndarray` that a filled
|
||||
convex polygon will be created from
|
||||
storage_size: int, optional
|
||||
The size (in pixels) that the compressed mask should be stored at. Default: 128.
|
||||
storage_centering, str (optional):
|
||||
The centering to store the mask at. One of `"legacy"`, `"face"`, `"head"`.
|
||||
Default: `"face"`
|
||||
dilation: float, optional
|
||||
The amount of dilation to apply to the mask. as a percentage of the mask size. Default: 0.0
|
||||
"""
|
||||
def __init__(self,
|
||||
points: list[np.ndarray],
|
||||
storage_size: int = 128,
|
||||
storage_centering: CenteringType = "face",
|
||||
dilation: float = 0.0) -> None:
|
||||
super().__init__(storage_size=storage_size, storage_centering=storage_centering)
|
||||
self._points = points
|
||||
self.set_dilation(dilation)
|
||||
|
||||
@property
|
||||
def mask(self) -> np.ndarray:
|
||||
""" :class:`numpy.ndarray`: Overrides the default mask property, creating the processed
|
||||
mask at first call and compressing it. The decompressed mask is returned from this
|
||||
property. """
|
||||
return self.stored_mask
|
||||
|
||||
def generate_mask(self, affine_matrix: np.ndarray, interpolator: int) -> None:
|
||||
""" Generate the mask.
|
||||
|
||||
Creates the mask applying any requested dilation and blurring and assigns compressed mask
|
||||
to :attr:`_mask`
|
||||
|
||||
Parameters
|
||||
----------
|
||||
affine_matrix: :class:`numpy.ndarray`
|
||||
The transformation matrix required to transform the mask to the original frame.
|
||||
interpolator, int:
|
||||
The CV2 interpolator required to transform this mask to it's original frame
|
||||
"""
|
||||
mask = np.zeros((self.stored_size, self.stored_size, 1), dtype="float32")
|
||||
for landmarks in self._points:
|
||||
lms = np.rint(landmarks).astype("int")
|
||||
cv2.fillConvexPoly(mask, cv2.convexHull(lms), [1.0], lineType=cv2.LINE_AA)
|
||||
if self._dilation[-1] is not None:
|
||||
self._dilate_mask(mask)
|
||||
if self._blur_kernel != 0 and self._blur_type is not None:
|
||||
mask = BlurMask(self._blur_type,
|
||||
mask,
|
||||
self._blur_kernel,
|
||||
passes=self._blur_passes).blurred
|
||||
logger.trace("mask: (shape: %s, dtype: %s)", # type:ignore[attr-defined]
|
||||
mask.shape, mask.dtype)
|
||||
self.add(mask, affine_matrix, interpolator)
|
||||
|
||||
|
||||
class BlurMask():
|
||||
""" Factory class to return the correct blur object for requested blur type.
|
||||
|
||||
Works for square images only. Currently supports Gaussian and Normalized Box Filters.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
blur_type: ["gaussian", "normalized"]
|
||||
The type of blur to use
|
||||
mask: :class:`numpy.ndarray`
|
||||
The mask to apply the blur to
|
||||
kernel: int or float
|
||||
Either the kernel size (in pixels) or the size of the kernel as a ratio of mask size
|
||||
is_ratio: bool, optional
|
||||
Whether the given :attr:`kernel` parameter is a ratio or not. If ``True`` then the
|
||||
actual kernel size will be calculated from the given ratio and the mask size. If
|
||||
``False`` then the kernel size will be set directly from the :attr:`kernel` parameter.
|
||||
Default: ``False``
|
||||
passes: int, optional
|
||||
The number of passes to perform when blurring. Default: ``1``
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> print(mask.shape)
|
||||
(128, 128, 1)
|
||||
>>> new_mask = BlurMask("gaussian", mask, 3, is_ratio=False, passes=1).blurred
|
||||
>>> print(new_mask.shape)
|
||||
(128, 128, 1)
|
||||
"""
|
||||
def __init__(self,
|
||||
blur_type: T.Literal["gaussian", "normalized"],
|
||||
mask: np.ndarray,
|
||||
kernel: int | float,
|
||||
is_ratio: bool = False,
|
||||
passes: int = 1) -> None:
|
||||
logger.trace("Initializing %s: (blur_type: '%s', " # type:ignore[attr-defined]
|
||||
"mask_shape: %s, kernel: %s, is_ratio: %s, passes: %s)",
|
||||
self.__class__.__name__, blur_type,
|
||||
mask.shape, kernel, is_ratio, passes)
|
||||
self._blur_type = blur_type
|
||||
self._mask = mask
|
||||
self._passes = passes
|
||||
kernel_size = self._get_kernel_size(kernel, is_ratio)
|
||||
self._kernel_size = self._get_kernel_tuple(kernel_size)
|
||||
logger.trace("Initialized %s", self.__class__.__name__) # type:ignore[attr-defined]
|
||||
|
||||
@property
|
||||
def blurred(self) -> np.ndarray:
|
||||
""" :class:`numpy.ndarray`: The final mask with blurring applied. """
|
||||
func = self._func_mapping[self._blur_type]
|
||||
kwargs = self._get_kwargs()
|
||||
blurred = self._mask
|
||||
for i in range(self._passes):
|
||||
assert isinstance(kwargs["ksize"], tuple)
|
||||
ksize = int(kwargs["ksize"][0])
|
||||
logger.trace("Pass: %s, kernel_size: %s", # type:ignore[attr-defined]
|
||||
i + 1, (ksize, ksize))
|
||||
blurred = func(blurred, **kwargs)
|
||||
ksize = int(round(ksize * self._multipass_factor))
|
||||
kwargs["ksize"] = self._get_kernel_tuple(ksize)
|
||||
blurred = blurred[..., None]
|
||||
logger.trace("Returning blurred mask. Shape: %s", # type:ignore[attr-defined]
|
||||
blurred.shape)
|
||||
return blurred
|
||||
|
||||
@property
|
||||
def _multipass_factor(self) -> float:
|
||||
""" For multiple passes the kernel must be scaled down. This value is
|
||||
different for box filter and gaussian """
|
||||
factor = {"gaussian": 0.8, "normalized": 0.5}
|
||||
return factor[self._blur_type]
|
||||
|
||||
@property
|
||||
def _sigma(self) -> T.Literal[0]:
|
||||
""" int: The Sigma for Gaussian Blur. Returns 0 to force calculation from kernel size. """
|
||||
return 0
|
||||
|
||||
@property
|
||||
def _func_mapping(self) -> dict[T.Literal["gaussian", "normalized"], Callable]:
|
||||
""" dict: :attr:`_blur_type` mapped to cv2 Function name. """
|
||||
return {"gaussian": cv2.GaussianBlur, "normalized": cv2.blur}
|
||||
|
||||
@property
|
||||
def _kwarg_requirements(self) -> dict[T.Literal["gaussian", "normalized"], list[str]]:
|
||||
""" dict: :attr:`_blur_type` mapped to cv2 Function required keyword arguments. """
|
||||
return {"gaussian": ['ksize', 'sigmaX'], "normalized": ['ksize']}
|
||||
|
||||
@property
|
||||
def _kwarg_mapping(self) -> dict[str, int | tuple[int, int]]:
|
||||
""" dict: cv2 function keyword arguments mapped to their parameters. """
|
||||
return {"ksize": self._kernel_size, "sigmaX": self._sigma}
|
||||
|
||||
def _get_kernel_size(self, kernel: int | float, is_ratio: bool) -> int:
|
||||
""" Set the kernel size to absolute value.
|
||||
|
||||
If :attr:`is_ratio` is ``True`` then the kernel size is calculated from the given ratio and
|
||||
the :attr:`_mask` size, otherwise the given kernel size is just returned.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
kernel: int or float
|
||||
Either the kernel size (in pixels) or the size of the kernel as a ratio of mask size
|
||||
is_ratio: bool, optional
|
||||
Whether the given :attr:`kernel` parameter is a ratio or not. If ``True`` then the
|
||||
actual kernel size will be calculated from the given ratio and the mask size. If
|
||||
``False`` then the kernel size will be set directly from the :attr:`kernel` parameter.
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The size (in pixels) of the blur kernel
|
||||
"""
|
||||
if not is_ratio:
|
||||
return int(kernel)
|
||||
|
||||
mask_diameter = np.sqrt(np.sum(self._mask))
|
||||
radius = round(max(1., mask_diameter * kernel / 100.))
|
||||
kernel_size = int(radius * 2 + 1)
|
||||
logger.trace("kernel_size: %s", kernel_size) # type:ignore[attr-defined]
|
||||
return kernel_size
|
||||
|
||||
@staticmethod
|
||||
def _get_kernel_tuple(kernel_size: int) -> tuple[int, int]:
|
||||
""" Make sure kernel_size is odd and return it as a tuple.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
kernel_size: int
|
||||
The size in pixels of the blur kernel
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple
|
||||
The kernel size as a tuple of ('int', 'int')
|
||||
"""
|
||||
kernel_size += 1 if kernel_size % 2 == 0 else 0
|
||||
retval = (kernel_size, kernel_size)
|
||||
logger.trace(retval) # type:ignore[attr-defined]
|
||||
return retval
|
||||
|
||||
def _get_kwargs(self) -> dict[str, int | tuple[int, int]]:
|
||||
""" dict: the valid keyword arguments for the requested :attr:`_blur_type` """
|
||||
retval = {kword: self._kwarg_mapping[kword]
|
||||
for kword in self._kwarg_requirements[self._blur_type]}
|
||||
logger.trace("BlurMask kwargs: %s", retval) # type:ignore[attr-defined]
|
||||
return retval
|
||||
|
||||
|
||||
_HASHES_SEEN: dict[str, dict[str, int]] = {}
|
||||
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
#!/usr/bin/env python3
|
||||
""" The Frame Viewer for Faceswap's Manual Tool. """
|
||||
|
||||
from ._base import View # noqa
|
||||
from .bounding_box import BoundingBox # noqa
|
||||
from .extract_box import ExtractBox # noqa
|
||||
from .landmarks import Landmarks, Mesh # noqa
|
||||
from .mask import Mask # noqa
|
||||
from ._base import View
|
||||
from .bounding_box import BoundingBox
|
||||
from .extract_box import ExtractBox
|
||||
from .landmarks import Landmarks, Mesh
|
||||
from .mask import Mask
|
||||
|
|
|
@ -27,8 +27,7 @@ from .frameviewer.frame import DisplayFrame
|
|||
from .thumbnails import ThumbsCreator
|
||||
|
||||
if T.TYPE_CHECKING:
|
||||
from lib.align import DetectedFace
|
||||
from lib.align.detected_face import Mask
|
||||
from lib.align import DetectedFace, Mask
|
||||
from lib.queue_manager import EventQueue
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -819,7 +818,7 @@ class Aligner():
|
|||
|
||||
Returns
|
||||
-------
|
||||
dict[str, :class:`~lib.align.detected_face.Mask`]
|
||||
dict[str, :class:`~lib.align.aligned_mask.Mask`]
|
||||
The updated masks
|
||||
"""
|
||||
logger.trace("frame_index: %s, face_index: %s", # type:ignore[attr-defined]
|
||||
|
|
Loading…
Add table
Reference in a new issue