mirror of
https://github.com/zebrajr/faceswap.git
synced 2025-12-06 12:20:27 +01:00
Add ability to export and import alignment data (#1383)
* tools.alignments - add export job * plugins.extract: Update __repr__ for ExtractorBatch dataclass * plugins.extract: Initial implementation of external import plugins * plugins.extract: Disable lm masks on ROI alignment data import * lib.align: Add `landmark_type` property to AlignedFace and return dummy data for ROI Landmarks pose estimate * plugins.extract: Add centering config item for align import and fix filename mapping for images * plugins.extract: Log warning on downstream plugins on limited alignment data * tools: Fix plugins for 4 point ROI landmarks (alignments, sort, mask) * tools.manual: Fix for 2D-4 ROI landmarks * training: Fix for 4 point ROI landmarks * lib.convert: Average color plugin. Avoid divide by zero errors * extract - external: - Default detector to 'external' when importing alignments - Handle different frame origin co-ordinates * alignments: Store video extension in alignments file * plugins.extract.external: Handle video file keys * plugins.extract.external: Output warning if missing data * locales + docs * plugins.extract.align.external: Roll the corner points to top-left for different origins * Clean up * linting fix
This commit is contained in:
parent
118e615724
commit
1c081aea7d
|
|
@ -7,6 +7,7 @@ The align Package handles detected faces, their alignments and masks.
|
||||||
.. contents:: Contents
|
.. contents:: Contents
|
||||||
:local:
|
:local:
|
||||||
|
|
||||||
|
|
||||||
aligned\_face module
|
aligned\_face module
|
||||||
====================
|
====================
|
||||||
|
|
||||||
|
|
@ -16,10 +17,9 @@ Handles aligned faces and corresponding pose estimates
|
||||||
|
|
||||||
.. autosummary::
|
.. autosummary::
|
||||||
:nosignatures:
|
:nosignatures:
|
||||||
|
|
||||||
~lib.align.aligned_face.AlignedFace
|
~lib.align.aligned_face.AlignedFace
|
||||||
~lib.align.aligned_face.get_matrix_scaling
|
~lib.align.aligned_face.get_matrix_scaling
|
||||||
~lib.align.aligned_face.PoseEstimate
|
|
||||||
~lib.align.aligned_face.transform_image
|
~lib.align.aligned_face.transform_image
|
||||||
|
|
||||||
.. rubric:: Module
|
.. rubric:: Module
|
||||||
|
|
@ -29,6 +29,7 @@ Handles aligned faces and corresponding pose estimates
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
alignments module
|
alignments module
|
||||||
=================
|
=================
|
||||||
|
|
||||||
|
|
@ -38,7 +39,7 @@ Handles alignments stored in a serialized alignments.fsa file
|
||||||
|
|
||||||
.. autosummary::
|
.. autosummary::
|
||||||
:nosignatures:
|
:nosignatures:
|
||||||
|
|
||||||
~lib.align.alignments.Alignments
|
~lib.align.alignments.Alignments
|
||||||
~lib.align.alignments.Thumbnails
|
~lib.align.alignments.Thumbnails
|
||||||
|
|
||||||
|
|
@ -49,6 +50,17 @@ Handles alignments stored in a serialized alignments.fsa file
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
|
constants module
|
||||||
|
================
|
||||||
|
Holds various constants for use in generating and manipulating aligned face images
|
||||||
|
|
||||||
|
.. automodule:: lib.align.constants
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
detected\_face module
|
detected\_face module
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
|
|
@ -58,7 +70,7 @@ Handles detected face objects and their associated masks.
|
||||||
|
|
||||||
.. autosummary::
|
.. autosummary::
|
||||||
:nosignatures:
|
:nosignatures:
|
||||||
|
|
||||||
~lib.align.detected_face.BlurMask
|
~lib.align.detected_face.BlurMask
|
||||||
~lib.align.detected_face.DetectedFace
|
~lib.align.detected_face.DetectedFace
|
||||||
~lib.align.detected_face.Mask
|
~lib.align.detected_face.Mask
|
||||||
|
|
@ -70,3 +82,13 @@ Handles detected face objects and their associated masks.
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
|
pose module
|
||||||
|
===========
|
||||||
|
Handles pose estimates based on aligned face data
|
||||||
|
|
||||||
|
.. automodule:: lib.align.pose
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
|
||||||
|
|
@ -8,18 +8,16 @@ The Extract Package handles the various plugins available for extracting face se
|
||||||
:local:
|
:local:
|
||||||
|
|
||||||
|
|
||||||
|
extract\_media module
|
||||||
|
=====================
|
||||||
|
.. automodule:: plugins.extract.extract_media
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
pipeline module
|
pipeline module
|
||||||
===============
|
===============
|
||||||
.. rubric:: Module Summary
|
|
||||||
|
|
||||||
.. autosummary::
|
|
||||||
:nosignatures:
|
|
||||||
|
|
||||||
~plugins.extract.pipeline.ExtractMedia
|
|
||||||
~plugins.extract.pipeline.Extractor
|
|
||||||
|
|
||||||
.. rubric:: Module
|
|
||||||
|
|
||||||
.. automodule:: plugins.extract.pipeline
|
.. automodule:: plugins.extract.pipeline
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ Handles the display of faces in the Face Viewer section of Faceswap's Manual Too
|
||||||
.. contents:: Contents
|
.. contents:: Contents
|
||||||
:local:
|
:local:
|
||||||
|
|
||||||
|
|
||||||
frame module
|
frame module
|
||||||
============
|
============
|
||||||
|
|
||||||
|
|
@ -28,6 +29,27 @@ frame module
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
|
interact module
|
||||||
|
===============
|
||||||
|
|
||||||
|
.. rubric:: Module Summary
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
~tools.manual.faceviewer.interact.ActiveFrame
|
||||||
|
~tools.manual.faceviewer.interact.Asset
|
||||||
|
~tools.manual.faceviewer.interact.HoverBox
|
||||||
|
|
||||||
|
.. rubric:: Module
|
||||||
|
|
||||||
|
.. automodule:: tools.manual.faceviewer.interact
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
viewport module
|
viewport module
|
||||||
===============
|
===============
|
||||||
|
|
||||||
|
|
@ -36,8 +58,6 @@ viewport module
|
||||||
.. autosummary::
|
.. autosummary::
|
||||||
:nosignatures:
|
:nosignatures:
|
||||||
|
|
||||||
~tools.manual.faceviewer.viewport.ActiveFrame
|
|
||||||
~tools.manual.faceviewer.viewport.HoverBox
|
|
||||||
~tools.manual.faceviewer.viewport.TKFace
|
~tools.manual.faceviewer.viewport.TKFace
|
||||||
~tools.manual.faceviewer.viewport.Viewport
|
~tools.manual.faceviewer.viewport.Viewport
|
||||||
~tools.manual.faceviewer.viewport.VisibleObjects
|
~tools.manual.faceviewer.viewport.VisibleObjects
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
""" Package for handling alignments files, detected faces and aligned faces along with their
|
""" Package for handling alignments files, detected faces and aligned faces along with their
|
||||||
associated objects. """
|
associated objects. """
|
||||||
from .aligned_face import (AlignedFace, _EXTRACT_RATIOS, get_adjusted_center, # noqa
|
from .aligned_face import (AlignedFace, get_adjusted_center, get_matrix_scaling,
|
||||||
get_matrix_scaling, get_centered_size, PoseEstimate, transform_image)
|
get_centered_size, transform_image)
|
||||||
from .alignments import Alignments # noqa
|
from .alignments import Alignments
|
||||||
from .detected_face import BlurMask, DetectedFace, Mask, update_legacy_png_header # noqa
|
from .constants import CenteringType, EXTRACT_RATIOS, LANDMARK_PARTS, LandmarkType
|
||||||
|
from .detected_face import BlurMask, DetectedFace, Mask, update_legacy_png_header
|
||||||
|
|
|
||||||
|
|
@ -1,63 +1,22 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
""" Aligner for faceswap.py """
|
""" Aligner for faceswap.py """
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
import logging
|
import logging
|
||||||
import typing as T
|
import typing as T
|
||||||
|
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
from lib.logger import parse_class_init
|
||||||
|
|
||||||
|
from .constants import CenteringType, EXTRACT_RATIOS, LandmarkType, _MEAN_FACE
|
||||||
|
from .pose import PoseEstimate
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
CenteringType = T.Literal["face", "head", "legacy"]
|
|
||||||
|
|
||||||
_MEAN_FACE = np.array([[0.010086, 0.106454], [0.085135, 0.038915], [0.191003, 0.018748],
|
|
||||||
[0.300643, 0.034489], [0.403270, 0.077391], [0.596729, 0.077391],
|
|
||||||
[0.699356, 0.034489], [0.808997, 0.018748], [0.914864, 0.038915],
|
|
||||||
[0.989913, 0.106454], [0.500000, 0.203352], [0.500000, 0.307009],
|
|
||||||
[0.500000, 0.409805], [0.500000, 0.515625], [0.376753, 0.587326],
|
|
||||||
[0.435909, 0.609345], [0.500000, 0.628106], [0.564090, 0.609345],
|
|
||||||
[0.623246, 0.587326], [0.131610, 0.216423], [0.196995, 0.178758],
|
|
||||||
[0.275698, 0.179852], [0.344479, 0.231733], [0.270791, 0.245099],
|
|
||||||
[0.192616, 0.244077], [0.655520, 0.231733], [0.724301, 0.179852],
|
|
||||||
[0.803005, 0.178758], [0.868389, 0.216423], [0.807383, 0.244077],
|
|
||||||
[0.729208, 0.245099], [0.264022, 0.780233], [0.350858, 0.745405],
|
|
||||||
[0.438731, 0.727388], [0.500000, 0.742578], [0.561268, 0.727388],
|
|
||||||
[0.649141, 0.745405], [0.735977, 0.780233], [0.652032, 0.864805],
|
|
||||||
[0.566594, 0.902192], [0.500000, 0.909281], [0.433405, 0.902192],
|
|
||||||
[0.347967, 0.864805], [0.300252, 0.784792], [0.437969, 0.778746],
|
|
||||||
[0.500000, 0.785343], [0.562030, 0.778746], [0.699747, 0.784792],
|
|
||||||
[0.563237, 0.824182], [0.500000, 0.831803], [0.436763, 0.824182]])
|
|
||||||
|
|
||||||
_MEAN_FACE_3D = np.array([[4.056931, -11.432347, 1.636229], # 8 chin LL
|
|
||||||
[1.833492, -12.542305, 4.061275], # 7 chin L
|
|
||||||
[0.0, -12.901019, 4.070434], # 6 chin C
|
|
||||||
[-1.833492, -12.542305, 4.061275], # 5 chin R
|
|
||||||
[-4.056931, -11.432347, 1.636229], # 4 chin RR
|
|
||||||
[6.825897, 1.275284, 4.402142], # 33 L eyebrow L
|
|
||||||
[1.330353, 1.636816, 6.903745], # 29 L eyebrow R
|
|
||||||
[-1.330353, 1.636816, 6.903745], # 34 R eyebrow L
|
|
||||||
[-6.825897, 1.275284, 4.402142], # 38 R eyebrow R
|
|
||||||
[1.930245, -5.060977, 5.914376], # 54 nose LL
|
|
||||||
[0.746313, -5.136947, 6.263227], # 53 nose L
|
|
||||||
[0.0, -5.485328, 6.76343], # 52 nose C
|
|
||||||
[-0.746313, -5.136947, 6.263227], # 51 nose R
|
|
||||||
[-1.930245, -5.060977, 5.914376], # 50 nose RR
|
|
||||||
[5.311432, 0.0, 3.987654], # 13 L eye L
|
|
||||||
[1.78993, -0.091703, 4.413414], # 17 L eye R
|
|
||||||
[-1.78993, -0.091703, 4.413414], # 25 R eye L
|
|
||||||
[-5.311432, 0.0, 3.987654], # 21 R eye R
|
|
||||||
[2.774015, -7.566103, 5.048531], # 43 mouth L
|
|
||||||
[0.509714, -7.056507, 6.566167], # 42 mouth top L
|
|
||||||
[0.0, -7.131772, 6.704956], # 41 mouth top C
|
|
||||||
[-0.509714, -7.056507, 6.566167], # 40 mouth top R
|
|
||||||
[-2.774015, -7.566103, 5.048531], # 39 mouth R
|
|
||||||
[-0.589441, -8.443925, 6.109526], # 46 mouth bottom R
|
|
||||||
[0.0, -8.601736, 6.097667], # 45 mouth bottom C
|
|
||||||
[0.589441, -8.443925, 6.109526]]) # 44 mouth bottom L
|
|
||||||
|
|
||||||
_EXTRACT_RATIOS = {"legacy": 0.375, "face": 0.5, "head": 0.625}
|
|
||||||
|
|
||||||
|
|
||||||
def get_matrix_scaling(matrix: np.ndarray) -> tuple[int, int]:
|
def get_matrix_scaling(matrix: np.ndarray) -> tuple[int, int]:
|
||||||
|
|
@ -82,7 +41,7 @@ def get_matrix_scaling(matrix: np.ndarray) -> tuple[int, int]:
|
||||||
interpolators = cv2.INTER_CUBIC, cv2.INTER_AREA
|
interpolators = cv2.INTER_CUBIC, cv2.INTER_AREA
|
||||||
else:
|
else:
|
||||||
interpolators = cv2.INTER_AREA, cv2.INTER_CUBIC
|
interpolators = cv2.INTER_AREA, cv2.INTER_CUBIC
|
||||||
logger.trace("interpolator: %s, inverse interpolator: %s", # type: ignore
|
logger.trace("interpolator: %s, inverse interpolator: %s", # type:ignore[attr-defined]
|
||||||
interpolators[0], interpolators[1])
|
interpolators[0], interpolators[1])
|
||||||
return interpolators
|
return interpolators
|
||||||
|
|
||||||
|
|
@ -109,7 +68,7 @@ def transform_image(image: np.ndarray,
|
||||||
:class:`numpy.ndarray`
|
:class:`numpy.ndarray`
|
||||||
The transformed image
|
The transformed image
|
||||||
"""
|
"""
|
||||||
logger.trace("image shape: %s, matrix: %s, size: %s. padding: %s", # type: ignore
|
logger.trace("image shape: %s, matrix: %s, size: %s. padding: %s", # type:ignore[attr-defined]
|
||||||
image.shape, matrix, size, padding)
|
image.shape, matrix, size, padding)
|
||||||
# transform the matrix for size and padding
|
# transform the matrix for size and padding
|
||||||
mat = matrix * (size - 2 * padding)
|
mat = matrix * (size - 2 * padding)
|
||||||
|
|
@ -118,7 +77,7 @@ def transform_image(image: np.ndarray,
|
||||||
# transform image
|
# transform image
|
||||||
interpolators = get_matrix_scaling(mat)
|
interpolators = get_matrix_scaling(mat)
|
||||||
retval = cv2.warpAffine(image, mat, (size, size), flags=interpolators[0])
|
retval = cv2.warpAffine(image, mat, (size, size), flags=interpolators[0])
|
||||||
logger.trace("transformed matrix: %s, final image shape: %s", # type: ignore
|
logger.trace("transformed matrix: %s, final image shape: %s", # type:ignore[attr-defined]
|
||||||
mat, image.shape)
|
mat, image.shape)
|
||||||
return retval
|
return retval
|
||||||
|
|
||||||
|
|
@ -146,13 +105,14 @@ def get_adjusted_center(image_size: int,
|
||||||
:class:`numpy.ndarray`
|
:class:`numpy.ndarray`
|
||||||
The center point of the image at the given size for the target centering
|
The center point of the image at the given size for the target centering
|
||||||
"""
|
"""
|
||||||
source_size = image_size - (image_size * _EXTRACT_RATIOS[source_centering])
|
source_size = image_size - (image_size * EXTRACT_RATIOS[source_centering])
|
||||||
offset = target_offset - source_offset
|
offset = target_offset - source_offset
|
||||||
offset *= source_size
|
offset *= source_size
|
||||||
center = np.rint(offset + image_size / 2).astype("int32")
|
center = np.rint(offset + image_size / 2).astype("int32")
|
||||||
logger.trace("image_size: %s, source_offset: %s, target_offset: %s, " # type: ignore
|
logger.trace( # type:ignore[attr-defined]
|
||||||
"source_centering: '%s', adjusted_offset: %s, center: %s", image_size,
|
"image_size: %s, source_offset: %s, target_offset: %s, source_centering: '%s', "
|
||||||
source_offset, target_offset, source_centering, offset, center)
|
"adjusted_offset: %s, center: %s",
|
||||||
|
image_size, source_offset, target_offset, source_centering, offset, center)
|
||||||
return center
|
return center
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -196,158 +156,16 @@ def get_centered_size(source_centering: CenteringType,
|
||||||
if source_centering == target_centering and coverage_ratio == 1.0:
|
if source_centering == target_centering and coverage_ratio == 1.0:
|
||||||
retval = size
|
retval = size
|
||||||
else:
|
else:
|
||||||
src_size = size - (size * _EXTRACT_RATIOS[source_centering])
|
src_size = size - (size * EXTRACT_RATIOS[source_centering])
|
||||||
retval = 2 * int(np.rint((src_size / (1 - _EXTRACT_RATIOS[target_centering])
|
retval = 2 * int(np.rint((src_size / (1 - EXTRACT_RATIOS[target_centering])
|
||||||
* coverage_ratio) / 2))
|
* coverage_ratio) / 2))
|
||||||
logger.trace("source_centering: %s, target_centering: %s, size: %s, " # type: ignore
|
logger.trace( # type:ignore[attr-defined]
|
||||||
"coverage_ratio: %s, source_size: %s, crop_size: %s", source_centering,
|
"source_centering: %s, target_centering: %s, size: %s, coverage_ratio: %s, "
|
||||||
target_centering, size, coverage_ratio, src_size, retval)
|
"source_size: %s, crop_size: %s",
|
||||||
|
source_centering, target_centering, size, coverage_ratio, src_size, retval)
|
||||||
return retval
|
return retval
|
||||||
|
|
||||||
|
|
||||||
class PoseEstimate():
|
|
||||||
""" Estimates pose from a generic 3D head model for the given 2D face landmarks.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
landmarks: :class:`numpy.ndarry`
|
|
||||||
The original 68 point landmarks aligned to 0.0 - 1.0 range
|
|
||||||
|
|
||||||
References
|
|
||||||
----------
|
|
||||||
Head Pose Estimation using OpenCV and Dlib - https://www.learnopencv.com/tag/solvepnp/
|
|
||||||
3D Model points - http://aifi.isr.uc.pt/Downloads/OpenGL/glAnthropometric3DModel.cpp
|
|
||||||
"""
|
|
||||||
def __init__(self, landmarks: np.ndarray) -> None:
|
|
||||||
self._distortion_coefficients = np.zeros((4, 1)) # Assuming no lens distortion
|
|
||||||
self._xyz_2d: np.ndarray | None = None
|
|
||||||
|
|
||||||
self._camera_matrix = self._get_camera_matrix()
|
|
||||||
self._rotation, self._translation = self._solve_pnp(landmarks)
|
|
||||||
self._offset = self._get_offset()
|
|
||||||
self._pitch_yaw_roll: tuple[float, float, float] = (0, 0, 0)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def xyz_2d(self) -> np.ndarray:
|
|
||||||
""" :class:`numpy.ndarray` projected (x, y) coordinates for each x, y, z point at a
|
|
||||||
constant distance from adjusted center of the skull (0.5, 0.5) in the 2D space. """
|
|
||||||
if self._xyz_2d is None:
|
|
||||||
xyz = cv2.projectPoints(np.array([[6., 0., -2.3],
|
|
||||||
[0., 6., -2.3],
|
|
||||||
[0., 0., 3.7]]).astype("float32"),
|
|
||||||
self._rotation,
|
|
||||||
self._translation,
|
|
||||||
self._camera_matrix,
|
|
||||||
self._distortion_coefficients)[0].squeeze()
|
|
||||||
self._xyz_2d = xyz - self._offset["head"]
|
|
||||||
return self._xyz_2d
|
|
||||||
|
|
||||||
@property
|
|
||||||
def offset(self) -> dict[CenteringType, np.ndarray]:
|
|
||||||
""" dict: The amount to offset a standard 0.0 - 1.0 umeyama transformation matrix for a
|
|
||||||
from the center of the face (between the eyes) or center of the head (middle of skull)
|
|
||||||
rather than the nose area. """
|
|
||||||
return self._offset
|
|
||||||
|
|
||||||
@property
|
|
||||||
def pitch(self) -> float:
|
|
||||||
""" float: The pitch of the aligned face in eular angles """
|
|
||||||
if not any(self._pitch_yaw_roll):
|
|
||||||
self._get_pitch_yaw_roll()
|
|
||||||
return self._pitch_yaw_roll[0]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def yaw(self) -> float:
|
|
||||||
""" float: The yaw of the aligned face in eular angles """
|
|
||||||
if not any(self._pitch_yaw_roll):
|
|
||||||
self._get_pitch_yaw_roll()
|
|
||||||
return self._pitch_yaw_roll[1]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def roll(self) -> float:
|
|
||||||
""" float: The roll of the aligned face in eular angles """
|
|
||||||
if not any(self._pitch_yaw_roll):
|
|
||||||
self._get_pitch_yaw_roll()
|
|
||||||
return self._pitch_yaw_roll[2]
|
|
||||||
|
|
||||||
def _get_pitch_yaw_roll(self) -> None:
|
|
||||||
""" Obtain the yaw, roll and pitch from the :attr:`_rotation` in eular angles. """
|
|
||||||
proj_matrix = np.zeros((3, 4), dtype="float32")
|
|
||||||
proj_matrix[:3, :3] = cv2.Rodrigues(self._rotation)[0]
|
|
||||||
euler = cv2.decomposeProjectionMatrix(proj_matrix)[-1]
|
|
||||||
self._pitch_yaw_roll = T.cast(tuple[float, float, float], tuple(euler.squeeze()))
|
|
||||||
logger.trace("yaw_pitch: %s", self._pitch_yaw_roll) # type: ignore
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _get_camera_matrix(cls) -> np.ndarray:
|
|
||||||
""" Obtain an estimate of the camera matrix based off the original frame dimensions.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
:class:`numpy.ndarray`
|
|
||||||
An estimated camera matrix
|
|
||||||
"""
|
|
||||||
focal_length = 4
|
|
||||||
camera_matrix = np.array([[focal_length, 0, 0.5],
|
|
||||||
[0, focal_length, 0.5],
|
|
||||||
[0, 0, 1]], dtype="double")
|
|
||||||
logger.trace("camera_matrix: %s", camera_matrix) # type: ignore
|
|
||||||
return camera_matrix
|
|
||||||
|
|
||||||
def _solve_pnp(self, landmarks: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
|
||||||
""" Solve the Perspective-n-Point for the given landmarks.
|
|
||||||
|
|
||||||
Takes 2D landmarks in world space and estimates the rotation and translation vectors
|
|
||||||
in 3D space.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
landmarks: :class:`numpy.ndarry`
|
|
||||||
The original 68 point landmark co-ordinates relating to the original frame
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
rotation: :class:`numpy.ndarray`
|
|
||||||
The solved rotation vector
|
|
||||||
translation: :class:`numpy.ndarray`
|
|
||||||
The solved translation vector
|
|
||||||
"""
|
|
||||||
points = landmarks[[6, 7, 8, 9, 10, 17, 21, 22, 26, 31, 32, 33, 34,
|
|
||||||
35, 36, 39, 42, 45, 48, 50, 51, 52, 54, 56, 57, 58]]
|
|
||||||
_, rotation, translation = cv2.solvePnP(_MEAN_FACE_3D,
|
|
||||||
points,
|
|
||||||
self._camera_matrix,
|
|
||||||
self._distortion_coefficients,
|
|
||||||
flags=cv2.SOLVEPNP_ITERATIVE)
|
|
||||||
logger.trace("points: %s, rotation: %s, translation: %s", # type: ignore
|
|
||||||
points, rotation, translation)
|
|
||||||
return rotation, translation
|
|
||||||
|
|
||||||
def _get_offset(self) -> dict[CenteringType, np.ndarray]:
|
|
||||||
""" Obtain the offset between the original center of the extracted face to the new center
|
|
||||||
of the head in 2D space.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
:class:`numpy.ndarray`
|
|
||||||
The x, y offset of the new center from the old center.
|
|
||||||
"""
|
|
||||||
offset: dict[CenteringType, np.ndarray] = {"legacy": np.array([0.0, 0.0])}
|
|
||||||
points: dict[T.Literal["face", "head"], tuple[float, ...]] = {"head": (0.0, 0.0, -2.3),
|
|
||||||
"face": (0.0, -1.5, 4.2)}
|
|
||||||
|
|
||||||
for key, pnts in points.items():
|
|
||||||
center = cv2.projectPoints(np.array([pnts]).astype("float32"),
|
|
||||||
self._rotation,
|
|
||||||
self._translation,
|
|
||||||
self._camera_matrix,
|
|
||||||
self._distortion_coefficients)[0].squeeze()
|
|
||||||
logger.trace("center %s: %s", key, center) # type: ignore
|
|
||||||
offset[key] = center - (0.5, 0.5)
|
|
||||||
logger.trace("offset: %s", offset) # type: ignore
|
|
||||||
return offset
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class _FaceCache: # pylint:disable=too-many-instance-attributes
|
class _FaceCache: # pylint:disable=too-many-instance-attributes
|
||||||
""" Cache for storing items related to a single aligned face.
|
""" Cache for storing items related to a single aligned face.
|
||||||
|
|
@ -464,27 +282,26 @@ class AlignedFace():
|
||||||
dtype: str | None = None,
|
dtype: str | None = None,
|
||||||
is_aligned: bool = False,
|
is_aligned: bool = False,
|
||||||
is_legacy: bool = False) -> None:
|
is_legacy: bool = False) -> None:
|
||||||
logger.trace("Initializing: %s (image shape: %s, centering: '%s', " # type: ignore
|
logger.trace(parse_class_init(locals())) # type:ignore[attr-defined]
|
||||||
"size: %s, coverage_ratio: %s, dtype: %s, is_aligned: %s, is_legacy: %s)",
|
|
||||||
self.__class__.__name__, image if image is None else image.shape,
|
|
||||||
centering, size, coverage_ratio, dtype, is_aligned, is_legacy)
|
|
||||||
self._frame_landmarks = landmarks
|
self._frame_landmarks = landmarks
|
||||||
|
self._landmark_type = LandmarkType.from_shape(landmarks.shape)
|
||||||
self._centering = centering
|
self._centering = centering
|
||||||
self._size = size
|
self._size = size
|
||||||
self._coverage_ratio = coverage_ratio
|
self._coverage_ratio = coverage_ratio
|
||||||
self._dtype = dtype
|
self._dtype = dtype
|
||||||
self._is_aligned = is_aligned
|
self._is_aligned = is_aligned
|
||||||
self._source_centering: CenteringType = "legacy" if is_legacy and is_aligned else "head"
|
self._source_centering: CenteringType = "legacy" if is_legacy and is_aligned else "head"
|
||||||
self._matrices = {"legacy": _umeyama(landmarks[17:], _MEAN_FACE, True)[0:2],
|
|
||||||
"face": np.array([]),
|
|
||||||
"head": np.array([])}
|
|
||||||
self._padding = self._padding_from_coverage(size, coverage_ratio)
|
self._padding = self._padding_from_coverage(size, coverage_ratio)
|
||||||
|
|
||||||
|
lookup = self._landmark_type
|
||||||
|
self._mean_lookup = LandmarkType.LM_2D_51 if lookup == LandmarkType.LM_2D_68 else lookup
|
||||||
|
|
||||||
self._cache = _FaceCache()
|
self._cache = _FaceCache()
|
||||||
|
self._matrices: dict[CenteringType, np.ndarray] = {"legacy": self._get_default_matrix()}
|
||||||
|
|
||||||
self._face = self.extract_face(image)
|
self._face = self.extract_face(image)
|
||||||
logger.trace("Initialized: %s (matrix: %s, padding: %s, face shape: %s)", # type: ignore
|
logger.trace("Initialized: %s (padding: %s, face shape: %s)", # type:ignore[attr-defined]
|
||||||
self.__class__.__name__, self._matrices["legacy"], self._padding,
|
self.__class__.__name__, self._padding,
|
||||||
self._face if self._face is None else self._face.shape)
|
self._face if self._face is None else self._face.shape)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -508,11 +325,11 @@ class AlignedFace():
|
||||||
""" :class:`numpy.ndarray`: The 3x2 transformation matrix for extracting and aligning the
|
""" :class:`numpy.ndarray`: The 3x2 transformation matrix for extracting and aligning the
|
||||||
core face area out of the original frame, with no padding or sizing applied. The returned
|
core face area out of the original frame, with no padding or sizing applied. The returned
|
||||||
matrix is offset for the given :attr:`centering`. """
|
matrix is offset for the given :attr:`centering`. """
|
||||||
if not np.any(self._matrices[self._centering]):
|
if self._centering not in self._matrices:
|
||||||
matrix = self._matrices["legacy"].copy()
|
matrix = self._matrices["legacy"].copy()
|
||||||
matrix[:, 2] -= self.pose.offset[self._centering]
|
matrix[:, 2] -= self.pose.offset[self._centering]
|
||||||
self._matrices[self._centering] = matrix
|
self._matrices[self._centering] = matrix
|
||||||
logger.trace("original matrix: %s, new matrix: %s", # type: ignore
|
logger.trace("original matrix: %s, new matrix: %s", # type:ignore[attr-defined]
|
||||||
self._matrices["legacy"], matrix)
|
self._matrices["legacy"], matrix)
|
||||||
return self._matrices[self._centering]
|
return self._matrices[self._centering]
|
||||||
|
|
||||||
|
|
@ -523,7 +340,7 @@ class AlignedFace():
|
||||||
if self._cache.pose is None:
|
if self._cache.pose is None:
|
||||||
lms = np.nan_to_num(cv2.transform(np.expand_dims(self._frame_landmarks, axis=1),
|
lms = np.nan_to_num(cv2.transform(np.expand_dims(self._frame_landmarks, axis=1),
|
||||||
self._matrices["legacy"]).squeeze())
|
self._matrices["legacy"]).squeeze())
|
||||||
self._cache.pose = PoseEstimate(lms)
|
self._cache.pose = PoseEstimate(lms, self._landmark_type)
|
||||||
return self._cache.pose
|
return self._cache.pose
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -535,7 +352,7 @@ class AlignedFace():
|
||||||
matrix = self.matrix.copy()
|
matrix = self.matrix.copy()
|
||||||
mat = matrix * (self._size - 2 * self.padding)
|
mat = matrix * (self._size - 2 * self.padding)
|
||||||
mat[:, 2] += self.padding
|
mat[:, 2] += self.padding
|
||||||
logger.trace("adjusted_matrix: %s", mat) # type: ignore
|
logger.trace("adjusted_matrix: %s", mat) # type:ignore[attr-defined]
|
||||||
self._cache.adjusted_matrix = mat
|
self._cache.adjusted_matrix = mat
|
||||||
return self._cache.adjusted_matrix
|
return self._cache.adjusted_matrix
|
||||||
|
|
||||||
|
|
@ -557,7 +374,7 @@ class AlignedFace():
|
||||||
[self._size - 1, self._size - 1],
|
[self._size - 1, self._size - 1],
|
||||||
[self._size - 1, 0]])
|
[self._size - 1, 0]])
|
||||||
roi = np.rint(self.transform_points(roi, invert=True)).astype("int32")
|
roi = np.rint(self.transform_points(roi, invert=True)).astype("int32")
|
||||||
logger.trace("original roi: %s", roi) # type: ignore
|
logger.trace("original roi: %s", roi) # type:ignore[attr-defined]
|
||||||
self._cache.original_roi = roi
|
self._cache.original_roi = roi
|
||||||
return self._cache.original_roi
|
return self._cache.original_roi
|
||||||
|
|
||||||
|
|
@ -568,10 +385,15 @@ class AlignedFace():
|
||||||
with self._cache.lock("landmarks"):
|
with self._cache.lock("landmarks"):
|
||||||
if self._cache.landmarks is None:
|
if self._cache.landmarks is None:
|
||||||
lms = self.transform_points(self._frame_landmarks)
|
lms = self.transform_points(self._frame_landmarks)
|
||||||
logger.trace("aligned landmarks: %s", lms) # type: ignore
|
logger.trace("aligned landmarks: %s", lms) # type:ignore[attr-defined]
|
||||||
self._cache.landmarks = lms
|
self._cache.landmarks = lms
|
||||||
return self._cache.landmarks
|
return self._cache.landmarks
|
||||||
|
|
||||||
|
@property
|
||||||
|
def landmark_type(self) -> LandmarkType:
|
||||||
|
""":class:`~LandmarkType`: The type of landmarks that generated this aligned face """
|
||||||
|
return self._landmark_type
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def normalized_landmarks(self) -> np.ndarray:
|
def normalized_landmarks(self) -> np.ndarray:
|
||||||
""" :class:`numpy.ndarray`: The 68 point facial landmarks normalized to 0.0 - 1.0 as
|
""" :class:`numpy.ndarray`: The 68 point facial landmarks normalized to 0.0 - 1.0 as
|
||||||
|
|
@ -579,8 +401,8 @@ class AlignedFace():
|
||||||
with self._cache.lock("landmarks_normalized"):
|
with self._cache.lock("landmarks_normalized"):
|
||||||
if self._cache.landmarks_normalized is None:
|
if self._cache.landmarks_normalized is None:
|
||||||
lms = np.expand_dims(self._frame_landmarks, axis=1)
|
lms = np.expand_dims(self._frame_landmarks, axis=1)
|
||||||
lms = cv2.transform(lms, self._matrices["legacy"], lms.shape).squeeze()
|
lms = cv2.transform(lms, self._matrices["legacy"]).squeeze()
|
||||||
logger.trace("normalized landmarks: %s", lms) # type: ignore
|
logger.trace("normalized landmarks: %s", lms) # type:ignore[attr-defined]
|
||||||
self._cache.landmarks_normalized = lms
|
self._cache.landmarks_normalized = lms
|
||||||
return self._cache.landmarks_normalized
|
return self._cache.landmarks_normalized
|
||||||
|
|
||||||
|
|
@ -590,7 +412,7 @@ class AlignedFace():
|
||||||
with self._cache.lock("interpolators"):
|
with self._cache.lock("interpolators"):
|
||||||
if not any(self._cache.interpolators):
|
if not any(self._cache.interpolators):
|
||||||
interpolators = get_matrix_scaling(self.adjusted_matrix)
|
interpolators = get_matrix_scaling(self.adjusted_matrix)
|
||||||
logger.trace("interpolators: %s", interpolators) # type: ignore
|
logger.trace("interpolators: %s", interpolators) # type:ignore[attr-defined]
|
||||||
self._cache.interpolators = interpolators
|
self._cache.interpolators = interpolators
|
||||||
return self._cache.interpolators
|
return self._cache.interpolators
|
||||||
|
|
||||||
|
|
@ -600,8 +422,12 @@ class AlignedFace():
|
||||||
used for aligning the image. """
|
used for aligning the image. """
|
||||||
with self._cache.lock("average_distance"):
|
with self._cache.lock("average_distance"):
|
||||||
if not self._cache.average_distance:
|
if not self._cache.average_distance:
|
||||||
average_distance = np.mean(np.abs(self.normalized_landmarks[17:] - _MEAN_FACE))
|
mean_face = _MEAN_FACE[self._mean_lookup]
|
||||||
logger.trace("average_distance: %s", average_distance) # type: ignore
|
lms = self.normalized_landmarks
|
||||||
|
if self._landmark_type == LandmarkType.LM_2D_68:
|
||||||
|
lms = lms[17:] # 68 point landmarks only use core face items
|
||||||
|
average_distance = np.mean(np.abs(lms - mean_face))
|
||||||
|
logger.trace("average_distance: %s", average_distance) # type:ignore[attr-defined]
|
||||||
self._cache.average_distance = average_distance
|
self._cache.average_distance = average_distance
|
||||||
return self._cache.average_distance
|
return self._cache.average_distance
|
||||||
|
|
||||||
|
|
@ -612,10 +438,13 @@ class AlignedFace():
|
||||||
mouth, negative values indicate that eyes/eyebrows are misaligned below the mouth. """
|
mouth, negative values indicate that eyes/eyebrows are misaligned below the mouth. """
|
||||||
with self._cache.lock("relative_eye_mouth_position"):
|
with self._cache.lock("relative_eye_mouth_position"):
|
||||||
if not self._cache.relative_eye_mouth_position:
|
if not self._cache.relative_eye_mouth_position:
|
||||||
lowest_eyes = np.max(self.normalized_landmarks[np.r_[17:27, 36:48], 1])
|
if self._landmark_type != LandmarkType.LM_2D_68:
|
||||||
highest_mouth = np.min(self.normalized_landmarks[48:68, 1])
|
position = 1.0 # arbitrary positive value
|
||||||
position = highest_mouth - lowest_eyes
|
else:
|
||||||
logger.trace("lowest_eyes: %s, highest_mouth: %s, " # type: ignore
|
lowest_eyes = np.max(self.normalized_landmarks[np.r_[17:27, 36:48], 1])
|
||||||
|
highest_mouth = np.min(self.normalized_landmarks[48:68, 1])
|
||||||
|
position = highest_mouth - lowest_eyes
|
||||||
|
logger.trace("lowest_eyes: %s, highest_mouth: %s, " # type:ignore[attr-defined]
|
||||||
"relative_eye_mouth_position: %s", lowest_eyes, highest_mouth,
|
"relative_eye_mouth_position: %s", lowest_eyes, highest_mouth,
|
||||||
position)
|
position)
|
||||||
self._cache.relative_eye_mouth_position = position
|
self._cache.relative_eye_mouth_position = position
|
||||||
|
|
@ -638,9 +467,24 @@ class AlignedFace():
|
||||||
dict
|
dict
|
||||||
The padding required, in pixels for 'head', 'face' and 'legacy' face types
|
The padding required, in pixels for 'head', 'face' and 'legacy' face types
|
||||||
"""
|
"""
|
||||||
retval = {_type: round((size * (coverage_ratio - (1 - _EXTRACT_RATIOS[_type]))) / 2)
|
retval = {_type: round((size * (coverage_ratio - (1 - EXTRACT_RATIOS[_type]))) / 2)
|
||||||
for _type in T.get_args(T.Literal["legacy", "face", "head"])}
|
for _type in T.get_args(T.Literal["legacy", "face", "head"])}
|
||||||
logger.trace(retval) # type: ignore
|
logger.trace(retval) # type:ignore[attr-defined]
|
||||||
|
return retval
|
||||||
|
|
||||||
|
def _get_default_matrix(self) -> np.ndarray:
|
||||||
|
""" Get the default (legacy) matrix. All subsequent matrices are calculated from this
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
:class:`numpy.ndarray`
|
||||||
|
The default 'legacy' matrix
|
||||||
|
"""
|
||||||
|
lms = self._frame_landmarks
|
||||||
|
if self._landmark_type == LandmarkType.LM_2D_68:
|
||||||
|
lms = lms[17:] # 68 point landmarks only use core face items
|
||||||
|
retval = _umeyama(lms, _MEAN_FACE[self._mean_lookup], True)[0:2]
|
||||||
|
logger.trace("Default matrix: %s", retval) # type:ignore[attr-defined]
|
||||||
return retval
|
return retval
|
||||||
|
|
||||||
def transform_points(self, points: np.ndarray, invert: bool = False) -> np.ndarray:
|
def transform_points(self, points: np.ndarray, invert: bool = False) -> np.ndarray:
|
||||||
|
|
@ -662,9 +506,9 @@ class AlignedFace():
|
||||||
"""
|
"""
|
||||||
retval = np.expand_dims(points, axis=1)
|
retval = np.expand_dims(points, axis=1)
|
||||||
mat = cv2.invertAffineTransform(self.adjusted_matrix) if invert else self.adjusted_matrix
|
mat = cv2.invertAffineTransform(self.adjusted_matrix) if invert else self.adjusted_matrix
|
||||||
retval = cv2.transform(retval, mat, retval.shape).squeeze()
|
retval = cv2.transform(retval, mat).squeeze()
|
||||||
logger.trace("invert: %s, Original points: %s, transformed points: %s", # type: ignore
|
logger.trace( # type:ignore[attr-defined]
|
||||||
invert, points, retval)
|
"invert: %s, Original points: %s, transformed points: %s", invert, points, retval)
|
||||||
return retval
|
return retval
|
||||||
|
|
||||||
def extract_face(self, image: np.ndarray | None) -> np.ndarray | None:
|
def extract_face(self, image: np.ndarray | None) -> np.ndarray | None:
|
||||||
|
|
@ -684,8 +528,8 @@ class AlignedFace():
|
||||||
``None`` if no image has been provided.
|
``None`` if no image has been provided.
|
||||||
"""
|
"""
|
||||||
if image is None:
|
if image is None:
|
||||||
logger.trace("_extract_face called without a loaded image. " # type: ignore
|
logger.trace("_extract_face called without a loaded " # type:ignore[attr-defined]
|
||||||
"Returning empty face.")
|
"image. Returning empty face.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if self._is_aligned and (self._centering != self._source_centering or
|
if self._is_aligned and (self._centering != self._source_centering or
|
||||||
|
|
@ -721,8 +565,9 @@ class AlignedFace():
|
||||||
:class:`numpy.ndarray`
|
:class:`numpy.ndarray`
|
||||||
The aligned image with the correct centering, scaled to image input size
|
The aligned image with the correct centering, scaled to image input size
|
||||||
"""
|
"""
|
||||||
logger.trace("image_size: %s, target_size: %s, coverage_ratio: %s", # type: ignore
|
logger.trace( # type:ignore[attr-defined]
|
||||||
image.shape[0], self.size, self._coverage_ratio)
|
"image_size: %s, target_size: %s, coverage_ratio: %s",
|
||||||
|
image.shape[0], self.size, self._coverage_ratio)
|
||||||
|
|
||||||
img_size = image.shape[0]
|
img_size = image.shape[0]
|
||||||
target_size = get_centered_size(self._source_centering,
|
target_size = get_centered_size(self._source_centering,
|
||||||
|
|
@ -733,8 +578,9 @@ class AlignedFace():
|
||||||
|
|
||||||
slices = self._get_cropped_slices(img_size, target_size)
|
slices = self._get_cropped_slices(img_size, target_size)
|
||||||
out[slices["out"][0], slices["out"][1], :] = image[slices["in"][0], slices["in"][1], :]
|
out[slices["out"][0], slices["out"][1], :] = image[slices["in"][0], slices["in"][1], :]
|
||||||
logger.trace("Cropped from aligned extract: (centering: %s, in shape: %s, " # type: ignore
|
logger.trace( # type:ignore[attr-defined]
|
||||||
"out shape: %s)", self._centering, image.shape, out.shape)
|
"Cropped from aligned extract: (centering: %s, in shape: %s, out shape: %s)",
|
||||||
|
self._centering, image.shape, out.shape)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def _get_cropped_slices(self,
|
def _get_cropped_slices(self,
|
||||||
|
|
@ -766,7 +612,7 @@ class AlignedFace():
|
||||||
slice(max(roi[0] * -1, 0),
|
slice(max(roi[0] * -1, 0),
|
||||||
target_size - min(target_size, max(0, roi[2] - image_size))))
|
target_size - min(target_size, max(0, roi[2] - image_size))))
|
||||||
self._cache.cropped_slices[self._centering] = {"in": slice_in, "out": slice_out}
|
self._cache.cropped_slices[self._centering] = {"in": slice_in, "out": slice_out}
|
||||||
logger.trace("centering: %s, cropped_slices: %s", # type: ignore
|
logger.trace("centering: %s, cropped_slices: %s", # type:ignore[attr-defined]
|
||||||
self._centering, self._cache.cropped_slices[self._centering])
|
self._centering, self._cache.cropped_slices[self._centering])
|
||||||
return self._cache.cropped_slices[self._centering]
|
return self._cache.cropped_slices[self._centering]
|
||||||
|
|
||||||
|
|
@ -805,8 +651,9 @@ class AlignedFace():
|
||||||
self._source_centering)
|
self._source_centering)
|
||||||
padding = target_size // 2
|
padding = target_size // 2
|
||||||
roi = np.array([center - padding, center + padding]).ravel()
|
roi = np.array([center - padding, center + padding]).ravel()
|
||||||
logger.trace("centering: '%s', center: %s, padding: %s, " # type: ignore
|
logger.trace( # type:ignore[attr-defined]
|
||||||
"sub roi: %s", centering, center, padding, roi)
|
"centering: '%s', center: %s, padding: %s, sub roi: %s",
|
||||||
|
centering, center, padding, roi)
|
||||||
self._cache.cropped_roi[centering] = roi
|
self._cache.cropped_roi[centering] = roi
|
||||||
return self._cache.cropped_roi[centering]
|
return self._cache.cropped_roi[centering]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -252,12 +252,14 @@ class Alignments():
|
||||||
|
|
||||||
sample_filename = next(fname for fname in self.data)
|
sample_filename = next(fname for fname in self.data)
|
||||||
basename = sample_filename[:sample_filename.rfind("_")]
|
basename = sample_filename[:sample_filename.rfind("_")]
|
||||||
logger.debug("sample filename: %s, base filename: %s", sample_filename, basename)
|
ext = os.path.splitext(sample_filename)[-1]
|
||||||
|
logger.debug("sample filename: '%s', base filename: '%s' extension: '%s'",
|
||||||
|
sample_filename, basename, ext)
|
||||||
logger.info("Saving video meta information to Alignments file")
|
logger.info("Saving video meta information to Alignments file")
|
||||||
|
|
||||||
for idx, pts in enumerate(pts_time):
|
for idx, pts in enumerate(pts_time):
|
||||||
meta: dict[str, float | int] = {"pts_time": pts, "keyframe": idx in keyframes}
|
meta: dict[str, float | int] = {"pts_time": pts, "keyframe": idx in keyframes}
|
||||||
key = f"{basename}_{idx + 1:06d}.png"
|
key = f"{basename}_{idx + 1:06d}{ext}"
|
||||||
if key not in self.data:
|
if key not in self.data:
|
||||||
self.data[key] = {"video_meta": meta, "faces": []}
|
self.data[key] = {"video_meta": meta, "faces": []}
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
111
lib/align/constants.py
Normal file
111
lib/align/constants.py
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
""" Constants that are required across faceswap's lib.align package """
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import typing as T
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
CenteringType = T.Literal["face", "head", "legacy"]
|
||||||
|
|
||||||
|
EXTRACT_RATIOS: dict[CenteringType, float] = {"legacy": 0.375, "face": 0.5, "head": 0.625}
|
||||||
|
"""dict[Literal["legacy", "face", head"] float]: The amount of padding applied to each
|
||||||
|
centering type when generating aligned faces """
|
||||||
|
|
||||||
|
|
||||||
|
class LandmarkType(Enum):
|
||||||
|
""" Enumeration for the landmark types that Faceswap supports """
|
||||||
|
LM_2D_4 = 1
|
||||||
|
LM_2D_51 = 2
|
||||||
|
LM_2D_68 = 3
|
||||||
|
LM_3D_26 = 4
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_shape(cls, shape: tuple[int, ...]) -> LandmarkType:
|
||||||
|
""" The landmark type for a given shape
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
shape: tuple[int, ...]
|
||||||
|
The shape to get the landmark type for
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Type[LandmarkType]
|
||||||
|
The enum for the given shape
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
ValueError
|
||||||
|
If the requested shape is not valid
|
||||||
|
"""
|
||||||
|
shapes: dict[tuple[int, ...], LandmarkType] = {(4, 2): cls.LM_2D_4,
|
||||||
|
(51, 2): cls.LM_2D_51,
|
||||||
|
(68, 2): cls.LM_2D_68,
|
||||||
|
(26, 3): cls.LM_3D_26}
|
||||||
|
if shape not in shapes:
|
||||||
|
raise ValueError(f"The given shape {shape} is not valid. Valid shapes: {list(shapes)}")
|
||||||
|
return shapes[shape]
|
||||||
|
|
||||||
|
|
||||||
|
_MEAN_FACE: dict[LandmarkType, np.ndarray] = {
|
||||||
|
LandmarkType.LM_2D_4: np.array(
|
||||||
|
[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]), # Clockwise from TL
|
||||||
|
LandmarkType.LM_2D_51: np.array([
|
||||||
|
[0.010086, 0.106454], [0.085135, 0.038915], [0.191003, 0.018748], [0.300643, 0.034489],
|
||||||
|
[0.403270, 0.077391], [0.596729, 0.077391], [0.699356, 0.034489], [0.808997, 0.018748],
|
||||||
|
[0.914864, 0.038915], [0.989913, 0.106454], [0.500000, 0.203352], [0.500000, 0.307009],
|
||||||
|
[0.500000, 0.409805], [0.500000, 0.515625], [0.376753, 0.587326], [0.435909, 0.609345],
|
||||||
|
[0.500000, 0.628106], [0.564090, 0.609345], [0.623246, 0.587326], [0.131610, 0.216423],
|
||||||
|
[0.196995, 0.178758], [0.275698, 0.179852], [0.344479, 0.231733], [0.270791, 0.245099],
|
||||||
|
[0.192616, 0.244077], [0.655520, 0.231733], [0.724301, 0.179852], [0.803005, 0.178758],
|
||||||
|
[0.868389, 0.216423], [0.807383, 0.244077], [0.729208, 0.245099], [0.264022, 0.780233],
|
||||||
|
[0.350858, 0.745405], [0.438731, 0.727388], [0.500000, 0.742578], [0.561268, 0.727388],
|
||||||
|
[0.649141, 0.745405], [0.735977, 0.780233], [0.652032, 0.864805], [0.566594, 0.902192],
|
||||||
|
[0.500000, 0.909281], [0.433405, 0.902192], [0.347967, 0.864805], [0.300252, 0.784792],
|
||||||
|
[0.437969, 0.778746], [0.500000, 0.785343], [0.562030, 0.778746], [0.699747, 0.784792],
|
||||||
|
[0.563237, 0.824182], [0.500000, 0.831803], [0.436763, 0.824182]]),
|
||||||
|
LandmarkType.LM_3D_26: np.array([
|
||||||
|
[4.056931, -11.432347, 1.636229], # 8 chin LL
|
||||||
|
[1.833492, -12.542305, 4.061275], # 7 chin L
|
||||||
|
[0.0, -12.901019, 4.070434], # 6 chin C
|
||||||
|
[-1.833492, -12.542305, 4.061275], # 5 chin R
|
||||||
|
[-4.056931, -11.432347, 1.636229], # 4 chin RR
|
||||||
|
[6.825897, 1.275284, 4.402142], # 33 L eyebrow L
|
||||||
|
[1.330353, 1.636816, 6.903745], # 29 L eyebrow R
|
||||||
|
[-1.330353, 1.636816, 6.903745], # 34 R eyebrow L
|
||||||
|
[-6.825897, 1.275284, 4.402142], # 38 R eyebrow R
|
||||||
|
[1.930245, -5.060977, 5.914376], # 54 nose LL
|
||||||
|
[0.746313, -5.136947, 6.263227], # 53 nose L
|
||||||
|
[0.0, -5.485328, 6.76343], # 52 nose C
|
||||||
|
[-0.746313, -5.136947, 6.263227], # 51 nose R
|
||||||
|
[-1.930245, -5.060977, 5.914376], # 50 nose RR
|
||||||
|
[5.311432, 0.0, 3.987654], # 13 L eye L
|
||||||
|
[1.78993, -0.091703, 4.413414], # 17 L eye R
|
||||||
|
[-1.78993, -0.091703, 4.413414], # 25 R eye L
|
||||||
|
[-5.311432, 0.0, 3.987654], # 21 R eye R
|
||||||
|
[2.774015, -7.566103, 5.048531], # 43 mouth L
|
||||||
|
[0.509714, -7.056507, 6.566167], # 42 mouth top L
|
||||||
|
[0.0, -7.131772, 6.704956], # 41 mouth top C
|
||||||
|
[-0.509714, -7.056507, 6.566167], # 40 mouth top R
|
||||||
|
[-2.774015, -7.566103, 5.048531], # 39 mouth R
|
||||||
|
[-0.589441, -8.443925, 6.109526], # 46 mouth bottom R
|
||||||
|
[0.0, -8.601736, 6.097667], # 45 mouth bottom C
|
||||||
|
[0.589441, -8.443925, 6.109526]])} # 44 mouth bottom L
|
||||||
|
"""dict[:class:`~LandmarkType, np.ndarray]: 'Mean' landmark points for various landmark types. Used
|
||||||
|
for aligning faces """
|
||||||
|
|
||||||
|
LANDMARK_PARTS: dict[LandmarkType, dict[str, tuple[int, int, bool]]] = {
|
||||||
|
LandmarkType.LM_2D_68: {"mouth_outer": (48, 60, True),
|
||||||
|
"mouth_inner": (60, 68, True),
|
||||||
|
"right_eyebrow": (17, 22, False),
|
||||||
|
"left_eyebrow": (22, 27, False),
|
||||||
|
"right_eye": (36, 42, True),
|
||||||
|
"left_eye": (42, 48, True),
|
||||||
|
"nose": (27, 36, False),
|
||||||
|
"jaw": (0, 17, False),
|
||||||
|
"chin": (8, 11, False)},
|
||||||
|
LandmarkType.LM_2D_4: {"face": (0, 4, True)}}
|
||||||
|
"""dict[:class:`LandmarkType`, dict[str, tuple[int, int, bool]]: For each landmark type, stores
|
||||||
|
the (start index, end index, is polygon) information about each part of the face. """
|
||||||
|
|
@ -15,7 +15,7 @@ from lib.image import encode_image, read_image
|
||||||
from lib.utils import FaceswapError
|
from lib.utils import FaceswapError
|
||||||
from .alignments import (Alignments, AlignmentFileDict, MaskAlignmentsFileDict,
|
from .alignments import (Alignments, AlignmentFileDict, MaskAlignmentsFileDict,
|
||||||
PNGHeaderAlignmentsDict, PNGHeaderDict, PNGHeaderSourceDict)
|
PNGHeaderAlignmentsDict, PNGHeaderDict, PNGHeaderSourceDict)
|
||||||
from . import AlignedFace, get_adjusted_center, get_centered_size
|
from . import AlignedFace, get_adjusted_center, get_centered_size, LANDMARK_PARTS
|
||||||
|
|
||||||
if T.TYPE_CHECKING:
|
if T.TYPE_CHECKING:
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
|
@ -231,12 +231,26 @@ class DetectedFace():
|
||||||
-------
|
-------
|
||||||
:class:`numpy.ndarray`
|
:class:`numpy.ndarray`
|
||||||
The generated landmarks mask for the selected area
|
The generated landmarks mask for the selected area
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
FaceSwapError
|
||||||
|
If the aligned face does not contain the correct landmarks to generate a landmark mask
|
||||||
"""
|
"""
|
||||||
# TODO Face mask generation from landmarks
|
# TODO Face mask generation from landmarks
|
||||||
logger.trace("area: %s, dilation: %s", area, dilation) # type:ignore[attr-defined]
|
logger.trace("area: %s, dilation: %s", area, dilation) # type:ignore[attr-defined]
|
||||||
areas = {"mouth": [slice(48, 60)], "eye": [slice(36, 42), slice(42, 48)]}
|
|
||||||
points = [self.aligned.landmarks[zone]
|
lm_type = self.aligned.landmark_type
|
||||||
for zone in areas[area]]
|
if lm_type not in LANDMARK_PARTS:
|
||||||
|
raise FaceswapError(f"Landmark based masks cannot be created for {lm_type.name}")
|
||||||
|
|
||||||
|
lm_parts = LANDMARK_PARTS[self.aligned.landmark_type]
|
||||||
|
mapped = {"mouth": ["mouth_outer"], "eye": ["right_eye", "left_eye"]}
|
||||||
|
if not all(part in lm_parts for parts in mapped.values() for part in parts):
|
||||||
|
raise FaceswapError(f"Landmark based masks cannot be created for {lm_type.name}")
|
||||||
|
|
||||||
|
areas = {key: [slice(*lm_parts[v][:2]) for v in val]for key, val in mapped.items()}
|
||||||
|
points = [self.aligned.landmarks[zone] for zone in areas[area]]
|
||||||
|
|
||||||
lmmask = LandmarksMask(points,
|
lmmask = LandmarksMask(points,
|
||||||
storage_size=self.aligned.size,
|
storage_size=self.aligned.size,
|
||||||
|
|
@ -660,9 +674,9 @@ class Mask():
|
||||||
The mask that is to be added as output from :mod:`plugins.extract.mask`.
|
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``
|
It should be in the range 0.0 - 1.0 ideally with a ``dtype`` of ``float32``
|
||||||
"""
|
"""
|
||||||
mask = (cv2.resize(mask,
|
mask = (cv2.resize(mask * 255.0,
|
||||||
(self.stored_size, self.stored_size),
|
(self.stored_size, self.stored_size),
|
||||||
interpolation=cv2.INTER_AREA) * 255.0).astype("uint8")
|
interpolation=cv2.INTER_AREA)).astype("uint8")
|
||||||
self._mask = compress(mask.tobytes())
|
self._mask = compress(mask.tobytes())
|
||||||
|
|
||||||
def set_dilation(self, amount: float) -> None:
|
def set_dilation(self, amount: float) -> None:
|
||||||
|
|
@ -903,7 +917,7 @@ class LandmarksMask(Mask):
|
||||||
mask = np.zeros((self.stored_size, self.stored_size, 1), dtype="float32")
|
mask = np.zeros((self.stored_size, self.stored_size, 1), dtype="float32")
|
||||||
for landmarks in self._points:
|
for landmarks in self._points:
|
||||||
lms = np.rint(landmarks).astype("int")
|
lms = np.rint(landmarks).astype("int")
|
||||||
cv2.fillConvexPoly(mask, cv2.convexHull(lms), 1.0, lineType=cv2.LINE_AA)
|
cv2.fillConvexPoly(mask, cv2.convexHull(lms), [1.0], lineType=cv2.LINE_AA)
|
||||||
if self._dilation[-1] is not None:
|
if self._dilation[-1] is not None:
|
||||||
self._dilate_mask(mask)
|
self._dilate_mask(mask)
|
||||||
if self._blur_kernel != 0 and self._blur_type is not None:
|
if self._blur_kernel != 0 and self._blur_type is not None:
|
||||||
|
|
|
||||||
187
lib/align/pose.py
Normal file
187
lib/align/pose.py
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
""" Holds estimated pose information for a faceswap aligned face """
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import typing as T
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from lib.logger import parse_class_init
|
||||||
|
|
||||||
|
from .constants import _MEAN_FACE, LandmarkType
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
if T.TYPE_CHECKING:
|
||||||
|
from .constants import CenteringType
|
||||||
|
|
||||||
|
|
||||||
|
class PoseEstimate():
|
||||||
|
""" Estimates pose from a generic 3D head model for the given 2D face landmarks.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
landmarks: :class:`numpy.ndarry`
|
||||||
|
The original 68 point landmarks aligned to 0.0 - 1.0 range
|
||||||
|
landmarks_type: :class:`~LandmarksType`
|
||||||
|
The type of landmarks that are generating this face
|
||||||
|
|
||||||
|
References
|
||||||
|
----------
|
||||||
|
Head Pose Estimation using OpenCV and Dlib - https://www.learnopencv.com/tag/solvepnp/
|
||||||
|
3D Model points - http://aifi.isr.uc.pt/Downloads/OpenGL/glAnthropometric3DModel.cpp
|
||||||
|
"""
|
||||||
|
_logged_once = False
|
||||||
|
|
||||||
|
def __init__(self, landmarks: np.ndarray, landmarks_type: LandmarkType) -> None:
|
||||||
|
logger.trace(parse_class_init(locals())) # type:ignore[attr-defined]
|
||||||
|
self._distortion_coefficients = np.zeros((4, 1)) # Assuming no lens distortion
|
||||||
|
self._xyz_2d: np.ndarray | None = None
|
||||||
|
|
||||||
|
if landmarks_type != LandmarkType.LM_2D_68:
|
||||||
|
self._log_once("Pose estimation is not available for non-68 point landmarks. Pose and "
|
||||||
|
"offset data will all be returned as the incorrect value of '0'")
|
||||||
|
self._landmarks_type = landmarks_type
|
||||||
|
self._camera_matrix = self._get_camera_matrix()
|
||||||
|
self._rotation, self._translation = self._solve_pnp(landmarks)
|
||||||
|
self._offset = self._get_offset()
|
||||||
|
self._pitch_yaw_roll: tuple[float, float, float] = (0, 0, 0)
|
||||||
|
logger.trace("Initialized %s", self.__class__.__name__) # type:ignore[attr-defined]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def xyz_2d(self) -> np.ndarray:
|
||||||
|
""" :class:`numpy.ndarray` projected (x, y) coordinates for each x, y, z point at a
|
||||||
|
constant distance from adjusted center of the skull (0.5, 0.5) in the 2D space. """
|
||||||
|
if self._xyz_2d is None:
|
||||||
|
xyz = cv2.projectPoints(np.array([[6., 0., -2.3],
|
||||||
|
[0., 6., -2.3],
|
||||||
|
[0., 0., 3.7]]).astype("float32"),
|
||||||
|
self._rotation,
|
||||||
|
self._translation,
|
||||||
|
self._camera_matrix,
|
||||||
|
self._distortion_coefficients)[0].squeeze()
|
||||||
|
self._xyz_2d = xyz - self._offset["head"]
|
||||||
|
return self._xyz_2d
|
||||||
|
|
||||||
|
@property
|
||||||
|
def offset(self) -> dict[CenteringType, np.ndarray]:
|
||||||
|
""" dict: The amount to offset a standard 0.0 - 1.0 umeyama transformation matrix for a
|
||||||
|
from the center of the face (between the eyes) or center of the head (middle of skull)
|
||||||
|
rather than the nose area. """
|
||||||
|
return self._offset
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pitch(self) -> float:
|
||||||
|
""" float: The pitch of the aligned face in eular angles """
|
||||||
|
if not any(self._pitch_yaw_roll):
|
||||||
|
self._get_pitch_yaw_roll()
|
||||||
|
return self._pitch_yaw_roll[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def yaw(self) -> float:
|
||||||
|
""" float: The yaw of the aligned face in eular angles """
|
||||||
|
if not any(self._pitch_yaw_roll):
|
||||||
|
self._get_pitch_yaw_roll()
|
||||||
|
return self._pitch_yaw_roll[1]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def roll(self) -> float:
|
||||||
|
""" float: The roll of the aligned face in eular angles """
|
||||||
|
if not any(self._pitch_yaw_roll):
|
||||||
|
self._get_pitch_yaw_roll()
|
||||||
|
return self._pitch_yaw_roll[2]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _log_once(cls, message: str) -> None:
|
||||||
|
""" Log a warning about unsupported landmarks if a message has not already been logged """
|
||||||
|
if cls._logged_once:
|
||||||
|
return
|
||||||
|
logger.warning(message)
|
||||||
|
cls._logged_once = True
|
||||||
|
|
||||||
|
def _get_pitch_yaw_roll(self) -> None:
|
||||||
|
""" Obtain the yaw, roll and pitch from the :attr:`_rotation` in eular angles. """
|
||||||
|
proj_matrix = np.zeros((3, 4), dtype="float32")
|
||||||
|
proj_matrix[:3, :3] = cv2.Rodrigues(self._rotation)[0]
|
||||||
|
euler = cv2.decomposeProjectionMatrix(proj_matrix)[-1]
|
||||||
|
self._pitch_yaw_roll = T.cast(tuple[float, float, float], tuple(euler.squeeze()))
|
||||||
|
logger.trace("yaw_pitch: %s", self._pitch_yaw_roll) # type:ignore[attr-defined]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_camera_matrix(cls) -> np.ndarray:
|
||||||
|
""" Obtain an estimate of the camera matrix based off the original frame dimensions.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
:class:`numpy.ndarray`
|
||||||
|
An estimated camera matrix
|
||||||
|
"""
|
||||||
|
focal_length = 4
|
||||||
|
camera_matrix = np.array([[focal_length, 0, 0.5],
|
||||||
|
[0, focal_length, 0.5],
|
||||||
|
[0, 0, 1]], dtype="double")
|
||||||
|
logger.trace("camera_matrix: %s", camera_matrix) # type:ignore[attr-defined]
|
||||||
|
return camera_matrix
|
||||||
|
|
||||||
|
def _solve_pnp(self, landmarks: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
||||||
|
""" Solve the Perspective-n-Point for the given landmarks.
|
||||||
|
|
||||||
|
Takes 2D landmarks in world space and estimates the rotation and translation vectors
|
||||||
|
in 3D space.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
landmarks: :class:`numpy.ndarry`
|
||||||
|
The original 68 point landmark co-ordinates relating to the original frame
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
rotation: :class:`numpy.ndarray`
|
||||||
|
The solved rotation vector
|
||||||
|
translation: :class:`numpy.ndarray`
|
||||||
|
The solved translation vector
|
||||||
|
"""
|
||||||
|
if self._landmarks_type != LandmarkType.LM_2D_68:
|
||||||
|
points: np.ndarray = np.empty([])
|
||||||
|
rotation = np.array([[0.0], [0.0], [0.0]])
|
||||||
|
translation = rotation.copy()
|
||||||
|
else:
|
||||||
|
points = landmarks[[6, 7, 8, 9, 10, 17, 21, 22, 26, 31, 32, 33, 34,
|
||||||
|
35, 36, 39, 42, 45, 48, 50, 51, 52, 54, 56, 57, 58]]
|
||||||
|
_, rotation, translation = cv2.solvePnP(_MEAN_FACE[LandmarkType.LM_3D_26],
|
||||||
|
points,
|
||||||
|
self._camera_matrix,
|
||||||
|
self._distortion_coefficients,
|
||||||
|
flags=cv2.SOLVEPNP_ITERATIVE)
|
||||||
|
logger.trace("points: %s, rotation: %s, translation: %s", # type:ignore[attr-defined]
|
||||||
|
points, rotation, translation)
|
||||||
|
return rotation, translation
|
||||||
|
|
||||||
|
def _get_offset(self) -> dict[CenteringType, np.ndarray]:
|
||||||
|
""" Obtain the offset between the original center of the extracted face to the new center
|
||||||
|
of the head in 2D space.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
:class:`numpy.ndarray`
|
||||||
|
The x, y offset of the new center from the old center.
|
||||||
|
"""
|
||||||
|
offset: dict[CenteringType, np.ndarray] = {"legacy": np.array([0.0, 0.0])}
|
||||||
|
if self._landmarks_type != LandmarkType.LM_2D_68:
|
||||||
|
offset["face"] = np.array([0.0, 0.0])
|
||||||
|
offset["head"] = np.array([0.0, 0.0])
|
||||||
|
else:
|
||||||
|
points: dict[T.Literal["face", "head"], tuple[float, ...]] = {"head": (0.0, 0.0, -2.3),
|
||||||
|
"face": (0.0, -1.5, 4.2)}
|
||||||
|
for key, pnts in points.items():
|
||||||
|
center = cv2.projectPoints(np.array([pnts]).astype("float32"),
|
||||||
|
self._rotation,
|
||||||
|
self._translation,
|
||||||
|
self._camera_matrix,
|
||||||
|
self._distortion_coefficients)[0].squeeze()
|
||||||
|
logger.trace("center %s: %s", key, center) # type:ignore[attr-defined]
|
||||||
|
offset[key] = center - np.array([0.5, 0.5])
|
||||||
|
logger.trace("offset: %s", offset) # type:ignore[attr-defined]
|
||||||
|
return offset
|
||||||
|
|
@ -140,7 +140,9 @@ class ExtractArgs(ExtractConvertArgs):
|
||||||
"other GPU detectors but can often return more false positives."
|
"other GPU detectors but can often return more false positives."
|
||||||
"\nL|s3fd: Best detector. Slow on CPU, faster on GPU. Can detect more faces and "
|
"\nL|s3fd: Best detector. Slow on CPU, faster on GPU. Can detect more faces and "
|
||||||
"fewer false positives than other GPU detectors, but is a lot more resource "
|
"fewer false positives than other GPU detectors, but is a lot more resource "
|
||||||
"intensive.")})
|
"intensive."
|
||||||
|
"\nL|external: Import a face detection bounding box from a json file. ("
|
||||||
|
"configurable in Detect settings)")})
|
||||||
argument_list.append({
|
argument_list.append({
|
||||||
"opts": ("-A", "--aligner"),
|
"opts": ("-A", "--aligner"),
|
||||||
"action": Radio,
|
"action": Radio,
|
||||||
|
|
@ -152,7 +154,9 @@ class ExtractArgs(ExtractConvertArgs):
|
||||||
"R|Aligner to use."
|
"R|Aligner to use."
|
||||||
"\nL|cv2-dnn: A CPU only landmark detector. Faster, less resource intensive, but "
|
"\nL|cv2-dnn: A CPU only landmark detector. Faster, less resource intensive, but "
|
||||||
"less accurate. Only use this if not using a GPU and time is important."
|
"less accurate. Only use this if not using a GPU and time is important."
|
||||||
"\nL|fan: Best aligner. Fast on GPU, slow on CPU.")})
|
"\nL|fan: Best aligner. Fast on GPU, slow on CPU."
|
||||||
|
"\nL|external: Import 68 point 2D landmarks or an aligned bounding box from a "
|
||||||
|
"json file. (configurable in Align settings)")})
|
||||||
argument_list.append({
|
argument_list.append({
|
||||||
"opts": ("-M", "--masker"),
|
"opts": ("-M", "--masker"),
|
||||||
"action": MultiOption,
|
"action": MultiOption,
|
||||||
|
|
|
||||||
|
|
@ -1253,7 +1253,8 @@ class ImagesLoader(ImageIO):
|
||||||
reader.close()
|
reader.close()
|
||||||
|
|
||||||
def _dummy_video_framename(self, index):
|
def _dummy_video_framename(self, index):
|
||||||
""" Return a dummy filename for video files
|
""" Return a dummy filename for video files. The file name is made up of:
|
||||||
|
<video_filename>_<frame_number>.<video_extension>
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
|
@ -1268,8 +1269,8 @@ class ImagesLoader(ImageIO):
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
str: A dummied filename for a video frame """
|
str: A dummied filename for a video frame """
|
||||||
vidname = os.path.splitext(os.path.basename(self.location))[0]
|
vidname, ext = os.path.splitext(os.path.basename(self.location))
|
||||||
return "{}_{:06d}.png".format(vidname, index + 1)
|
return f"{vidname}_{index + 1:06d}{ext}"
|
||||||
|
|
||||||
def _from_folder(self):
|
def _from_folder(self):
|
||||||
""" Generator for loading images from a folder
|
""" Generator for loading images from a folder
|
||||||
|
|
@ -1565,6 +1566,7 @@ class ImagesSaver(ImageIO):
|
||||||
with open(filename, "wb") as out_file:
|
with open(filename, "wb") as out_file:
|
||||||
out_file.write(image)
|
out_file.write(image)
|
||||||
else:
|
else:
|
||||||
|
assert isinstance(image, np.ndarray)
|
||||||
cv2.imwrite(filename, image)
|
cv2.imwrite(filename, image)
|
||||||
logger.trace("Saved image: '%s'", filename) # type:ignore
|
logger.trace("Saved image: '%s'", filename) # type:ignore
|
||||||
except Exception as err: # pylint:disable=broad-except
|
except Exception as err: # pylint:disable=broad-except
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,7 @@ import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
from lib.align import DetectedFace
|
from lib.align import CenteringType, DetectedFace, LandmarkType
|
||||||
from lib.align.aligned_face import CenteringType
|
|
||||||
from lib.image import read_image_batch, read_image_meta_batch
|
from lib.image import read_image_batch, read_image_meta_batch
|
||||||
from lib.utils import FaceswapError
|
from lib.utils import FaceswapError
|
||||||
|
|
||||||
|
|
@ -280,6 +279,11 @@ class _Cache():
|
||||||
The list of full paths to the images to load the metadata from
|
The list of full paths to the images to load the metadata from
|
||||||
side: str
|
side: str
|
||||||
`"a"` or `"b"`. The side of the model being cached. Used for info output
|
`"a"` or `"b"`. The side of the model being cached. Used for info output
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
FaceSwapError
|
||||||
|
If unsupported landmark type exists
|
||||||
"""
|
"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
for filename, meta in tqdm(read_image_meta_batch(filenames),
|
for filename, meta in tqdm(read_image_meta_batch(filenames),
|
||||||
|
|
@ -294,6 +298,13 @@ class _Cache():
|
||||||
# Version Check
|
# Version Check
|
||||||
self._validate_version(meta, filename)
|
self._validate_version(meta, filename)
|
||||||
detected_face = self._load_detected_face(filename, meta["alignments"])
|
detected_face = self._load_detected_face(filename, meta["alignments"])
|
||||||
|
|
||||||
|
aligned = detected_face.aligned
|
||||||
|
assert aligned is not None
|
||||||
|
if aligned.landmark_type != LandmarkType.LM_2D_68:
|
||||||
|
raise FaceswapError("68 Point facial Landmarks are required for Warp-to-"
|
||||||
|
f"landmarks. The face that failed was: '{filename}'")
|
||||||
|
|
||||||
self._cache[key] = detected_face
|
self._cache[key] = detected_face
|
||||||
self._partially_loaded.append(key)
|
self._partially_loaded.append(key)
|
||||||
|
|
||||||
|
|
@ -421,11 +432,14 @@ class _Cache():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if self._config["mask_type"] not in detected_face.mask:
|
if self._config["mask_type"] not in detected_face.mask:
|
||||||
|
exist_masks = list(detected_face.mask)
|
||||||
|
msg = "No masks exist for this face"
|
||||||
|
if exist_masks:
|
||||||
|
msg = f"The masks that exist for this face are: {exist_masks}"
|
||||||
raise FaceswapError(
|
raise FaceswapError(
|
||||||
f"You have selected the mask type '{self._config['mask_type']}' but at least one "
|
f"You have selected the mask type '{self._config['mask_type']}' but at least one "
|
||||||
"face does not contain the selected mask.\n"
|
"face does not contain the selected mask.\n"
|
||||||
f"The face that failed was: '{filename}'\n"
|
f"The face that failed was: '{filename}'\n{msg}")
|
||||||
f"The masks that exist for this face are: {list(detected_face.mask)}")
|
|
||||||
|
|
||||||
mask = detected_face.mask[str(self._config["mask_type"])]
|
mask = detected_face.mask[str(self._config["mask_type"])]
|
||||||
assert isinstance(self._config["mask_dilation"], float)
|
assert isinstance(self._config["mask_dilation"], float)
|
||||||
|
|
@ -469,7 +483,12 @@ class _Cache():
|
||||||
assert isinstance(multiplier, int)
|
assert isinstance(multiplier, int)
|
||||||
if not self._config["penalized_mask_loss"] or multiplier <= 1:
|
if not self._config["penalized_mask_loss"] or multiplier <= 1:
|
||||||
return None
|
return None
|
||||||
mask = detected_face.get_landmark_mask(area, self._size // 16, 2.5)
|
try:
|
||||||
|
mask = detected_face.get_landmark_mask(area, self._size // 16, 2.5)
|
||||||
|
except FaceswapError as err:
|
||||||
|
logger.error(str(err))
|
||||||
|
raise FaceswapError("Eye/Mouth multiplier masks could not be generated due to missing "
|
||||||
|
f"landmark data. The file that failed was: '{filename}'") from err
|
||||||
logger.trace("Caching localized '%s' mask for: %s %s", # type: ignore
|
logger.trace("Caching localized '%s' mask for: %s %s", # type: ignore
|
||||||
area, filename, mask.shape)
|
area, filename, mask.shape)
|
||||||
return mask
|
return mask
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -6,8 +6,8 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: faceswap.spanish\n"
|
"Project-Id-Version: faceswap.spanish\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-03-28 18:11+0000\n"
|
"POT-Creation-Date: 2024-04-12 11:56+0100\n"
|
||||||
"PO-Revision-Date: 2024-03-28 18:13+0000\n"
|
"PO-Revision-Date: 2024-04-12 12:02+0100\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: tokafondo\n"
|
"Language-Team: tokafondo\n"
|
||||||
"Language: es\n"
|
"Language: es\n"
|
||||||
|
|
@ -20,7 +20,7 @@ msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:46 lib/cli/args_extract_convert.py:56
|
#: lib/cli/args_extract_convert.py:46 lib/cli/args_extract_convert.py:56
|
||||||
#: lib/cli/args_extract_convert.py:64 lib/cli/args_extract_convert.py:122
|
#: lib/cli/args_extract_convert.py:64 lib/cli/args_extract_convert.py:122
|
||||||
#: lib/cli/args_extract_convert.py:479 lib/cli/args_extract_convert.py:488
|
#: lib/cli/args_extract_convert.py:483 lib/cli/args_extract_convert.py:492
|
||||||
msgid "Data"
|
msgid "Data"
|
||||||
msgstr "Datos"
|
msgstr "Datos"
|
||||||
|
|
||||||
|
|
@ -65,12 +65,12 @@ msgstr ""
|
||||||
"varios videos y/o carpetas de imágenes de las que desea extraer. Las caras "
|
"varios videos y/o carpetas de imágenes de las que desea extraer. Las caras "
|
||||||
"se enviarán a subcarpetas separadas en output_dir."
|
"se enviarán a subcarpetas separadas en output_dir."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:133 lib/cli/args_extract_convert.py:150
|
#: lib/cli/args_extract_convert.py:133 lib/cli/args_extract_convert.py:152
|
||||||
#: lib/cli/args_extract_convert.py:163 lib/cli/args_extract_convert.py:202
|
#: lib/cli/args_extract_convert.py:167 lib/cli/args_extract_convert.py:206
|
||||||
#: lib/cli/args_extract_convert.py:220 lib/cli/args_extract_convert.py:233
|
#: lib/cli/args_extract_convert.py:224 lib/cli/args_extract_convert.py:237
|
||||||
#: lib/cli/args_extract_convert.py:243 lib/cli/args_extract_convert.py:253
|
#: lib/cli/args_extract_convert.py:247 lib/cli/args_extract_convert.py:257
|
||||||
#: lib/cli/args_extract_convert.py:499 lib/cli/args_extract_convert.py:525
|
#: lib/cli/args_extract_convert.py:503 lib/cli/args_extract_convert.py:529
|
||||||
#: lib/cli/args_extract_convert.py:564
|
#: lib/cli/args_extract_convert.py:568
|
||||||
msgid "Plugins"
|
msgid "Plugins"
|
||||||
msgstr "Extensiones"
|
msgstr "Extensiones"
|
||||||
|
|
||||||
|
|
@ -84,7 +84,9 @@ msgid ""
|
||||||
"than other GPU detectors but can often return more false positives.\n"
|
"than other GPU detectors but can often return more false positives.\n"
|
||||||
"L|s3fd: Best detector. Slow on CPU, faster on GPU. Can detect more faces and "
|
"L|s3fd: Best detector. Slow on CPU, faster on GPU. Can detect more faces and "
|
||||||
"fewer false positives than other GPU detectors, but is a lot more resource "
|
"fewer false positives than other GPU detectors, but is a lot more resource "
|
||||||
"intensive."
|
"intensive.\n"
|
||||||
|
"L|external: Import a face detection bounding box from a json file. "
|
||||||
|
"(configurable in Detect settings)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"R|Detector de caras a usar. Algunos tienen ajustes configurables en '/config/"
|
"R|Detector de caras a usar. Algunos tienen ajustes configurables en '/config/"
|
||||||
"extract.ini' o 'Ajustes > Configurar Extensiones de Extracción:\n"
|
"extract.ini' o 'Ajustes > Configurar Extensiones de Extracción:\n"
|
||||||
|
|
@ -95,21 +97,28 @@ msgstr ""
|
||||||
"positivos.\n"
|
"positivos.\n"
|
||||||
"L|s3fd: El mejor detector. Lento en la CPU, y más rápido en la GPU. Puede "
|
"L|s3fd: El mejor detector. Lento en la CPU, y más rápido en la GPU. Puede "
|
||||||
"detectar más caras y tiene menos falsos positivos que otros detectores "
|
"detectar más caras y tiene menos falsos positivos que otros detectores "
|
||||||
"basados en GPU, pero uso muchos más recursos."
|
"basados en GPU, pero uso muchos más recursos.\n"
|
||||||
|
"L|external: importe un cuadro de detección de detección de cara desde un "
|
||||||
|
"archivo JSON. (configurable en la configuración de detección)"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:152
|
#: lib/cli/args_extract_convert.py:154
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|Aligner to use.\n"
|
"R|Aligner to use.\n"
|
||||||
"L|cv2-dnn: A CPU only landmark detector. Faster, less resource intensive, "
|
"L|cv2-dnn: A CPU only landmark detector. Faster, less resource intensive, "
|
||||||
"but less accurate. Only use this if not using a GPU and time is important.\n"
|
"but less accurate. Only use this if not using a GPU and time is important.\n"
|
||||||
"L|fan: Best aligner. Fast on GPU, slow on CPU."
|
"L|fan: Best aligner. Fast on GPU, slow on CPU.\n"
|
||||||
|
"L|external: Import 68 point 2D landmarks or an aligned bounding box from a "
|
||||||
|
"json file. (configurable in Align settings)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"R|Alineador a usar.\n"
|
"R|Alineador a usar.\n"
|
||||||
"L|cv2-dnn: Detector que usa sólo la CPU. Más rápido, usa menos recursos, "
|
"L|cv2-dnn: Detector que usa sólo la CPU. Más rápido, usa menos recursos, "
|
||||||
"pero es menos preciso. Elegir este si necesita rapidez y no usar la GPU.\n"
|
"pero es menos preciso. Elegir este si necesita rapidez y no usar la GPU.\n"
|
||||||
"L|fan: El mejor alineador. Rápido en la GPU, y lento en la CPU."
|
"L|fan: El mejor alineador. Rápido en la GPU, y lento en la CPU.\n"
|
||||||
|
"L|external: importar 68 puntos 2D Modos de referencia o un cuadro "
|
||||||
|
"delimitador alineado de un archivo JSON. (configurable en la configuración "
|
||||||
|
"alineada)"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:165
|
#: lib/cli/args_extract_convert.py:169
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|Additional Masker(s) to use. The masks generated here will all take up GPU "
|
"R|Additional Masker(s) to use. The masks generated here will all take up GPU "
|
||||||
"RAM. You can select none, one or multiple masks, but the extraction may take "
|
"RAM. You can select none, one or multiple masks, but the extraction may take "
|
||||||
|
|
@ -179,7 +188,7 @@ msgstr ""
|
||||||
"referencia y la máscara se extiende hacia arriba en la frente.\n"
|
"referencia y la máscara se extiende hacia arriba en la frente.\n"
|
||||||
"(eg: `-M unet-dfl vgg-clear`, `--masker vgg-obstructed`)"
|
"(eg: `-M unet-dfl vgg-clear`, `--masker vgg-obstructed`)"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:204
|
#: lib/cli/args_extract_convert.py:208
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|Performing normalization can help the aligner better align faces with "
|
"R|Performing normalization can help the aligner better align faces with "
|
||||||
"difficult lighting conditions at an extraction speed cost. Different methods "
|
"difficult lighting conditions at an extraction speed cost. Different methods "
|
||||||
|
|
@ -202,7 +211,7 @@ msgstr ""
|
||||||
"L|hist: Iguala los histogramas de los canales RGB.\n"
|
"L|hist: Iguala los histogramas de los canales RGB.\n"
|
||||||
"L|mean: Normalizar los colores de la cara a la media."
|
"L|mean: Normalizar los colores de la cara a la media."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:222
|
#: lib/cli/args_extract_convert.py:226
|
||||||
msgid ""
|
msgid ""
|
||||||
"The number of times to re-feed the detected face into the aligner. Each time "
|
"The number of times to re-feed the detected face into the aligner. Each time "
|
||||||
"the face is re-fed into the aligner the bounding box is adjusted by a small "
|
"the face is re-fed into the aligner the bounding box is adjusted by a small "
|
||||||
|
|
@ -219,7 +228,7 @@ msgstr ""
|
||||||
"más veces se vuelva a introducir la cara en el alineador, menos "
|
"más veces se vuelva a introducir la cara en el alineador, menos "
|
||||||
"microfluctuaciones se producirán, pero la extracción será más larga."
|
"microfluctuaciones se producirán, pero la extracción será más larga."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:235
|
#: lib/cli/args_extract_convert.py:239
|
||||||
msgid ""
|
msgid ""
|
||||||
"Re-feed the initially found aligned face through the aligner. Can help "
|
"Re-feed the initially found aligned face through the aligner. Can help "
|
||||||
"produce better alignments for faces that are rotated beyond 45 degrees in "
|
"produce better alignments for faces that are rotated beyond 45 degrees in "
|
||||||
|
|
@ -230,7 +239,7 @@ msgstr ""
|
||||||
"se giran más de 45 grados en el marco o se encuentran en ángulos extremos. "
|
"se giran más de 45 grados en el marco o se encuentran en ángulos extremos. "
|
||||||
"Ralentiza la extracción."
|
"Ralentiza la extracción."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:245
|
#: lib/cli/args_extract_convert.py:249
|
||||||
msgid ""
|
msgid ""
|
||||||
"If a face isn't found, rotate the images to try to find a face. Can find "
|
"If a face isn't found, rotate the images to try to find a face. Can find "
|
||||||
"more faces at the cost of extraction speed. Pass in a single number to use "
|
"more faces at the cost of extraction speed. Pass in a single number to use "
|
||||||
|
|
@ -242,7 +251,7 @@ msgstr ""
|
||||||
"un solo número para usar incrementos de ese tamaño hasta 360, o pase una "
|
"un solo número para usar incrementos de ese tamaño hasta 360, o pase una "
|
||||||
"lista de números para enumerar exactamente qué ángulos comprobar."
|
"lista de números para enumerar exactamente qué ángulos comprobar."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:255
|
#: lib/cli/args_extract_convert.py:259
|
||||||
msgid ""
|
msgid ""
|
||||||
"Obtain and store face identity encodings from VGGFace2. Slows down extract a "
|
"Obtain and store face identity encodings from VGGFace2. Slows down extract a "
|
||||||
"little, but will save time if using 'sort by face'"
|
"little, but will save time if using 'sort by face'"
|
||||||
|
|
@ -250,15 +259,15 @@ msgstr ""
|
||||||
"Obtenga y almacene codificaciones de identidad facial de VGGFace2. Ralentiza "
|
"Obtenga y almacene codificaciones de identidad facial de VGGFace2. Ralentiza "
|
||||||
"un poco la extracción, pero ahorrará tiempo si usa 'sort by face'"
|
"un poco la extracción, pero ahorrará tiempo si usa 'sort by face'"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:265 lib/cli/args_extract_convert.py:276
|
#: lib/cli/args_extract_convert.py:269 lib/cli/args_extract_convert.py:280
|
||||||
#: lib/cli/args_extract_convert.py:289 lib/cli/args_extract_convert.py:303
|
#: lib/cli/args_extract_convert.py:293 lib/cli/args_extract_convert.py:307
|
||||||
#: lib/cli/args_extract_convert.py:610 lib/cli/args_extract_convert.py:619
|
#: lib/cli/args_extract_convert.py:614 lib/cli/args_extract_convert.py:623
|
||||||
#: lib/cli/args_extract_convert.py:634 lib/cli/args_extract_convert.py:647
|
#: lib/cli/args_extract_convert.py:638 lib/cli/args_extract_convert.py:651
|
||||||
#: lib/cli/args_extract_convert.py:661
|
#: lib/cli/args_extract_convert.py:665
|
||||||
msgid "Face Processing"
|
msgid "Face Processing"
|
||||||
msgstr "Proceso de Caras"
|
msgstr "Proceso de Caras"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:267
|
#: lib/cli/args_extract_convert.py:271
|
||||||
msgid ""
|
msgid ""
|
||||||
"Filters out faces detected below this size. Length, in pixels across the "
|
"Filters out faces detected below this size. Length, in pixels across the "
|
||||||
"diagonal of the bounding box. Set to 0 for off"
|
"diagonal of the bounding box. Set to 0 for off"
|
||||||
|
|
@ -267,7 +276,7 @@ msgstr ""
|
||||||
"a lo largo de la diagonal del cuadro delimitador. Establecer a 0 para "
|
"a lo largo de la diagonal del cuadro delimitador. Establecer a 0 para "
|
||||||
"desactivar"
|
"desactivar"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:278
|
#: lib/cli/args_extract_convert.py:282
|
||||||
msgid ""
|
msgid ""
|
||||||
"Optionally filter out people who you do not wish to extract by passing in "
|
"Optionally filter out people who you do not wish to extract by passing in "
|
||||||
"images of those people. Should be a small variety of images at different "
|
"images of those people. Should be a small variety of images at different "
|
||||||
|
|
@ -280,7 +289,7 @@ msgstr ""
|
||||||
"contenga las imágenes requeridas o múltiples archivos de imágenes, separados "
|
"contenga las imágenes requeridas o múltiples archivos de imágenes, separados "
|
||||||
"por espacios."
|
"por espacios."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:291
|
#: lib/cli/args_extract_convert.py:295
|
||||||
msgid ""
|
msgid ""
|
||||||
"Optionally select people you wish to extract by passing in images of that "
|
"Optionally select people you wish to extract by passing in images of that "
|
||||||
"person. Should be a small variety of images at different angles and in "
|
"person. Should be a small variety of images at different angles and in "
|
||||||
|
|
@ -293,7 +302,7 @@ msgstr ""
|
||||||
"contenga las imágenes requeridas o múltiples archivos de imágenes, separados "
|
"contenga las imágenes requeridas o múltiples archivos de imágenes, separados "
|
||||||
"por espacios."
|
"por espacios."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:305
|
#: lib/cli/args_extract_convert.py:309
|
||||||
msgid ""
|
msgid ""
|
||||||
"For use with the optional nfilter/filter files. Threshold for positive face "
|
"For use with the optional nfilter/filter files. Threshold for positive face "
|
||||||
"recognition. Higher values are stricter."
|
"recognition. Higher values are stricter."
|
||||||
|
|
@ -301,12 +310,12 @@ msgstr ""
|
||||||
"Para usar con los archivos nfilter/filter opcionales. Umbral para el "
|
"Para usar con los archivos nfilter/filter opcionales. Umbral para el "
|
||||||
"reconocimiento facial positivo. Los valores más altos son más estrictos."
|
"reconocimiento facial positivo. Los valores más altos son más estrictos."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:314 lib/cli/args_extract_convert.py:327
|
#: lib/cli/args_extract_convert.py:318 lib/cli/args_extract_convert.py:331
|
||||||
#: lib/cli/args_extract_convert.py:340 lib/cli/args_extract_convert.py:352
|
#: lib/cli/args_extract_convert.py:344 lib/cli/args_extract_convert.py:356
|
||||||
msgid "output"
|
msgid "output"
|
||||||
msgstr "salida"
|
msgstr "salida"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:316
|
#: lib/cli/args_extract_convert.py:320
|
||||||
msgid ""
|
msgid ""
|
||||||
"The output size of extracted faces. Make sure that the model you intend to "
|
"The output size of extracted faces. Make sure that the model you intend to "
|
||||||
"train supports your required size. This will only need to be changed for hi-"
|
"train supports your required size. This will only need to be changed for hi-"
|
||||||
|
|
@ -316,7 +325,7 @@ msgstr ""
|
||||||
"pretende entrenar admite el tamaño deseado. Esto sólo tendrá que ser "
|
"pretende entrenar admite el tamaño deseado. Esto sólo tendrá que ser "
|
||||||
"cambiado para los modelos de alta resolución."
|
"cambiado para los modelos de alta resolución."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:329
|
#: lib/cli/args_extract_convert.py:333
|
||||||
msgid ""
|
msgid ""
|
||||||
"Extract every 'nth' frame. This option will skip frames when extracting "
|
"Extract every 'nth' frame. This option will skip frames when extracting "
|
||||||
"faces. For example a value of 1 will extract faces from every frame, a value "
|
"faces. For example a value of 1 will extract faces from every frame, a value "
|
||||||
|
|
@ -326,7 +335,7 @@ msgstr ""
|
||||||
"extraer las caras. Por ejemplo, un valor de 1 extraerá las caras de cada "
|
"extraer las caras. Por ejemplo, un valor de 1 extraerá las caras de cada "
|
||||||
"fotograma, un valor de 10 extraerá las caras de cada 10 fotogramas."
|
"fotograma, un valor de 10 extraerá las caras de cada 10 fotogramas."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:342
|
#: lib/cli/args_extract_convert.py:346
|
||||||
msgid ""
|
msgid ""
|
||||||
"Automatically save the alignments file after a set amount of frames. By "
|
"Automatically save the alignments file after a set amount of frames. By "
|
||||||
"default the alignments file is only saved at the end of the extraction "
|
"default the alignments file is only saved at the end of the extraction "
|
||||||
|
|
@ -342,20 +351,19 @@ msgstr ""
|
||||||
"ADVERTENCIA: No interrumpa el script al escribir el archivo porque podría "
|
"ADVERTENCIA: No interrumpa el script al escribir el archivo porque podría "
|
||||||
"corromperse. Poner a 0 para desactivar"
|
"corromperse. Poner a 0 para desactivar"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:353
|
#: lib/cli/args_extract_convert.py:357
|
||||||
msgid "Draw landmarks on the ouput faces for debugging purposes."
|
msgid "Draw landmarks on the ouput faces for debugging purposes."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Dibujar puntos de referencia en las caras de salida para fines de depuración."
|
"Dibujar puntos de referencia en las caras de salida para fines de depuración."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:359 lib/cli/args_extract_convert.py:369
|
#: lib/cli/args_extract_convert.py:363 lib/cli/args_extract_convert.py:373
|
||||||
#: lib/cli/args_extract_convert.py:377 lib/cli/args_extract_convert.py:384
|
#: lib/cli/args_extract_convert.py:381 lib/cli/args_extract_convert.py:388
|
||||||
#: lib/cli/args_extract_convert.py:674 lib/cli/args_extract_convert.py:686
|
#: lib/cli/args_extract_convert.py:678 lib/cli/args_extract_convert.py:691
|
||||||
#: lib/cli/args_extract_convert.py:695 lib/cli/args_extract_convert.py:716
|
#: lib/cli/args_extract_convert.py:712 lib/cli/args_extract_convert.py:718
|
||||||
#: lib/cli/args_extract_convert.py:722
|
|
||||||
msgid "settings"
|
msgid "settings"
|
||||||
msgstr "ajustes"
|
msgstr "ajustes"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:361
|
#: lib/cli/args_extract_convert.py:365
|
||||||
msgid ""
|
msgid ""
|
||||||
"Don't run extraction in parallel. Will run each part of the extraction "
|
"Don't run extraction in parallel. Will run each part of the extraction "
|
||||||
"process separately (one after the other) rather than all at the same time. "
|
"process separately (one after the other) rather than all at the same time. "
|
||||||
|
|
@ -365,7 +373,7 @@ msgstr ""
|
||||||
"extracción por separado (una tras otra) en lugar de hacerlo todo al mismo "
|
"extracción por separado (una tras otra) en lugar de hacerlo todo al mismo "
|
||||||
"tiempo. Útil si la VRAM es escasa."
|
"tiempo. Útil si la VRAM es escasa."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:371
|
#: lib/cli/args_extract_convert.py:375
|
||||||
msgid ""
|
msgid ""
|
||||||
"Skips frames that have already been extracted and exist in the alignments "
|
"Skips frames that have already been extracted and exist in the alignments "
|
||||||
"file"
|
"file"
|
||||||
|
|
@ -373,19 +381,19 @@ msgstr ""
|
||||||
"Omite los fotogramas que ya han sido extraídos y que existen en el archivo "
|
"Omite los fotogramas que ya han sido extraídos y que existen en el archivo "
|
||||||
"de alineaciones"
|
"de alineaciones"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:378
|
#: lib/cli/args_extract_convert.py:382
|
||||||
msgid "Skip frames that already have detected faces in the alignments file"
|
msgid "Skip frames that already have detected faces in the alignments file"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Omitir los fotogramas que ya tienen caras detectadas en el archivo de "
|
"Omitir los fotogramas que ya tienen caras detectadas en el archivo de "
|
||||||
"alineaciones"
|
"alineaciones"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:385
|
#: lib/cli/args_extract_convert.py:389
|
||||||
msgid "Skip saving the detected faces to disk. Just create an alignments file"
|
msgid "Skip saving the detected faces to disk. Just create an alignments file"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"No guardar las caras detectadas en el disco. Crear sólo un archivo de "
|
"No guardar las caras detectadas en el disco. Crear sólo un archivo de "
|
||||||
"alineaciones"
|
"alineaciones"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:459
|
#: lib/cli/args_extract_convert.py:463
|
||||||
msgid ""
|
msgid ""
|
||||||
"Swap the original faces in a source video/images to your final faces.\n"
|
"Swap the original faces in a source video/images to your final faces.\n"
|
||||||
"Conversion plugins can be configured in the 'Settings' Menu"
|
"Conversion plugins can be configured in the 'Settings' Menu"
|
||||||
|
|
@ -395,7 +403,7 @@ msgstr ""
|
||||||
"Los plugins de conversión pueden ser configurados en el menú "
|
"Los plugins de conversión pueden ser configurados en el menú "
|
||||||
"\"Configuración\""
|
"\"Configuración\""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:481
|
#: lib/cli/args_extract_convert.py:485
|
||||||
msgid ""
|
msgid ""
|
||||||
"Only required if converting from images to video. Provide The original video "
|
"Only required if converting from images to video. Provide The original video "
|
||||||
"that the source frames were extracted from (for extracting the fps and "
|
"that the source frames were extracted from (for extracting the fps and "
|
||||||
|
|
@ -405,7 +413,7 @@ msgstr ""
|
||||||
"original del que se extrajeron los fotogramas de origen (para extraer los "
|
"original del que se extrajeron los fotogramas de origen (para extraer los "
|
||||||
"fps y el audio)."
|
"fps y el audio)."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:490
|
#: lib/cli/args_extract_convert.py:494
|
||||||
msgid ""
|
msgid ""
|
||||||
"Model directory. The directory containing the trained model you wish to use "
|
"Model directory. The directory containing the trained model you wish to use "
|
||||||
"for conversion."
|
"for conversion."
|
||||||
|
|
@ -413,7 +421,7 @@ msgstr ""
|
||||||
"Directorio del modelo. El directorio que contiene el modelo entrenado que "
|
"Directorio del modelo. El directorio que contiene el modelo entrenado que "
|
||||||
"desea utilizar para la conversión."
|
"desea utilizar para la conversión."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:501
|
#: lib/cli/args_extract_convert.py:505
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|Performs color adjustment to the swapped face. Some of these options have "
|
"R|Performs color adjustment to the swapped face. Some of these options have "
|
||||||
"configurable settings in '/config/convert.ini' or 'Settings > Configure "
|
"configurable settings in '/config/convert.ini' or 'Settings > Configure "
|
||||||
|
|
@ -453,7 +461,7 @@ msgstr ""
|
||||||
"colores. Generalmente no da resultados muy satisfactorios.\n"
|
"colores. Generalmente no da resultados muy satisfactorios.\n"
|
||||||
"L|none: No realice el ajuste de color."
|
"L|none: No realice el ajuste de color."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:527
|
#: lib/cli/args_extract_convert.py:531
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|Masker to use. NB: The mask you require must exist within the alignments "
|
"R|Masker to use. NB: The mask you require must exist within the alignments "
|
||||||
"file. You can add additional masks with the Mask Tool.\n"
|
"file. You can add additional masks with the Mask Tool.\n"
|
||||||
|
|
@ -529,7 +537,7 @@ msgstr ""
|
||||||
"L|predicted: Si la opción 'Learn Mask' se habilitó durante el entrenamiento, "
|
"L|predicted: Si la opción 'Learn Mask' se habilitó durante el entrenamiento, "
|
||||||
"esto usará la máscara que fue creada por el modelo entrenado."
|
"esto usará la máscara que fue creada por el modelo entrenado."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:566
|
#: lib/cli/args_extract_convert.py:570
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|The plugin to use to output the converted images. The writers are "
|
"R|The plugin to use to output the converted images. The writers are "
|
||||||
"configurable in '/config/convert.ini' or 'Settings > Configure Convert "
|
"configurable in '/config/convert.ini' or 'Settings > Configure Convert "
|
||||||
|
|
@ -562,12 +570,12 @@ msgstr ""
|
||||||
"L|pillow: [images] Más lento que opencv, pero tiene más opciones y soporta "
|
"L|pillow: [images] Más lento que opencv, pero tiene más opciones y soporta "
|
||||||
"más formatos."
|
"más formatos."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:587 lib/cli/args_extract_convert.py:596
|
#: lib/cli/args_extract_convert.py:591 lib/cli/args_extract_convert.py:600
|
||||||
#: lib/cli/args_extract_convert.py:707
|
#: lib/cli/args_extract_convert.py:703
|
||||||
msgid "Frame Processing"
|
msgid "Frame Processing"
|
||||||
msgstr "Proceso de fotogramas"
|
msgstr "Proceso de fotogramas"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:589
|
#: lib/cli/args_extract_convert.py:593
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"Scale the final output frames by this amount. 100%% will output the frames "
|
"Scale the final output frames by this amount. 100%% will output the frames "
|
||||||
|
|
@ -577,7 +585,7 @@ msgstr ""
|
||||||
"a los fotogramas a las dimensiones de origen. 50%% a la mitad de tamaño. "
|
"a los fotogramas a las dimensiones de origen. 50%% a la mitad de tamaño. "
|
||||||
"200%% al doble de tamaño"
|
"200%% al doble de tamaño"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:598
|
#: lib/cli/args_extract_convert.py:602
|
||||||
msgid ""
|
msgid ""
|
||||||
"Frame ranges to apply transfer to e.g. For frames 10 to 50 and 90 to 100 use "
|
"Frame ranges to apply transfer to e.g. For frames 10 to 50 and 90 to 100 use "
|
||||||
"--frame-ranges 10-50 90-100. Frames falling outside of the selected range "
|
"--frame-ranges 10-50 90-100. Frames falling outside of the selected range "
|
||||||
|
|
@ -591,7 +599,7 @@ msgstr ""
|
||||||
"imágenes, ¡los nombres de los archivos deben terminar con el número de "
|
"imágenes, ¡los nombres de los archivos deben terminar con el número de "
|
||||||
"fotograma!"
|
"fotograma!"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:612
|
#: lib/cli/args_extract_convert.py:616
|
||||||
msgid ""
|
msgid ""
|
||||||
"Scale the swapped face by this percentage. Positive values will enlarge the "
|
"Scale the swapped face by this percentage. Positive values will enlarge the "
|
||||||
"face, Negative values will shrink the face."
|
"face, Negative values will shrink the face."
|
||||||
|
|
@ -599,7 +607,7 @@ msgstr ""
|
||||||
"Escale la cara intercambiada según este porcentaje. Los valores positivos "
|
"Escale la cara intercambiada según este porcentaje. Los valores positivos "
|
||||||
"agrandarán la cara, los valores negativos la reducirán."
|
"agrandarán la cara, los valores negativos la reducirán."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:621
|
#: lib/cli/args_extract_convert.py:625
|
||||||
msgid ""
|
msgid ""
|
||||||
"If you have not cleansed your alignments file, then you can filter out faces "
|
"If you have not cleansed your alignments file, then you can filter out faces "
|
||||||
"by defining a folder here that contains the faces extracted from your input "
|
"by defining a folder here that contains the faces extracted from your input "
|
||||||
|
|
@ -615,7 +623,7 @@ msgstr ""
|
||||||
"especificada. Si se deja en blanco, se convertirán todas las caras que "
|
"especificada. Si se deja en blanco, se convertirán todas las caras que "
|
||||||
"existan en el archivo de alineaciones."
|
"existan en el archivo de alineaciones."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:636
|
#: lib/cli/args_extract_convert.py:640
|
||||||
msgid ""
|
msgid ""
|
||||||
"Optionally filter out people who you do not wish to process by passing in an "
|
"Optionally filter out people who you do not wish to process by passing in an "
|
||||||
"image of that person. Should be a front portrait with a single person in the "
|
"image of that person. Should be a front portrait with a single person in the "
|
||||||
|
|
@ -629,7 +637,7 @@ msgstr ""
|
||||||
"uso del filtro de caras disminuirá significativamente la velocidad de "
|
"uso del filtro de caras disminuirá significativamente la velocidad de "
|
||||||
"extracción y no se puede garantizar su precisión."
|
"extracción y no se puede garantizar su precisión."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:649
|
#: lib/cli/args_extract_convert.py:653
|
||||||
msgid ""
|
msgid ""
|
||||||
"Optionally select people you wish to process by passing in an image of that "
|
"Optionally select people you wish to process by passing in an image of that "
|
||||||
"person. Should be a front portrait with a single person in the image. "
|
"person. Should be a front portrait with a single person in the image. "
|
||||||
|
|
@ -643,7 +651,7 @@ msgstr ""
|
||||||
"del filtro facial disminuirá significativamente la velocidad de extracción y "
|
"del filtro facial disminuirá significativamente la velocidad de extracción y "
|
||||||
"no se puede garantizar su precisión."
|
"no se puede garantizar su precisión."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:663
|
#: lib/cli/args_extract_convert.py:667
|
||||||
msgid ""
|
msgid ""
|
||||||
"For use with the optional nfilter/filter files. Threshold for positive face "
|
"For use with the optional nfilter/filter files. Threshold for positive face "
|
||||||
"recognition. Lower values are stricter. NB: Using face filter will "
|
"recognition. Lower values are stricter. NB: Using face filter will "
|
||||||
|
|
@ -655,7 +663,7 @@ msgstr ""
|
||||||
"NB: El uso del filtro facial disminuirá significativamente la velocidad de "
|
"NB: El uso del filtro facial disminuirá significativamente la velocidad de "
|
||||||
"extracción y no se puede garantizar su precisión."
|
"extracción y no se puede garantizar su precisión."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:676
|
#: lib/cli/args_extract_convert.py:680
|
||||||
msgid ""
|
msgid ""
|
||||||
"The maximum number of parallel processes for performing conversion. "
|
"The maximum number of parallel processes for performing conversion. "
|
||||||
"Converting images is system RAM heavy so it is possible to run out of memory "
|
"Converting images is system RAM heavy so it is possible to run out of memory "
|
||||||
|
|
@ -672,15 +680,7 @@ msgstr ""
|
||||||
"procesos que los disponibles en su sistema. Si 'singleprocess' está "
|
"procesos que los disponibles en su sistema. Si 'singleprocess' está "
|
||||||
"habilitado, este ajuste será ignorado."
|
"habilitado, este ajuste será ignorado."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:688
|
#: lib/cli/args_extract_convert.py:693
|
||||||
msgid ""
|
|
||||||
"[LEGACY] This only needs to be selected if a legacy model is being loaded or "
|
|
||||||
"if there are multiple models in the model folder"
|
|
||||||
msgstr ""
|
|
||||||
"[LEGACY] Sólo es necesario seleccionar esta opción si se está cargando un "
|
|
||||||
"modelo heredado si hay varios modelos en la carpeta de modelos"
|
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:697
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"Enable On-The-Fly Conversion. NOT recommended. You should generate a clean "
|
"Enable On-The-Fly Conversion. NOT recommended. You should generate a clean "
|
||||||
"alignments file for your destination video. However, if you wish you can "
|
"alignments file for your destination video. However, if you wish you can "
|
||||||
|
|
@ -695,7 +695,7 @@ msgstr ""
|
||||||
"de baja calidad. Si se encuentra un archivo de alineaciones, esta opción "
|
"de baja calidad. Si se encuentra un archivo de alineaciones, esta opción "
|
||||||
"será ignorada."
|
"será ignorada."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:709
|
#: lib/cli/args_extract_convert.py:705
|
||||||
msgid ""
|
msgid ""
|
||||||
"When used with --frame-ranges outputs the unchanged frames that are not "
|
"When used with --frame-ranges outputs the unchanged frames that are not "
|
||||||
"processed instead of discarding them."
|
"processed instead of discarding them."
|
||||||
|
|
@ -703,11 +703,18 @@ msgstr ""
|
||||||
"Cuando se usa con --frame-ranges, la salida incluye los fotogramas no "
|
"Cuando se usa con --frame-ranges, la salida incluye los fotogramas no "
|
||||||
"procesados en vez de descartarlos."
|
"procesados en vez de descartarlos."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:717
|
#: lib/cli/args_extract_convert.py:713
|
||||||
msgid "Swap the model. Instead converting from of A -> B, converts B -> A"
|
msgid "Swap the model. Instead converting from of A -> B, converts B -> A"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Intercambiar el modelo. En vez de convertir de A a B, convierte de B a A"
|
"Intercambiar el modelo. En vez de convertir de A a B, convierte de B a A"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:723
|
#: lib/cli/args_extract_convert.py:719
|
||||||
msgid "Disable multiprocessing. Slower but less resource intensive."
|
msgid "Disable multiprocessing. Slower but less resource intensive."
|
||||||
msgstr "Desactiva el multiproceso. Es más lento, pero usa menos recursos."
|
msgstr "Desactiva el multiproceso. Es más lento, pero usa menos recursos."
|
||||||
|
|
||||||
|
#~ msgid ""
|
||||||
|
#~ "[LEGACY] This only needs to be selected if a legacy model is being loaded "
|
||||||
|
#~ "or if there are multiple models in the model folder"
|
||||||
|
#~ msgstr ""
|
||||||
|
#~ "[LEGACY] Sólo es necesario seleccionar esta opción si se está cargando un "
|
||||||
|
#~ "modelo heredado si hay varios modelos en la carpeta de modelos"
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -6,8 +6,8 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: faceswap.spanish\n"
|
"Project-Id-Version: faceswap.spanish\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-03-28 23:49+0000\n"
|
"POT-Creation-Date: 2024-04-12 12:10+0100\n"
|
||||||
"PO-Revision-Date: 2024-03-29 00:02+0000\n"
|
"PO-Revision-Date: 2024-04-12 12:14+0100\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: tokafondo\n"
|
"Language-Team: tokafondo\n"
|
||||||
"Language: es_ES\n"
|
"Language: es_ES\n"
|
||||||
|
|
@ -65,17 +65,23 @@ msgstr ""
|
||||||
msgid " Use the output option (-o) to process results."
|
msgid " Use the output option (-o) to process results."
|
||||||
msgstr " Usar la opción de salida (-o) para procesar los resultados."
|
msgstr " Usar la opción de salida (-o) para procesar los resultados."
|
||||||
|
|
||||||
#: tools/alignments/cli.py:57 tools/alignments/cli.py:97
|
#: tools/alignments/cli.py:58 tools/alignments/cli.py:104
|
||||||
msgid "processing"
|
msgid "processing"
|
||||||
msgstr "proceso"
|
msgstr "proceso"
|
||||||
|
|
||||||
#: tools/alignments/cli.py:60
|
#: tools/alignments/cli.py:61
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|Choose which action you want to perform. NB: All actions require an "
|
"R|Choose which action you want to perform. NB: All actions require an "
|
||||||
"alignments file (-a) to be passed in.\n"
|
"alignments file (-a) to be passed in.\n"
|
||||||
"L|'draw': Draw landmarks on frames in the selected folder/video. A subfolder "
|
"L|'draw': Draw landmarks on frames in the selected folder/video. A subfolder "
|
||||||
"will be created within the frames folder to hold the output.{0}\n"
|
"will be created within the frames folder to hold the output.{0}\n"
|
||||||
|
"L|'export': Export the contents of an alignments file to a json file. Can be "
|
||||||
|
"used for editing alignment information in external tools and then re-"
|
||||||
|
"importing by using Faceswap's Extract 'Import' plugins. Note: masks and "
|
||||||
|
"identity vectors will not be included in the exported file, so will be re-"
|
||||||
|
"generated when the json file is imported back into Faceswap. All data is "
|
||||||
|
"exported with the origin (0, 0) at the top left of the canvas.\n"
|
||||||
"L|'extract': Re-extract faces from the source frames/video based on "
|
"L|'extract': Re-extract faces from the source frames/video based on "
|
||||||
"alignment data. This is a lot quicker than re-detecting faces. Can pass in "
|
"alignment data. This is a lot quicker than re-detecting faces. Can pass in "
|
||||||
"the '-een' (--extract-every-n) parameter to only extract every nth frame."
|
"the '-een' (--extract-every-n) parameter to only extract every nth frame."
|
||||||
|
|
@ -110,6 +116,14 @@ msgstr ""
|
||||||
"L|'draw': Dibuja puntos de referencia en los fotogramas de la carpeta o "
|
"L|'draw': Dibuja puntos de referencia en los fotogramas de la carpeta o "
|
||||||
"vídeo seleccionado. Se creará una subcarpeta dentro de la carpeta de "
|
"vídeo seleccionado. Se creará una subcarpeta dentro de la carpeta de "
|
||||||
"fotogramas para guardar el resultado.{0}\n"
|
"fotogramas para guardar el resultado.{0}\n"
|
||||||
|
"L|'export': Exportar el contenido de un archivo de alineaciones a un archivo "
|
||||||
|
"JSON. Se puede utilizar para editar información de alineación en "
|
||||||
|
"herramientas externas y luego volver a importar mediante el uso de "
|
||||||
|
"complementos de 'import' de extracto de Faceswap. Nota: Las máscaras y los "
|
||||||
|
"vectores de identidad no se incluirán en el archivo exportado, por lo que se "
|
||||||
|
"volverán a generar cuando el archivo JSON se importe a FacesWap. Todos los "
|
||||||
|
"datos se exportan con el origen (0, 0) en la parte superior izquierda del "
|
||||||
|
"lienzo.\n"
|
||||||
"L|'extract': Reextrae las caras de los fotogramas o vídeos de origen "
|
"L|'extract': Reextrae las caras de los fotogramas o vídeos de origen "
|
||||||
"basándose en los datos de alineación. Esto es mucho más rápido que volver a "
|
"basándose en los datos de alineación. Esto es mucho más rápido que volver a "
|
||||||
"detectar las caras. Se puede pasar el parámetro '-een' (--extract-every-n) "
|
"detectar las caras. Se puede pasar el parámetro '-een' (--extract-every-n) "
|
||||||
|
|
@ -142,7 +156,7 @@ msgstr ""
|
||||||
"L|'spatial': Realiza un filtrado espacial y temporal para suavizar las "
|
"L|'spatial': Realiza un filtrado espacial y temporal para suavizar las "
|
||||||
"alineaciones (¡EXPERIMENTAL!)"
|
"alineaciones (¡EXPERIMENTAL!)"
|
||||||
|
|
||||||
#: tools/alignments/cli.py:100
|
#: tools/alignments/cli.py:107
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|How to output discovered items ('faces' and 'frames' only):\n"
|
"R|How to output discovered items ('faces' and 'frames' only):\n"
|
||||||
"L|'console': Print the list of frames to the screen. (DEFAULT)\n"
|
"L|'console': Print the list of frames to the screen. (DEFAULT)\n"
|
||||||
|
|
@ -158,12 +172,12 @@ msgstr ""
|
||||||
"L|'move': Mueve los elementos descubiertos a una subcarpeta dentro del "
|
"L|'move': Mueve los elementos descubiertos a una subcarpeta dentro del "
|
||||||
"directorio de origen."
|
"directorio de origen."
|
||||||
|
|
||||||
#: tools/alignments/cli.py:111 tools/alignments/cli.py:134
|
#: tools/alignments/cli.py:118 tools/alignments/cli.py:141
|
||||||
#: tools/alignments/cli.py:141
|
#: tools/alignments/cli.py:148
|
||||||
msgid "data"
|
msgid "data"
|
||||||
msgstr "datos"
|
msgstr "datos"
|
||||||
|
|
||||||
#: tools/alignments/cli.py:118
|
#: tools/alignments/cli.py:125
|
||||||
msgid ""
|
msgid ""
|
||||||
"Full path to the alignments file to be processed. If you have input a "
|
"Full path to the alignments file to be processed. If you have input a "
|
||||||
"'frames_dir' and don't provide this option, the process will try to find the "
|
"'frames_dir' and don't provide this option, the process will try to find the "
|
||||||
|
|
@ -177,13 +191,13 @@ msgstr ""
|
||||||
"requieren un archivo de alineaciones con la excepción de 'from-faces' cuando "
|
"requieren un archivo de alineaciones con la excepción de 'from-faces' cuando "
|
||||||
"el archivo de alineaciones se generará en la carpeta de caras especificada."
|
"el archivo de alineaciones se generará en la carpeta de caras especificada."
|
||||||
|
|
||||||
#: tools/alignments/cli.py:135
|
#: tools/alignments/cli.py:142
|
||||||
msgid "Directory containing source frames that faces were extracted from."
|
msgid "Directory containing source frames that faces were extracted from."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Directorio que contiene los fotogramas de origen de los que se extrajeron "
|
"Directorio que contiene los fotogramas de origen de los que se extrajeron "
|
||||||
"las caras."
|
"las caras."
|
||||||
|
|
||||||
#: tools/alignments/cli.py:143
|
#: tools/alignments/cli.py:150
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|Run the aligmnents tool on multiple sources. The following jobs support "
|
"R|Run the aligmnents tool on multiple sources. The following jobs support "
|
||||||
"batch mode:\n"
|
"batch mode:\n"
|
||||||
|
|
@ -226,12 +240,12 @@ msgstr ""
|
||||||
"El archivo de alineaciones debe existir en la ubicación predeterminada. Para "
|
"El archivo de alineaciones debe existir en la ubicación predeterminada. Para "
|
||||||
"todos los demás trabajos, esta opción se ignora."
|
"todos los demás trabajos, esta opción se ignora."
|
||||||
|
|
||||||
#: tools/alignments/cli.py:169 tools/alignments/cli.py:181
|
#: tools/alignments/cli.py:176 tools/alignments/cli.py:188
|
||||||
#: tools/alignments/cli.py:191
|
#: tools/alignments/cli.py:198
|
||||||
msgid "extract"
|
msgid "extract"
|
||||||
msgstr "extracción"
|
msgstr "extracción"
|
||||||
|
|
||||||
#: tools/alignments/cli.py:171
|
#: tools/alignments/cli.py:178
|
||||||
msgid ""
|
msgid ""
|
||||||
"[Extract only] Extract every 'nth' frame. This option will skip frames when "
|
"[Extract only] Extract every 'nth' frame. This option will skip frames when "
|
||||||
"extracting faces. For example a value of 1 will extract faces from every "
|
"extracting faces. For example a value of 1 will extract faces from every "
|
||||||
|
|
@ -242,11 +256,11 @@ msgstr ""
|
||||||
"caras de cada fotograma, un valor de 10 extraerá las caras de cada 10 "
|
"caras de cada fotograma, un valor de 10 extraerá las caras de cada 10 "
|
||||||
"fotogramas."
|
"fotogramas."
|
||||||
|
|
||||||
#: tools/alignments/cli.py:182
|
#: tools/alignments/cli.py:189
|
||||||
msgid "[Extract only] The output size of extracted faces."
|
msgid "[Extract only] The output size of extracted faces."
|
||||||
msgstr "[Sólo extracción] El tamaño de salida de las caras extraídas."
|
msgstr "[Sólo extracción] El tamaño de salida de las caras extraídas."
|
||||||
|
|
||||||
#: tools/alignments/cli.py:193
|
#: tools/alignments/cli.py:200
|
||||||
msgid ""
|
msgid ""
|
||||||
"[Extract only] Only extract faces that have been resized by this percent or "
|
"[Extract only] Only extract faces that have been resized by this percent or "
|
||||||
"more to meet the specified extract size (`-sz`, `--size`). Useful for "
|
"more to meet the specified extract size (`-sz`, `--size`). Useful for "
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -7,8 +7,8 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-03-28 18:11+0000\n"
|
"POT-Creation-Date: 2024-04-12 11:56+0100\n"
|
||||||
"PO-Revision-Date: 2024-03-28 18:16+0000\n"
|
"PO-Revision-Date: 2024-04-12 12:00+0100\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: \n"
|
"Language-Team: \n"
|
||||||
"Language: ko_KR\n"
|
"Language: ko_KR\n"
|
||||||
|
|
@ -20,7 +20,7 @@ msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:46 lib/cli/args_extract_convert.py:56
|
#: lib/cli/args_extract_convert.py:46 lib/cli/args_extract_convert.py:56
|
||||||
#: lib/cli/args_extract_convert.py:64 lib/cli/args_extract_convert.py:122
|
#: lib/cli/args_extract_convert.py:64 lib/cli/args_extract_convert.py:122
|
||||||
#: lib/cli/args_extract_convert.py:479 lib/cli/args_extract_convert.py:488
|
#: lib/cli/args_extract_convert.py:483 lib/cli/args_extract_convert.py:492
|
||||||
msgid "Data"
|
msgid "Data"
|
||||||
msgstr "데이터"
|
msgstr "데이터"
|
||||||
|
|
||||||
|
|
@ -63,12 +63,12 @@ msgstr ""
|
||||||
"또는 이미지들을 가진 부모 폴더가 되야 합니다. 얼굴들은 output_dir에 분리된 하"
|
"또는 이미지들을 가진 부모 폴더가 되야 합니다. 얼굴들은 output_dir에 분리된 하"
|
||||||
"위 폴더에 저장됩니다."
|
"위 폴더에 저장됩니다."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:133 lib/cli/args_extract_convert.py:150
|
#: lib/cli/args_extract_convert.py:133 lib/cli/args_extract_convert.py:152
|
||||||
#: lib/cli/args_extract_convert.py:163 lib/cli/args_extract_convert.py:202
|
#: lib/cli/args_extract_convert.py:167 lib/cli/args_extract_convert.py:206
|
||||||
#: lib/cli/args_extract_convert.py:220 lib/cli/args_extract_convert.py:233
|
#: lib/cli/args_extract_convert.py:224 lib/cli/args_extract_convert.py:237
|
||||||
#: lib/cli/args_extract_convert.py:243 lib/cli/args_extract_convert.py:253
|
#: lib/cli/args_extract_convert.py:247 lib/cli/args_extract_convert.py:257
|
||||||
#: lib/cli/args_extract_convert.py:499 lib/cli/args_extract_convert.py:525
|
#: lib/cli/args_extract_convert.py:503 lib/cli/args_extract_convert.py:529
|
||||||
#: lib/cli/args_extract_convert.py:564
|
#: lib/cli/args_extract_convert.py:568
|
||||||
msgid "Plugins"
|
msgid "Plugins"
|
||||||
msgstr "플러그인들"
|
msgstr "플러그인들"
|
||||||
|
|
||||||
|
|
@ -82,7 +82,9 @@ msgid ""
|
||||||
"than other GPU detectors but can often return more false positives.\n"
|
"than other GPU detectors but can often return more false positives.\n"
|
||||||
"L|s3fd: Best detector. Slow on CPU, faster on GPU. Can detect more faces and "
|
"L|s3fd: Best detector. Slow on CPU, faster on GPU. Can detect more faces and "
|
||||||
"fewer false positives than other GPU detectors, but is a lot more resource "
|
"fewer false positives than other GPU detectors, but is a lot more resource "
|
||||||
"intensive."
|
"intensive.\n"
|
||||||
|
"L|external: Import a face detection bounding box from a json file. "
|
||||||
|
"(configurable in Detect settings)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"R|사용할 감지기. 몇몇 감지기들은 '/config/extract.ini' 또는 '설정 > 추출 플러"
|
"R|사용할 감지기. 몇몇 감지기들은 '/config/extract.ini' 또는 '설정 > 추출 플러"
|
||||||
"그인 설정'에서 설정이 가능합니다:\n"
|
"그인 설정'에서 설정이 가능합니다:\n"
|
||||||
|
|
@ -93,21 +95,27 @@ msgstr ""
|
||||||
"니다.\n"
|
"니다.\n"
|
||||||
"L|s3fd: 가장 좋은 감지기. CPU에선 느리고 GPU에선 빠릅니다. 다른 GPU 감지기들"
|
"L|s3fd: 가장 좋은 감지기. CPU에선 느리고 GPU에선 빠릅니다. 다른 GPU 감지기들"
|
||||||
"보다 더 많은 얼굴들을 감지할 수 있고 과 더 적은 false positives를 돌려주지만 "
|
"보다 더 많은 얼굴들을 감지할 수 있고 과 더 적은 false positives를 돌려주지만 "
|
||||||
"자원을 굉장히 많이 사용합니다."
|
"자원을 굉장히 많이 사용합니다.\n"
|
||||||
|
"L|external: JSON 파일에서 얼굴 감지 경계 박스를 가져옵니다. (설정 감지에서 구"
|
||||||
|
"성 가능)"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:152
|
#: lib/cli/args_extract_convert.py:154
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|Aligner to use.\n"
|
"R|Aligner to use.\n"
|
||||||
"L|cv2-dnn: A CPU only landmark detector. Faster, less resource intensive, "
|
"L|cv2-dnn: A CPU only landmark detector. Faster, less resource intensive, "
|
||||||
"but less accurate. Only use this if not using a GPU and time is important.\n"
|
"but less accurate. Only use this if not using a GPU and time is important.\n"
|
||||||
"L|fan: Best aligner. Fast on GPU, slow on CPU."
|
"L|fan: Best aligner. Fast on GPU, slow on CPU.\n"
|
||||||
|
"L|external: Import 68 point 2D landmarks or an aligned bounding box from a "
|
||||||
|
"json file. (configurable in Align settings)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"R|사용할 Aligner.\n"
|
"R|사용할 Aligner.\n"
|
||||||
"L|cv2-dnn: CPU만을 사용하는 특징점 감지기. 빠르고 자원을 덜 사용하지만 부정확"
|
"L|cv2-dnn: CPU만을 사용하는 특징점 감지기. 빠르고 자원을 덜 사용하지만 부정확"
|
||||||
"합니다. GPU를 사용하지 않고 시간이 중요할 때에만 사용하세요.\n"
|
"합니다. GPU를 사용하지 않고 시간이 중요할 때에만 사용하세요.\n"
|
||||||
"L|fan: 가장 좋은 aligner. GPU에선 빠르고 CPU에선 느립니다."
|
"L|fan: 가장 좋은 aligner. GPU에선 빠르고 CPU에선 느립니다.\n"
|
||||||
|
"L|external: JSON 파일에서 68 포인트 2D 랜드 마크 또는 정렬 된 경계 상자를 가"
|
||||||
|
"져옵니다. (정렬 설정에서 구성 가능)"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:165
|
#: lib/cli/args_extract_convert.py:169
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|Additional Masker(s) to use. The masks generated here will all take up GPU "
|
"R|Additional Masker(s) to use. The masks generated here will all take up GPU "
|
||||||
"RAM. You can select none, one or multiple masks, but the extraction may take "
|
"RAM. You can select none, one or multiple masks, but the extraction may take "
|
||||||
|
|
@ -168,7 +176,7 @@ msgstr ""
|
||||||
"로 뻗어 있습ㄴ다.\n"
|
"로 뻗어 있습ㄴ다.\n"
|
||||||
"(예: '-M unet-dfl vgg-clear', '--masker vgg-obstructed')"
|
"(예: '-M unet-dfl vgg-clear', '--masker vgg-obstructed')"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:204
|
#: lib/cli/args_extract_convert.py:208
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|Performing normalization can help the aligner better align faces with "
|
"R|Performing normalization can help the aligner better align faces with "
|
||||||
"difficult lighting conditions at an extraction speed cost. Different methods "
|
"difficult lighting conditions at an extraction speed cost. Different methods "
|
||||||
|
|
@ -189,7 +197,7 @@ msgstr ""
|
||||||
"L|hist: RGB 채널의 히스토그램을 동일하게 합니다.\n"
|
"L|hist: RGB 채널의 히스토그램을 동일하게 합니다.\n"
|
||||||
"L|mean: 얼굴 색상을 평균으로 정규화합니다."
|
"L|mean: 얼굴 색상을 평균으로 정규화합니다."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:222
|
#: lib/cli/args_extract_convert.py:226
|
||||||
msgid ""
|
msgid ""
|
||||||
"The number of times to re-feed the detected face into the aligner. Each time "
|
"The number of times to re-feed the detected face into the aligner. Each time "
|
||||||
"the face is re-fed into the aligner the bounding box is adjusted by a small "
|
"the face is re-fed into the aligner the bounding box is adjusted by a small "
|
||||||
|
|
@ -204,7 +212,7 @@ msgstr ""
|
||||||
"다. 얼굴이 aligner에 다시 공급되는 횟수가 많을수록 micro-jitter 적게 발생하지"
|
"다. 얼굴이 aligner에 다시 공급되는 횟수가 많을수록 micro-jitter 적게 발생하지"
|
||||||
"만 추출에 더 오랜 시간이 걸립니다."
|
"만 추출에 더 오랜 시간이 걸립니다."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:235
|
#: lib/cli/args_extract_convert.py:239
|
||||||
msgid ""
|
msgid ""
|
||||||
"Re-feed the initially found aligned face through the aligner. Can help "
|
"Re-feed the initially found aligned face through the aligner. Can help "
|
||||||
"produce better alignments for faces that are rotated beyond 45 degrees in "
|
"produce better alignments for faces that are rotated beyond 45 degrees in "
|
||||||
|
|
@ -214,7 +222,7 @@ msgstr ""
|
||||||
"회전하거나 극단적인 각도에 있는 얼굴을 더 잘 정렬할 수 있습니다. 추출 속도가 "
|
"회전하거나 극단적인 각도에 있는 얼굴을 더 잘 정렬할 수 있습니다. 추출 속도가 "
|
||||||
"느려집니다."
|
"느려집니다."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:245
|
#: lib/cli/args_extract_convert.py:249
|
||||||
msgid ""
|
msgid ""
|
||||||
"If a face isn't found, rotate the images to try to find a face. Can find "
|
"If a face isn't found, rotate the images to try to find a face. Can find "
|
||||||
"more faces at the cost of extraction speed. Pass in a single number to use "
|
"more faces at the cost of extraction speed. Pass in a single number to use "
|
||||||
|
|
@ -225,7 +233,7 @@ msgstr ""
|
||||||
"면서 더 많은 얼굴을 찾을 수 있습니다. 단일 숫자를 입력하여 해당 크기의 증분"
|
"면서 더 많은 얼굴을 찾을 수 있습니다. 단일 숫자를 입력하여 해당 크기의 증분"
|
||||||
"을 360까지 사용하거나 숫자 목록을 입력하여 확인할 각도를 정확하게 열거합니다."
|
"을 360까지 사용하거나 숫자 목록을 입력하여 확인할 각도를 정확하게 열거합니다."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:255
|
#: lib/cli/args_extract_convert.py:259
|
||||||
msgid ""
|
msgid ""
|
||||||
"Obtain and store face identity encodings from VGGFace2. Slows down extract a "
|
"Obtain and store face identity encodings from VGGFace2. Slows down extract a "
|
||||||
"little, but will save time if using 'sort by face'"
|
"little, but will save time if using 'sort by face'"
|
||||||
|
|
@ -233,15 +241,15 @@ msgstr ""
|
||||||
"VGGFace2에서 얼굴 식별 인코딩을 가져와 저장합니다. 추출 속도를 약간 늦추지만 "
|
"VGGFace2에서 얼굴 식별 인코딩을 가져와 저장합니다. 추출 속도를 약간 늦추지만 "
|
||||||
"'얼굴별로 정렬'을 사용하면 시간을 절약할 수 있습니다."
|
"'얼굴별로 정렬'을 사용하면 시간을 절약할 수 있습니다."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:265 lib/cli/args_extract_convert.py:276
|
#: lib/cli/args_extract_convert.py:269 lib/cli/args_extract_convert.py:280
|
||||||
#: lib/cli/args_extract_convert.py:289 lib/cli/args_extract_convert.py:303
|
#: lib/cli/args_extract_convert.py:293 lib/cli/args_extract_convert.py:307
|
||||||
#: lib/cli/args_extract_convert.py:610 lib/cli/args_extract_convert.py:619
|
#: lib/cli/args_extract_convert.py:614 lib/cli/args_extract_convert.py:623
|
||||||
#: lib/cli/args_extract_convert.py:634 lib/cli/args_extract_convert.py:647
|
#: lib/cli/args_extract_convert.py:638 lib/cli/args_extract_convert.py:651
|
||||||
#: lib/cli/args_extract_convert.py:661
|
#: lib/cli/args_extract_convert.py:665
|
||||||
msgid "Face Processing"
|
msgid "Face Processing"
|
||||||
msgstr "얼굴 처리"
|
msgstr "얼굴 처리"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:267
|
#: lib/cli/args_extract_convert.py:271
|
||||||
msgid ""
|
msgid ""
|
||||||
"Filters out faces detected below this size. Length, in pixels across the "
|
"Filters out faces detected below this size. Length, in pixels across the "
|
||||||
"diagonal of the bounding box. Set to 0 for off"
|
"diagonal of the bounding box. Set to 0 for off"
|
||||||
|
|
@ -249,7 +257,7 @@ msgstr ""
|
||||||
"이 크기 미만으로 탐지된 얼굴을 필터링합니다. 길이, 경계 상자의 대각선에 걸친 "
|
"이 크기 미만으로 탐지된 얼굴을 필터링합니다. 길이, 경계 상자의 대각선에 걸친 "
|
||||||
"픽셀 단위입니다. 0으로 설정하면 꺼집니다"
|
"픽셀 단위입니다. 0으로 설정하면 꺼집니다"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:278
|
#: lib/cli/args_extract_convert.py:282
|
||||||
msgid ""
|
msgid ""
|
||||||
"Optionally filter out people who you do not wish to extract by passing in "
|
"Optionally filter out people who you do not wish to extract by passing in "
|
||||||
"images of those people. Should be a small variety of images at different "
|
"images of those people. Should be a small variety of images at different "
|
||||||
|
|
@ -261,7 +269,7 @@ msgstr ""
|
||||||
"지들 또는 공백으로 구분된 여러 이미지 파일이 들어 있는 폴더를 선택할 수 있습"
|
"지들 또는 공백으로 구분된 여러 이미지 파일이 들어 있는 폴더를 선택할 수 있습"
|
||||||
"니다."
|
"니다."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:291
|
#: lib/cli/args_extract_convert.py:295
|
||||||
msgid ""
|
msgid ""
|
||||||
"Optionally select people you wish to extract by passing in images of that "
|
"Optionally select people you wish to extract by passing in images of that "
|
||||||
"person. Should be a small variety of images at different angles and in "
|
"person. Should be a small variety of images at different angles and in "
|
||||||
|
|
@ -272,7 +280,7 @@ msgstr ""
|
||||||
"와 조건이 다른 작은 다양한 이미지여야 합니다. 추출할 때 필요한 이미지들 또는 "
|
"와 조건이 다른 작은 다양한 이미지여야 합니다. 추출할 때 필요한 이미지들 또는 "
|
||||||
"공백으로 구분된 여러 이미지 파일이 들어 있는 폴더를 선택할 수 있습니다."
|
"공백으로 구분된 여러 이미지 파일이 들어 있는 폴더를 선택할 수 있습니다."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:305
|
#: lib/cli/args_extract_convert.py:309
|
||||||
msgid ""
|
msgid ""
|
||||||
"For use with the optional nfilter/filter files. Threshold for positive face "
|
"For use with the optional nfilter/filter files. Threshold for positive face "
|
||||||
"recognition. Higher values are stricter."
|
"recognition. Higher values are stricter."
|
||||||
|
|
@ -280,12 +288,12 @@ msgstr ""
|
||||||
"옵션인 nfilter/filter 파일과 함께 사용합니다. 긍정적인 얼굴 인식을 위한 임계"
|
"옵션인 nfilter/filter 파일과 함께 사용합니다. 긍정적인 얼굴 인식을 위한 임계"
|
||||||
"값. 값이 높을수록 엄격합니다."
|
"값. 값이 높을수록 엄격합니다."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:314 lib/cli/args_extract_convert.py:327
|
#: lib/cli/args_extract_convert.py:318 lib/cli/args_extract_convert.py:331
|
||||||
#: lib/cli/args_extract_convert.py:340 lib/cli/args_extract_convert.py:352
|
#: lib/cli/args_extract_convert.py:344 lib/cli/args_extract_convert.py:356
|
||||||
msgid "output"
|
msgid "output"
|
||||||
msgstr "출력"
|
msgstr "출력"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:316
|
#: lib/cli/args_extract_convert.py:320
|
||||||
msgid ""
|
msgid ""
|
||||||
"The output size of extracted faces. Make sure that the model you intend to "
|
"The output size of extracted faces. Make sure that the model you intend to "
|
||||||
"train supports your required size. This will only need to be changed for hi-"
|
"train supports your required size. This will only need to be changed for hi-"
|
||||||
|
|
@ -294,7 +302,7 @@ msgstr ""
|
||||||
"추출된 얼굴의 출력 크기입니다. 훈련하려는 모델이 필요한 크기를 지원하는지 꼭 "
|
"추출된 얼굴의 출력 크기입니다. 훈련하려는 모델이 필요한 크기를 지원하는지 꼭 "
|
||||||
"확인하세요. 이것은 고해상도 모델에 대해서만 변경하면 됩니다."
|
"확인하세요. 이것은 고해상도 모델에 대해서만 변경하면 됩니다."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:329
|
#: lib/cli/args_extract_convert.py:333
|
||||||
msgid ""
|
msgid ""
|
||||||
"Extract every 'nth' frame. This option will skip frames when extracting "
|
"Extract every 'nth' frame. This option will skip frames when extracting "
|
||||||
"faces. For example a value of 1 will extract faces from every frame, a value "
|
"faces. For example a value of 1 will extract faces from every frame, a value "
|
||||||
|
|
@ -304,7 +312,7 @@ msgstr ""
|
||||||
"설정합니다. 예를 들어, 값이 1이면 모든 프레임에서 얼굴이 추출되고, 값이 10이"
|
"설정합니다. 예를 들어, 값이 1이면 모든 프레임에서 얼굴이 추출되고, 값이 10이"
|
||||||
"면 모든 10번째 프레임에서 얼굴이 추출됩니다."
|
"면 모든 10번째 프레임에서 얼굴이 추출됩니다."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:342
|
#: lib/cli/args_extract_convert.py:346
|
||||||
msgid ""
|
msgid ""
|
||||||
"Automatically save the alignments file after a set amount of frames. By "
|
"Automatically save the alignments file after a set amount of frames. By "
|
||||||
"default the alignments file is only saved at the end of the extraction "
|
"default the alignments file is only saved at the end of the extraction "
|
||||||
|
|
@ -319,19 +327,18 @@ msgstr ""
|
||||||
"을 쓸 때 스크립트가 손상될 수 있으므로 스크립트를 중단하지 마십시오. 해제하려"
|
"을 쓸 때 스크립트가 손상될 수 있으므로 스크립트를 중단하지 마십시오. 해제하려"
|
||||||
"면 0으로 설정"
|
"면 0으로 설정"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:353
|
#: lib/cli/args_extract_convert.py:357
|
||||||
msgid "Draw landmarks on the ouput faces for debugging purposes."
|
msgid "Draw landmarks on the ouput faces for debugging purposes."
|
||||||
msgstr "디버깅을 위해 출력 얼굴에 특징점을 그립니다."
|
msgstr "디버깅을 위해 출력 얼굴에 특징점을 그립니다."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:359 lib/cli/args_extract_convert.py:369
|
#: lib/cli/args_extract_convert.py:363 lib/cli/args_extract_convert.py:373
|
||||||
#: lib/cli/args_extract_convert.py:377 lib/cli/args_extract_convert.py:384
|
#: lib/cli/args_extract_convert.py:381 lib/cli/args_extract_convert.py:388
|
||||||
#: lib/cli/args_extract_convert.py:674 lib/cli/args_extract_convert.py:686
|
#: lib/cli/args_extract_convert.py:678 lib/cli/args_extract_convert.py:691
|
||||||
#: lib/cli/args_extract_convert.py:695 lib/cli/args_extract_convert.py:716
|
#: lib/cli/args_extract_convert.py:712 lib/cli/args_extract_convert.py:718
|
||||||
#: lib/cli/args_extract_convert.py:722
|
|
||||||
msgid "settings"
|
msgid "settings"
|
||||||
msgstr "설정"
|
msgstr "설정"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:361
|
#: lib/cli/args_extract_convert.py:365
|
||||||
msgid ""
|
msgid ""
|
||||||
"Don't run extraction in parallel. Will run each part of the extraction "
|
"Don't run extraction in parallel. Will run each part of the extraction "
|
||||||
"process separately (one after the other) rather than all at the same time. "
|
"process separately (one after the other) rather than all at the same time. "
|
||||||
|
|
@ -341,22 +348,22 @@ msgstr ""
|
||||||
"는 것이 아니라 개별적으로(하나씩) 실행합니다. VRAM이 프리미엄인 경우 유용합니"
|
"는 것이 아니라 개별적으로(하나씩) 실행합니다. VRAM이 프리미엄인 경우 유용합니"
|
||||||
"다."
|
"다."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:371
|
#: lib/cli/args_extract_convert.py:375
|
||||||
msgid ""
|
msgid ""
|
||||||
"Skips frames that have already been extracted and exist in the alignments "
|
"Skips frames that have already been extracted and exist in the alignments "
|
||||||
"file"
|
"file"
|
||||||
msgstr "이미 추출되었거나 alignments 파일에 존재하는 프레임들을 스킵합니다"
|
msgstr "이미 추출되었거나 alignments 파일에 존재하는 프레임들을 스킵합니다"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:378
|
#: lib/cli/args_extract_convert.py:382
|
||||||
msgid "Skip frames that already have detected faces in the alignments file"
|
msgid "Skip frames that already have detected faces in the alignments file"
|
||||||
msgstr "이미 얼굴을 탐지하여 alignments 파일에 존재하는 프레임들을 스킵합니다"
|
msgstr "이미 얼굴을 탐지하여 alignments 파일에 존재하는 프레임들을 스킵합니다"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:385
|
#: lib/cli/args_extract_convert.py:389
|
||||||
msgid "Skip saving the detected faces to disk. Just create an alignments file"
|
msgid "Skip saving the detected faces to disk. Just create an alignments file"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"탐지된 얼굴을 디스크에 저장하지 않습니다. 그저 alignments 파일을 만듭니다"
|
"탐지된 얼굴을 디스크에 저장하지 않습니다. 그저 alignments 파일을 만듭니다"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:459
|
#: lib/cli/args_extract_convert.py:463
|
||||||
msgid ""
|
msgid ""
|
||||||
"Swap the original faces in a source video/images to your final faces.\n"
|
"Swap the original faces in a source video/images to your final faces.\n"
|
||||||
"Conversion plugins can be configured in the 'Settings' Menu"
|
"Conversion plugins can be configured in the 'Settings' Menu"
|
||||||
|
|
@ -364,7 +371,7 @@ msgstr ""
|
||||||
"원본 비디오/이미지의 원래 얼굴을 최종 얼굴으로 바꿉니다.\n"
|
"원본 비디오/이미지의 원래 얼굴을 최종 얼굴으로 바꿉니다.\n"
|
||||||
"변환 플러그인은 '설정' 메뉴에서 구성할 수 있습니다"
|
"변환 플러그인은 '설정' 메뉴에서 구성할 수 있습니다"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:481
|
#: lib/cli/args_extract_convert.py:485
|
||||||
msgid ""
|
msgid ""
|
||||||
"Only required if converting from images to video. Provide The original video "
|
"Only required if converting from images to video. Provide The original video "
|
||||||
"that the source frames were extracted from (for extracting the fps and "
|
"that the source frames were extracted from (for extracting the fps and "
|
||||||
|
|
@ -373,14 +380,14 @@ msgstr ""
|
||||||
"이미지에서 비디오로 변환하는 경우에만 필요합니다. 소스 프레임이 추출된 원본 "
|
"이미지에서 비디오로 변환하는 경우에만 필요합니다. 소스 프레임이 추출된 원본 "
|
||||||
"비디오(fps 및 오디오 추출용)를 입력하세요."
|
"비디오(fps 및 오디오 추출용)를 입력하세요."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:490
|
#: lib/cli/args_extract_convert.py:494
|
||||||
msgid ""
|
msgid ""
|
||||||
"Model directory. The directory containing the trained model you wish to use "
|
"Model directory. The directory containing the trained model you wish to use "
|
||||||
"for conversion."
|
"for conversion."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"모델 폴더. 당신이 변환에 사용하고자 하는 훈련된 모델을 가진 폴더입니다."
|
"모델 폴더. 당신이 변환에 사용하고자 하는 훈련된 모델을 가진 폴더입니다."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:501
|
#: lib/cli/args_extract_convert.py:505
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|Performs color adjustment to the swapped face. Some of these options have "
|
"R|Performs color adjustment to the swapped face. Some of these options have "
|
||||||
"configurable settings in '/config/convert.ini' or 'Settings > Configure "
|
"configurable settings in '/config/convert.ini' or 'Settings > Configure "
|
||||||
|
|
@ -416,7 +423,7 @@ msgstr ""
|
||||||
"공하지 않습니다.\n"
|
"공하지 않습니다.\n"
|
||||||
"L|none: 색상 조정을 수행하지 않습니다."
|
"L|none: 색상 조정을 수행하지 않습니다."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:527
|
#: lib/cli/args_extract_convert.py:531
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|Masker to use. NB: The mask you require must exist within the alignments "
|
"R|Masker to use. NB: The mask you require must exist within the alignments "
|
||||||
"file. You can add additional masks with the Mask Tool.\n"
|
"file. You can add additional masks with the Mask Tool.\n"
|
||||||
|
|
@ -480,7 +487,7 @@ msgstr ""
|
||||||
"L|predicted: 교육 중에 'Learn Mask(마스크 학습)' 옵션이 활성화된 경우에는 교"
|
"L|predicted: 교육 중에 'Learn Mask(마스크 학습)' 옵션이 활성화된 경우에는 교"
|
||||||
"육을 받은 모델이 만든 마스크가 사용됩니다."
|
"육을 받은 모델이 만든 마스크가 사용됩니다."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:566
|
#: lib/cli/args_extract_convert.py:570
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|The plugin to use to output the converted images. The writers are "
|
"R|The plugin to use to output the converted images. The writers are "
|
||||||
"configurable in '/config/convert.ini' or 'Settings > Configure Convert "
|
"configurable in '/config/convert.ini' or 'Settings > Configure Convert "
|
||||||
|
|
@ -510,12 +517,12 @@ msgstr ""
|
||||||
"L|pillow: [images] opencv보다 느리지만 더 많은 옵션이 있고 더 많은 형식을 지"
|
"L|pillow: [images] opencv보다 느리지만 더 많은 옵션이 있고 더 많은 형식을 지"
|
||||||
"원합니다."
|
"원합니다."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:587 lib/cli/args_extract_convert.py:596
|
#: lib/cli/args_extract_convert.py:591 lib/cli/args_extract_convert.py:600
|
||||||
#: lib/cli/args_extract_convert.py:707
|
#: lib/cli/args_extract_convert.py:703
|
||||||
msgid "Frame Processing"
|
msgid "Frame Processing"
|
||||||
msgstr "프레임 처리"
|
msgstr "프레임 처리"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:589
|
#: lib/cli/args_extract_convert.py:593
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"Scale the final output frames by this amount. 100%% will output the frames "
|
"Scale the final output frames by this amount. 100%% will output the frames "
|
||||||
|
|
@ -524,7 +531,7 @@ msgstr ""
|
||||||
"최종 출력 프레임의 크기를 이 양만큼 조정합니다. 100%%는 원본의 차원에서 프레"
|
"최종 출력 프레임의 크기를 이 양만큼 조정합니다. 100%%는 원본의 차원에서 프레"
|
||||||
"임을 출력합니다. 50%%는 절반 크기에서, 200%%는 두 배 크기에서"
|
"임을 출력합니다. 50%%는 절반 크기에서, 200%%는 두 배 크기에서"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:598
|
#: lib/cli/args_extract_convert.py:602
|
||||||
msgid ""
|
msgid ""
|
||||||
"Frame ranges to apply transfer to e.g. For frames 10 to 50 and 90 to 100 use "
|
"Frame ranges to apply transfer to e.g. For frames 10 to 50 and 90 to 100 use "
|
||||||
"--frame-ranges 10-50 90-100. Frames falling outside of the selected range "
|
"--frame-ranges 10-50 90-100. Frames falling outside of the selected range "
|
||||||
|
|
@ -536,7 +543,7 @@ msgstr ""
|
||||||
"으면 선택한 범위를 벗어나는 프레임이 삭제됩니다. NB: 이미지에서 변환하는 경"
|
"으면 선택한 범위를 벗어나는 프레임이 삭제됩니다. NB: 이미지에서 변환하는 경"
|
||||||
"우 파일 이름은 프레임 번호로 끝나야 합니다!"
|
"우 파일 이름은 프레임 번호로 끝나야 합니다!"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:612
|
#: lib/cli/args_extract_convert.py:616
|
||||||
msgid ""
|
msgid ""
|
||||||
"Scale the swapped face by this percentage. Positive values will enlarge the "
|
"Scale the swapped face by this percentage. Positive values will enlarge the "
|
||||||
"face, Negative values will shrink the face."
|
"face, Negative values will shrink the face."
|
||||||
|
|
@ -544,7 +551,7 @@ msgstr ""
|
||||||
"이 백분율로 교체된 면의 크기를 조정합니다. 양수 값은 얼굴을 확대하고, 음수 값"
|
"이 백분율로 교체된 면의 크기를 조정합니다. 양수 값은 얼굴을 확대하고, 음수 값"
|
||||||
"은 얼굴을 축소합니다."
|
"은 얼굴을 축소합니다."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:621
|
#: lib/cli/args_extract_convert.py:625
|
||||||
msgid ""
|
msgid ""
|
||||||
"If you have not cleansed your alignments file, then you can filter out faces "
|
"If you have not cleansed your alignments file, then you can filter out faces "
|
||||||
"by defining a folder here that contains the faces extracted from your input "
|
"by defining a folder here that contains the faces extracted from your input "
|
||||||
|
|
@ -558,7 +565,7 @@ msgstr ""
|
||||||
"alignments 파일 내에 존재하거나 지정된 폴더 내에 존재하는 얼굴만 변환됩니다. "
|
"alignments 파일 내에 존재하거나 지정된 폴더 내에 존재하는 얼굴만 변환됩니다. "
|
||||||
"이 항목을 공백으로 두면 alignments 파일 내에 있는 모든 얼굴이 변환됩니다."
|
"이 항목을 공백으로 두면 alignments 파일 내에 있는 모든 얼굴이 변환됩니다."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:636
|
#: lib/cli/args_extract_convert.py:640
|
||||||
msgid ""
|
msgid ""
|
||||||
"Optionally filter out people who you do not wish to process by passing in an "
|
"Optionally filter out people who you do not wish to process by passing in an "
|
||||||
"image of that person. Should be a front portrait with a single person in the "
|
"image of that person. Should be a front portrait with a single person in the "
|
||||||
|
|
@ -571,7 +578,7 @@ msgstr ""
|
||||||
"분하여 추가할 수 있습니다. 주의: 얼굴 필터를 사용하면 추출 속도가 현저히 감소"
|
"분하여 추가할 수 있습니다. 주의: 얼굴 필터를 사용하면 추출 속도가 현저히 감소"
|
||||||
"하므로 정확성을 보장할 수 없습니다."
|
"하므로 정확성을 보장할 수 없습니다."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:649
|
#: lib/cli/args_extract_convert.py:653
|
||||||
msgid ""
|
msgid ""
|
||||||
"Optionally select people you wish to process by passing in an image of that "
|
"Optionally select people you wish to process by passing in an image of that "
|
||||||
"person. Should be a front portrait with a single person in the image. "
|
"person. Should be a front portrait with a single person in the image. "
|
||||||
|
|
@ -584,7 +591,7 @@ msgstr ""
|
||||||
"가할 수 있습니다. 주의: 얼굴 필터를 사용하면 추출 속도가 현저히 감소하므로 정"
|
"가할 수 있습니다. 주의: 얼굴 필터를 사용하면 추출 속도가 현저히 감소하므로 정"
|
||||||
"확성을 보장할 수 없습니다."
|
"확성을 보장할 수 없습니다."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:663
|
#: lib/cli/args_extract_convert.py:667
|
||||||
msgid ""
|
msgid ""
|
||||||
"For use with the optional nfilter/filter files. Threshold for positive face "
|
"For use with the optional nfilter/filter files. Threshold for positive face "
|
||||||
"recognition. Lower values are stricter. NB: Using face filter will "
|
"recognition. Lower values are stricter. NB: Using face filter will "
|
||||||
|
|
@ -595,7 +602,7 @@ msgstr ""
|
||||||
"값. 낮은 값이 더 엄격합니다. 주의: 얼굴 필터를 사용하면 추출 속도가 현저히 감"
|
"값. 낮은 값이 더 엄격합니다. 주의: 얼굴 필터를 사용하면 추출 속도가 현저히 감"
|
||||||
"소하므로 정확성을 보장할 수 없습니다."
|
"소하므로 정확성을 보장할 수 없습니다."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:676
|
#: lib/cli/args_extract_convert.py:680
|
||||||
msgid ""
|
msgid ""
|
||||||
"The maximum number of parallel processes for performing conversion. "
|
"The maximum number of parallel processes for performing conversion. "
|
||||||
"Converting images is system RAM heavy so it is possible to run out of memory "
|
"Converting images is system RAM heavy so it is possible to run out of memory "
|
||||||
|
|
@ -611,15 +618,7 @@ msgstr ""
|
||||||
"를 사용하려고 시도하지 않습니다. 단일 프로세스가 활성화된 경우 이 설정은 무시"
|
"를 사용하려고 시도하지 않습니다. 단일 프로세스가 활성화된 경우 이 설정은 무시"
|
||||||
"됩니다."
|
"됩니다."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:688
|
#: lib/cli/args_extract_convert.py:693
|
||||||
msgid ""
|
|
||||||
"[LEGACY] This only needs to be selected if a legacy model is being loaded or "
|
|
||||||
"if there are multiple models in the model folder"
|
|
||||||
msgstr ""
|
|
||||||
"[LEGACY] 이것은 레거시 모델을 로드 중이거나 모델 폴더에 여러 모델이 있는 경우"
|
|
||||||
"에만 선택되어야 합니다"
|
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:697
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"Enable On-The-Fly Conversion. NOT recommended. You should generate a clean "
|
"Enable On-The-Fly Conversion. NOT recommended. You should generate a clean "
|
||||||
"alignments file for your destination video. However, if you wish you can "
|
"alignments file for your destination video. However, if you wish you can "
|
||||||
|
|
@ -633,7 +632,7 @@ msgstr ""
|
||||||
"하고 표준 이하의 결과로 이어질 것입니다. alignments 파일이 발견되면 이 옵션"
|
"하고 표준 이하의 결과로 이어질 것입니다. alignments 파일이 발견되면 이 옵션"
|
||||||
"은 무시됩니다."
|
"은 무시됩니다."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:709
|
#: lib/cli/args_extract_convert.py:705
|
||||||
msgid ""
|
msgid ""
|
||||||
"When used with --frame-ranges outputs the unchanged frames that are not "
|
"When used with --frame-ranges outputs the unchanged frames that are not "
|
||||||
"processed instead of discarding them."
|
"processed instead of discarding them."
|
||||||
|
|
@ -641,10 +640,17 @@ msgstr ""
|
||||||
"사용시 --frame-ranges 인자를 사용하면 변경되지 않은 프레임을 버리지 않은 결과"
|
"사용시 --frame-ranges 인자를 사용하면 변경되지 않은 프레임을 버리지 않은 결과"
|
||||||
"가 출력됩니다."
|
"가 출력됩니다."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:717
|
#: lib/cli/args_extract_convert.py:713
|
||||||
msgid "Swap the model. Instead converting from of A -> B, converts B -> A"
|
msgid "Swap the model. Instead converting from of A -> B, converts B -> A"
|
||||||
msgstr "모델을 바꿉니다. A -> B에서 변환하는 대신 B -> A로 변환"
|
msgstr "모델을 바꿉니다. A -> B에서 변환하는 대신 B -> A로 변환"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:723
|
#: lib/cli/args_extract_convert.py:719
|
||||||
msgid "Disable multiprocessing. Slower but less resource intensive."
|
msgid "Disable multiprocessing. Slower but less resource intensive."
|
||||||
msgstr "멀티프로세싱을 쓰지 않습니다. 느리지만 자원을 덜 소모합니다."
|
msgstr "멀티프로세싱을 쓰지 않습니다. 느리지만 자원을 덜 소모합니다."
|
||||||
|
|
||||||
|
#~ msgid ""
|
||||||
|
#~ "[LEGACY] This only needs to be selected if a legacy model is being loaded "
|
||||||
|
#~ "or if there are multiple models in the model folder"
|
||||||
|
#~ msgstr ""
|
||||||
|
#~ "[LEGACY] 이것은 레거시 모델을 로드 중이거나 모델 폴더에 여러 모델이 있는 "
|
||||||
|
#~ "경우에만 선택되어야 합니다"
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -7,8 +7,8 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-03-28 23:49+0000\n"
|
"POT-Creation-Date: 2024-04-12 12:10+0100\n"
|
||||||
"PO-Revision-Date: 2024-03-29 00:05+0000\n"
|
"PO-Revision-Date: 2024-04-12 12:17+0100\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: \n"
|
"Language-Team: \n"
|
||||||
"Language: ko_KR\n"
|
"Language: ko_KR\n"
|
||||||
|
|
@ -63,17 +63,23 @@ msgstr ""
|
||||||
msgid " Use the output option (-o) to process results."
|
msgid " Use the output option (-o) to process results."
|
||||||
msgstr " 결과를 진행하려면 (-o) 출력 옵션을 사용하세요."
|
msgstr " 결과를 진행하려면 (-o) 출력 옵션을 사용하세요."
|
||||||
|
|
||||||
#: tools/alignments/cli.py:57 tools/alignments/cli.py:97
|
#: tools/alignments/cli.py:58 tools/alignments/cli.py:104
|
||||||
msgid "processing"
|
msgid "processing"
|
||||||
msgstr "처리"
|
msgstr "처리"
|
||||||
|
|
||||||
#: tools/alignments/cli.py:60
|
#: tools/alignments/cli.py:61
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|Choose which action you want to perform. NB: All actions require an "
|
"R|Choose which action you want to perform. NB: All actions require an "
|
||||||
"alignments file (-a) to be passed in.\n"
|
"alignments file (-a) to be passed in.\n"
|
||||||
"L|'draw': Draw landmarks on frames in the selected folder/video. A subfolder "
|
"L|'draw': Draw landmarks on frames in the selected folder/video. A subfolder "
|
||||||
"will be created within the frames folder to hold the output.{0}\n"
|
"will be created within the frames folder to hold the output.{0}\n"
|
||||||
|
"L|'export': Export the contents of an alignments file to a json file. Can be "
|
||||||
|
"used for editing alignment information in external tools and then re-"
|
||||||
|
"importing by using Faceswap's Extract 'Import' plugins. Note: masks and "
|
||||||
|
"identity vectors will not be included in the exported file, so will be re-"
|
||||||
|
"generated when the json file is imported back into Faceswap. All data is "
|
||||||
|
"exported with the origin (0, 0) at the top left of the canvas.\n"
|
||||||
"L|'extract': Re-extract faces from the source frames/video based on "
|
"L|'extract': Re-extract faces from the source frames/video based on "
|
||||||
"alignment data. This is a lot quicker than re-detecting faces. Can pass in "
|
"alignment data. This is a lot quicker than re-detecting faces. Can pass in "
|
||||||
"the '-een' (--extract-every-n) parameter to only extract every nth frame."
|
"the '-een' (--extract-every-n) parameter to only extract every nth frame."
|
||||||
|
|
@ -107,6 +113,11 @@ msgstr ""
|
||||||
"을 전달해야 합니다.\n"
|
"을 전달해야 합니다.\n"
|
||||||
"L|'draw': 선택한 폴더/비디오의 프레임에 특징점을 그립니다. 출력을 저장할 하"
|
"L|'draw': 선택한 폴더/비디오의 프레임에 특징점을 그립니다. 출력을 저장할 하"
|
||||||
"위 폴더가 프레임 폴더 내에 생성됩니다.{0}\n"
|
"위 폴더가 프레임 폴더 내에 생성됩니다.{0}\n"
|
||||||
|
"L|'export': 정렬 파일의 내용을 JSON 파일로 내보내십시오. 외부 도구에서 정렬 "
|
||||||
|
"정보를 편집 한 다음 FaceSwap의 추출물 'Import'플러그인을 사용하여 다시 인상하"
|
||||||
|
"는 데 사용할 수 있습니다. 참고 : 마스크 및 ID 벡터는 내보내기 파일에 포함되"
|
||||||
|
"지 않으므로 JSON 파일이 다시 FaceSwap으로 가져 오면 다시 생성됩니다. 모든 데"
|
||||||
|
"이터는 캔버스의 왼쪽 상단에있는 원점 (0, 0)으로 내 보냅니다.\n"
|
||||||
"L|'extract': alignments 데이터를 기반으로 소스 프레임/비디오에서 얼굴을 재추"
|
"L|'extract': alignments 데이터를 기반으로 소스 프레임/비디오에서 얼굴을 재추"
|
||||||
"출합니다. 이것은 얼굴을 재감지하는 것보다 훨씬 더 빠릅니다. '-een'(--extract-"
|
"출합니다. 이것은 얼굴을 재감지하는 것보다 훨씬 더 빠릅니다. '-een'(--extract-"
|
||||||
"every-n) 매개 변수를 전달하여 모든 n번째 프레임을 추출할 수 있습니다.{1}\n"
|
"every-n) 매개 변수를 전달하여 모든 n번째 프레임을 추출할 수 있습니다.{1}\n"
|
||||||
|
|
@ -131,7 +142,7 @@ msgstr ""
|
||||||
"L| 'spatial': 공간 및 시간 필터링을 수행하여 alignments를 원활하게 수행합니다"
|
"L| 'spatial': 공간 및 시간 필터링을 수행하여 alignments를 원활하게 수행합니다"
|
||||||
"(실험적!)."
|
"(실험적!)."
|
||||||
|
|
||||||
#: tools/alignments/cli.py:100
|
#: tools/alignments/cli.py:107
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|How to output discovered items ('faces' and 'frames' only):\n"
|
"R|How to output discovered items ('faces' and 'frames' only):\n"
|
||||||
"L|'console': Print the list of frames to the screen. (DEFAULT)\n"
|
"L|'console': Print the list of frames to the screen. (DEFAULT)\n"
|
||||||
|
|
@ -145,12 +156,12 @@ msgstr ""
|
||||||
"L|'파일': 프레임 목록을 텍스트 파일(소스 디렉토리에 저장)로 출력합니다.\n"
|
"L|'파일': 프레임 목록을 텍스트 파일(소스 디렉토리에 저장)로 출력합니다.\n"
|
||||||
"L|'이동': 검색된 항목을 원본 디렉토리 내의 하위 폴더로 이동합니다."
|
"L|'이동': 검색된 항목을 원본 디렉토리 내의 하위 폴더로 이동합니다."
|
||||||
|
|
||||||
#: tools/alignments/cli.py:111 tools/alignments/cli.py:134
|
#: tools/alignments/cli.py:118 tools/alignments/cli.py:141
|
||||||
#: tools/alignments/cli.py:141
|
#: tools/alignments/cli.py:148
|
||||||
msgid "data"
|
msgid "data"
|
||||||
msgstr "데이터"
|
msgstr "데이터"
|
||||||
|
|
||||||
#: tools/alignments/cli.py:118
|
#: tools/alignments/cli.py:125
|
||||||
msgid ""
|
msgid ""
|
||||||
"Full path to the alignments file to be processed. If you have input a "
|
"Full path to the alignments file to be processed. If you have input a "
|
||||||
"'frames_dir' and don't provide this option, the process will try to find the "
|
"'frames_dir' and don't provide this option, the process will try to find the "
|
||||||
|
|
@ -163,11 +174,11 @@ msgstr ""
|
||||||
"다. 지정된 얼굴 폴더에 alignments 파일이 생성될 때 모든 작업은 'from-"
|
"다. 지정된 얼굴 폴더에 alignments 파일이 생성될 때 모든 작업은 'from-"
|
||||||
"faces'를 제외한 alignments 파일이 필요로 합니다."
|
"faces'를 제외한 alignments 파일이 필요로 합니다."
|
||||||
|
|
||||||
#: tools/alignments/cli.py:135
|
#: tools/alignments/cli.py:142
|
||||||
msgid "Directory containing source frames that faces were extracted from."
|
msgid "Directory containing source frames that faces were extracted from."
|
||||||
msgstr "얼굴 추출의 소스로 쓰인 원본 프레임이 저장된 디렉토리."
|
msgstr "얼굴 추출의 소스로 쓰인 원본 프레임이 저장된 디렉토리."
|
||||||
|
|
||||||
#: tools/alignments/cli.py:143
|
#: tools/alignments/cli.py:150
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|Run the aligmnents tool on multiple sources. The following jobs support "
|
"R|Run the aligmnents tool on multiple sources. The following jobs support "
|
||||||
"batch mode:\n"
|
"batch mode:\n"
|
||||||
|
|
@ -203,12 +214,12 @@ msgstr ""
|
||||||
"지의 하위 폴더여야 합니다. 에. 정렬 파일은 기본 위치에 있어야 합니다. 다른 모"
|
"지의 하위 폴더여야 합니다. 에. 정렬 파일은 기본 위치에 있어야 합니다. 다른 모"
|
||||||
"든 작업의 경우 이 옵션은 무시됩니다."
|
"든 작업의 경우 이 옵션은 무시됩니다."
|
||||||
|
|
||||||
#: tools/alignments/cli.py:169 tools/alignments/cli.py:181
|
#: tools/alignments/cli.py:176 tools/alignments/cli.py:188
|
||||||
#: tools/alignments/cli.py:191
|
#: tools/alignments/cli.py:198
|
||||||
msgid "extract"
|
msgid "extract"
|
||||||
msgstr "추출"
|
msgstr "추출"
|
||||||
|
|
||||||
#: tools/alignments/cli.py:171
|
#: tools/alignments/cli.py:178
|
||||||
msgid ""
|
msgid ""
|
||||||
"[Extract only] Extract every 'nth' frame. This option will skip frames when "
|
"[Extract only] Extract every 'nth' frame. This option will skip frames when "
|
||||||
"extracting faces. For example a value of 1 will extract faces from every "
|
"extracting faces. For example a value of 1 will extract faces from every "
|
||||||
|
|
@ -218,11 +229,11 @@ msgstr ""
|
||||||
"프레임을 건너뜁니다. 예를 들어, 값이 1이면 모든 프레임에서 얼굴이 추출되고, "
|
"프레임을 건너뜁니다. 예를 들어, 값이 1이면 모든 프레임에서 얼굴이 추출되고, "
|
||||||
"값이 10이면 모든 10번째 프레임에서 얼굴이 추출됩니다."
|
"값이 10이면 모든 10번째 프레임에서 얼굴이 추출됩니다."
|
||||||
|
|
||||||
#: tools/alignments/cli.py:182
|
#: tools/alignments/cli.py:189
|
||||||
msgid "[Extract only] The output size of extracted faces."
|
msgid "[Extract only] The output size of extracted faces."
|
||||||
msgstr "[Extract only] 추출된 얼굴들의 결과 크기입니다."
|
msgstr "[Extract only] 추출된 얼굴들의 결과 크기입니다."
|
||||||
|
|
||||||
#: tools/alignments/cli.py:193
|
#: tools/alignments/cli.py:200
|
||||||
msgid ""
|
msgid ""
|
||||||
"[Extract only] Only extract faces that have been resized by this percent or "
|
"[Extract only] Only extract faces that have been resized by this percent or "
|
||||||
"more to meet the specified extract size (`-sz`, `--size`). Useful for "
|
"more to meet the specified extract size (`-sz`, `--size`). Useful for "
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-03-28 18:11+0000\n"
|
"POT-Creation-Date: 2024-04-12 11:56+0100\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
|
@ -19,7 +19,7 @@ msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:46 lib/cli/args_extract_convert.py:56
|
#: lib/cli/args_extract_convert.py:46 lib/cli/args_extract_convert.py:56
|
||||||
#: lib/cli/args_extract_convert.py:64 lib/cli/args_extract_convert.py:122
|
#: lib/cli/args_extract_convert.py:64 lib/cli/args_extract_convert.py:122
|
||||||
#: lib/cli/args_extract_convert.py:479 lib/cli/args_extract_convert.py:488
|
#: lib/cli/args_extract_convert.py:483 lib/cli/args_extract_convert.py:492
|
||||||
msgid "Data"
|
msgid "Data"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -53,12 +53,12 @@ msgid ""
|
||||||
"will be output to separate sub-folders in the output_dir."
|
"will be output to separate sub-folders in the output_dir."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:133 lib/cli/args_extract_convert.py:150
|
#: lib/cli/args_extract_convert.py:133 lib/cli/args_extract_convert.py:152
|
||||||
#: lib/cli/args_extract_convert.py:163 lib/cli/args_extract_convert.py:202
|
#: lib/cli/args_extract_convert.py:167 lib/cli/args_extract_convert.py:206
|
||||||
#: lib/cli/args_extract_convert.py:220 lib/cli/args_extract_convert.py:233
|
#: lib/cli/args_extract_convert.py:224 lib/cli/args_extract_convert.py:237
|
||||||
#: lib/cli/args_extract_convert.py:243 lib/cli/args_extract_convert.py:253
|
#: lib/cli/args_extract_convert.py:247 lib/cli/args_extract_convert.py:257
|
||||||
#: lib/cli/args_extract_convert.py:499 lib/cli/args_extract_convert.py:525
|
#: lib/cli/args_extract_convert.py:503 lib/cli/args_extract_convert.py:529
|
||||||
#: lib/cli/args_extract_convert.py:564
|
#: lib/cli/args_extract_convert.py:568
|
||||||
msgid "Plugins"
|
msgid "Plugins"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -72,18 +72,22 @@ msgid ""
|
||||||
"than other GPU detectors but can often return more false positives.\n"
|
"than other GPU detectors but can often return more false positives.\n"
|
||||||
"L|s3fd: Best detector. Slow on CPU, faster on GPU. Can detect more faces and "
|
"L|s3fd: Best detector. Slow on CPU, faster on GPU. Can detect more faces and "
|
||||||
"fewer false positives than other GPU detectors, but is a lot more resource "
|
"fewer false positives than other GPU detectors, but is a lot more resource "
|
||||||
"intensive."
|
"intensive.\n"
|
||||||
|
"L|external: Import a face detection bounding box from a json file. "
|
||||||
|
"(configurable in Detect settings)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:152
|
#: lib/cli/args_extract_convert.py:154
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|Aligner to use.\n"
|
"R|Aligner to use.\n"
|
||||||
"L|cv2-dnn: A CPU only landmark detector. Faster, less resource intensive, "
|
"L|cv2-dnn: A CPU only landmark detector. Faster, less resource intensive, "
|
||||||
"but less accurate. Only use this if not using a GPU and time is important.\n"
|
"but less accurate. Only use this if not using a GPU and time is important.\n"
|
||||||
"L|fan: Best aligner. Fast on GPU, slow on CPU."
|
"L|fan: Best aligner. Fast on GPU, slow on CPU.\n"
|
||||||
|
"L|external: Import 68 point 2D landmarks or an aligned bounding box from a "
|
||||||
|
"json file. (configurable in Align settings)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:165
|
#: lib/cli/args_extract_convert.py:169
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|Additional Masker(s) to use. The masks generated here will all take up GPU "
|
"R|Additional Masker(s) to use. The masks generated here will all take up GPU "
|
||||||
"RAM. You can select none, one or multiple masks, but the extraction may take "
|
"RAM. You can select none, one or multiple masks, but the extraction may take "
|
||||||
|
|
@ -118,7 +122,7 @@ msgid ""
|
||||||
"(eg: `-M unet-dfl vgg-clear`, `--masker vgg-obstructed`)"
|
"(eg: `-M unet-dfl vgg-clear`, `--masker vgg-obstructed`)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:204
|
#: lib/cli/args_extract_convert.py:208
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|Performing normalization can help the aligner better align faces with "
|
"R|Performing normalization can help the aligner better align faces with "
|
||||||
"difficult lighting conditions at an extraction speed cost. Different methods "
|
"difficult lighting conditions at an extraction speed cost. Different methods "
|
||||||
|
|
@ -131,7 +135,7 @@ msgid ""
|
||||||
"L|mean: Normalize the face colors to the mean."
|
"L|mean: Normalize the face colors to the mean."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:222
|
#: lib/cli/args_extract_convert.py:226
|
||||||
msgid ""
|
msgid ""
|
||||||
"The number of times to re-feed the detected face into the aligner. Each time "
|
"The number of times to re-feed the detected face into the aligner. Each time "
|
||||||
"the face is re-fed into the aligner the bounding box is adjusted by a small "
|
"the face is re-fed into the aligner the bounding box is adjusted by a small "
|
||||||
|
|
@ -141,14 +145,14 @@ msgid ""
|
||||||
"occur but the longer extraction will take."
|
"occur but the longer extraction will take."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:235
|
#: lib/cli/args_extract_convert.py:239
|
||||||
msgid ""
|
msgid ""
|
||||||
"Re-feed the initially found aligned face through the aligner. Can help "
|
"Re-feed the initially found aligned face through the aligner. Can help "
|
||||||
"produce better alignments for faces that are rotated beyond 45 degrees in "
|
"produce better alignments for faces that are rotated beyond 45 degrees in "
|
||||||
"the frame or are at extreme angles. Slows down extraction."
|
"the frame or are at extreme angles. Slows down extraction."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:245
|
#: lib/cli/args_extract_convert.py:249
|
||||||
msgid ""
|
msgid ""
|
||||||
"If a face isn't found, rotate the images to try to find a face. Can find "
|
"If a face isn't found, rotate the images to try to find a face. Can find "
|
||||||
"more faces at the cost of extraction speed. Pass in a single number to use "
|
"more faces at the cost of extraction speed. Pass in a single number to use "
|
||||||
|
|
@ -156,27 +160,27 @@ msgid ""
|
||||||
"exactly what angles to check."
|
"exactly what angles to check."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:255
|
#: lib/cli/args_extract_convert.py:259
|
||||||
msgid ""
|
msgid ""
|
||||||
"Obtain and store face identity encodings from VGGFace2. Slows down extract a "
|
"Obtain and store face identity encodings from VGGFace2. Slows down extract a "
|
||||||
"little, but will save time if using 'sort by face'"
|
"little, but will save time if using 'sort by face'"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:265 lib/cli/args_extract_convert.py:276
|
#: lib/cli/args_extract_convert.py:269 lib/cli/args_extract_convert.py:280
|
||||||
#: lib/cli/args_extract_convert.py:289 lib/cli/args_extract_convert.py:303
|
#: lib/cli/args_extract_convert.py:293 lib/cli/args_extract_convert.py:307
|
||||||
#: lib/cli/args_extract_convert.py:610 lib/cli/args_extract_convert.py:619
|
#: lib/cli/args_extract_convert.py:614 lib/cli/args_extract_convert.py:623
|
||||||
#: lib/cli/args_extract_convert.py:634 lib/cli/args_extract_convert.py:647
|
#: lib/cli/args_extract_convert.py:638 lib/cli/args_extract_convert.py:651
|
||||||
#: lib/cli/args_extract_convert.py:661
|
#: lib/cli/args_extract_convert.py:665
|
||||||
msgid "Face Processing"
|
msgid "Face Processing"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:267
|
#: lib/cli/args_extract_convert.py:271
|
||||||
msgid ""
|
msgid ""
|
||||||
"Filters out faces detected below this size. Length, in pixels across the "
|
"Filters out faces detected below this size. Length, in pixels across the "
|
||||||
"diagonal of the bounding box. Set to 0 for off"
|
"diagonal of the bounding box. Set to 0 for off"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:278
|
#: lib/cli/args_extract_convert.py:282
|
||||||
msgid ""
|
msgid ""
|
||||||
"Optionally filter out people who you do not wish to extract by passing in "
|
"Optionally filter out people who you do not wish to extract by passing in "
|
||||||
"images of those people. Should be a small variety of images at different "
|
"images of those people. Should be a small variety of images at different "
|
||||||
|
|
@ -184,7 +188,7 @@ msgid ""
|
||||||
"or multiple image files, space separated, can be selected."
|
"or multiple image files, space separated, can be selected."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:291
|
#: lib/cli/args_extract_convert.py:295
|
||||||
msgid ""
|
msgid ""
|
||||||
"Optionally select people you wish to extract by passing in images of that "
|
"Optionally select people you wish to extract by passing in images of that "
|
||||||
"person. Should be a small variety of images at different angles and in "
|
"person. Should be a small variety of images at different angles and in "
|
||||||
|
|
@ -192,32 +196,32 @@ msgid ""
|
||||||
"image files, space separated, can be selected."
|
"image files, space separated, can be selected."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:305
|
#: lib/cli/args_extract_convert.py:309
|
||||||
msgid ""
|
msgid ""
|
||||||
"For use with the optional nfilter/filter files. Threshold for positive face "
|
"For use with the optional nfilter/filter files. Threshold for positive face "
|
||||||
"recognition. Higher values are stricter."
|
"recognition. Higher values are stricter."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:314 lib/cli/args_extract_convert.py:327
|
#: lib/cli/args_extract_convert.py:318 lib/cli/args_extract_convert.py:331
|
||||||
#: lib/cli/args_extract_convert.py:340 lib/cli/args_extract_convert.py:352
|
#: lib/cli/args_extract_convert.py:344 lib/cli/args_extract_convert.py:356
|
||||||
msgid "output"
|
msgid "output"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:316
|
#: lib/cli/args_extract_convert.py:320
|
||||||
msgid ""
|
msgid ""
|
||||||
"The output size of extracted faces. Make sure that the model you intend to "
|
"The output size of extracted faces. Make sure that the model you intend to "
|
||||||
"train supports your required size. This will only need to be changed for hi-"
|
"train supports your required size. This will only need to be changed for hi-"
|
||||||
"res models."
|
"res models."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:329
|
#: lib/cli/args_extract_convert.py:333
|
||||||
msgid ""
|
msgid ""
|
||||||
"Extract every 'nth' frame. This option will skip frames when extracting "
|
"Extract every 'nth' frame. This option will skip frames when extracting "
|
||||||
"faces. For example a value of 1 will extract faces from every frame, a value "
|
"faces. For example a value of 1 will extract faces from every frame, a value "
|
||||||
"of 10 will extract faces from every 10th frame."
|
"of 10 will extract faces from every 10th frame."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:342
|
#: lib/cli/args_extract_convert.py:346
|
||||||
msgid ""
|
msgid ""
|
||||||
"Automatically save the alignments file after a set amount of frames. By "
|
"Automatically save the alignments file after a set amount of frames. By "
|
||||||
"default the alignments file is only saved at the end of the extraction "
|
"default the alignments file is only saved at the end of the extraction "
|
||||||
|
|
@ -227,59 +231,58 @@ msgid ""
|
||||||
"turn off"
|
"turn off"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:353
|
#: lib/cli/args_extract_convert.py:357
|
||||||
msgid "Draw landmarks on the ouput faces for debugging purposes."
|
msgid "Draw landmarks on the ouput faces for debugging purposes."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:359 lib/cli/args_extract_convert.py:369
|
#: lib/cli/args_extract_convert.py:363 lib/cli/args_extract_convert.py:373
|
||||||
#: lib/cli/args_extract_convert.py:377 lib/cli/args_extract_convert.py:384
|
#: lib/cli/args_extract_convert.py:381 lib/cli/args_extract_convert.py:388
|
||||||
#: lib/cli/args_extract_convert.py:674 lib/cli/args_extract_convert.py:686
|
#: lib/cli/args_extract_convert.py:678 lib/cli/args_extract_convert.py:691
|
||||||
#: lib/cli/args_extract_convert.py:695 lib/cli/args_extract_convert.py:716
|
#: lib/cli/args_extract_convert.py:712 lib/cli/args_extract_convert.py:718
|
||||||
#: lib/cli/args_extract_convert.py:722
|
|
||||||
msgid "settings"
|
msgid "settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:361
|
#: lib/cli/args_extract_convert.py:365
|
||||||
msgid ""
|
msgid ""
|
||||||
"Don't run extraction in parallel. Will run each part of the extraction "
|
"Don't run extraction in parallel. Will run each part of the extraction "
|
||||||
"process separately (one after the other) rather than all at the same time. "
|
"process separately (one after the other) rather than all at the same time. "
|
||||||
"Useful if VRAM is at a premium."
|
"Useful if VRAM is at a premium."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:371
|
#: lib/cli/args_extract_convert.py:375
|
||||||
msgid ""
|
msgid ""
|
||||||
"Skips frames that have already been extracted and exist in the alignments "
|
"Skips frames that have already been extracted and exist in the alignments "
|
||||||
"file"
|
"file"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:378
|
#: lib/cli/args_extract_convert.py:382
|
||||||
msgid "Skip frames that already have detected faces in the alignments file"
|
msgid "Skip frames that already have detected faces in the alignments file"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:385
|
#: lib/cli/args_extract_convert.py:389
|
||||||
msgid "Skip saving the detected faces to disk. Just create an alignments file"
|
msgid "Skip saving the detected faces to disk. Just create an alignments file"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:459
|
#: lib/cli/args_extract_convert.py:463
|
||||||
msgid ""
|
msgid ""
|
||||||
"Swap the original faces in a source video/images to your final faces.\n"
|
"Swap the original faces in a source video/images to your final faces.\n"
|
||||||
"Conversion plugins can be configured in the 'Settings' Menu"
|
"Conversion plugins can be configured in the 'Settings' Menu"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:481
|
#: lib/cli/args_extract_convert.py:485
|
||||||
msgid ""
|
msgid ""
|
||||||
"Only required if converting from images to video. Provide The original video "
|
"Only required if converting from images to video. Provide The original video "
|
||||||
"that the source frames were extracted from (for extracting the fps and "
|
"that the source frames were extracted from (for extracting the fps and "
|
||||||
"audio)."
|
"audio)."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:490
|
#: lib/cli/args_extract_convert.py:494
|
||||||
msgid ""
|
msgid ""
|
||||||
"Model directory. The directory containing the trained model you wish to use "
|
"Model directory. The directory containing the trained model you wish to use "
|
||||||
"for conversion."
|
"for conversion."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:501
|
#: lib/cli/args_extract_convert.py:505
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|Performs color adjustment to the swapped face. Some of these options have "
|
"R|Performs color adjustment to the swapped face. Some of these options have "
|
||||||
"configurable settings in '/config/convert.ini' or 'Settings > Configure "
|
"configurable settings in '/config/convert.ini' or 'Settings > Configure "
|
||||||
|
|
@ -300,7 +303,7 @@ msgid ""
|
||||||
"L|none: Don't perform color adjustment."
|
"L|none: Don't perform color adjustment."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:527
|
#: lib/cli/args_extract_convert.py:531
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|Masker to use. NB: The mask you require must exist within the alignments "
|
"R|Masker to use. NB: The mask you require must exist within the alignments "
|
||||||
"file. You can add additional masks with the Mask Tool.\n"
|
"file. You can add additional masks with the Mask Tool.\n"
|
||||||
|
|
@ -337,7 +340,7 @@ msgid ""
|
||||||
"will use the mask that was created by the trained model."
|
"will use the mask that was created by the trained model."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:566
|
#: lib/cli/args_extract_convert.py:570
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|The plugin to use to output the converted images. The writers are "
|
"R|The plugin to use to output the converted images. The writers are "
|
||||||
"configurable in '/config/convert.ini' or 'Settings > Configure Convert "
|
"configurable in '/config/convert.ini' or 'Settings > Configure Convert "
|
||||||
|
|
@ -356,19 +359,19 @@ msgid ""
|
||||||
"more formats."
|
"more formats."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:587 lib/cli/args_extract_convert.py:596
|
#: lib/cli/args_extract_convert.py:591 lib/cli/args_extract_convert.py:600
|
||||||
#: lib/cli/args_extract_convert.py:707
|
#: lib/cli/args_extract_convert.py:703
|
||||||
msgid "Frame Processing"
|
msgid "Frame Processing"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:589
|
#: lib/cli/args_extract_convert.py:593
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"Scale the final output frames by this amount. 100%% will output the frames "
|
"Scale the final output frames by this amount. 100%% will output the frames "
|
||||||
"at source dimensions. 50%% at half size 200%% at double size"
|
"at source dimensions. 50%% at half size 200%% at double size"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:598
|
#: lib/cli/args_extract_convert.py:602
|
||||||
msgid ""
|
msgid ""
|
||||||
"Frame ranges to apply transfer to e.g. For frames 10 to 50 and 90 to 100 use "
|
"Frame ranges to apply transfer to e.g. For frames 10 to 50 and 90 to 100 use "
|
||||||
"--frame-ranges 10-50 90-100. Frames falling outside of the selected range "
|
"--frame-ranges 10-50 90-100. Frames falling outside of the selected range "
|
||||||
|
|
@ -376,13 +379,13 @@ msgid ""
|
||||||
"converting from images, then the filenames must end with the frame-number!"
|
"converting from images, then the filenames must end with the frame-number!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:612
|
#: lib/cli/args_extract_convert.py:616
|
||||||
msgid ""
|
msgid ""
|
||||||
"Scale the swapped face by this percentage. Positive values will enlarge the "
|
"Scale the swapped face by this percentage. Positive values will enlarge the "
|
||||||
"face, Negative values will shrink the face."
|
"face, Negative values will shrink the face."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:621
|
#: lib/cli/args_extract_convert.py:625
|
||||||
msgid ""
|
msgid ""
|
||||||
"If you have not cleansed your alignments file, then you can filter out faces "
|
"If you have not cleansed your alignments file, then you can filter out faces "
|
||||||
"by defining a folder here that contains the faces extracted from your input "
|
"by defining a folder here that contains the faces extracted from your input "
|
||||||
|
|
@ -392,7 +395,7 @@ msgid ""
|
||||||
"alignments file."
|
"alignments file."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:636
|
#: lib/cli/args_extract_convert.py:640
|
||||||
msgid ""
|
msgid ""
|
||||||
"Optionally filter out people who you do not wish to process by passing in an "
|
"Optionally filter out people who you do not wish to process by passing in an "
|
||||||
"image of that person. Should be a front portrait with a single person in the "
|
"image of that person. Should be a front portrait with a single person in the "
|
||||||
|
|
@ -401,7 +404,7 @@ msgid ""
|
||||||
"guaranteed."
|
"guaranteed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:649
|
#: lib/cli/args_extract_convert.py:653
|
||||||
msgid ""
|
msgid ""
|
||||||
"Optionally select people you wish to process by passing in an image of that "
|
"Optionally select people you wish to process by passing in an image of that "
|
||||||
"person. Should be a front portrait with a single person in the image. "
|
"person. Should be a front portrait with a single person in the image. "
|
||||||
|
|
@ -410,7 +413,7 @@ msgid ""
|
||||||
"guaranteed."
|
"guaranteed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:663
|
#: lib/cli/args_extract_convert.py:667
|
||||||
msgid ""
|
msgid ""
|
||||||
"For use with the optional nfilter/filter files. Threshold for positive face "
|
"For use with the optional nfilter/filter files. Threshold for positive face "
|
||||||
"recognition. Lower values are stricter. NB: Using face filter will "
|
"recognition. Lower values are stricter. NB: Using face filter will "
|
||||||
|
|
@ -418,7 +421,7 @@ msgid ""
|
||||||
"guaranteed."
|
"guaranteed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:676
|
#: lib/cli/args_extract_convert.py:680
|
||||||
msgid ""
|
msgid ""
|
||||||
"The maximum number of parallel processes for performing conversion. "
|
"The maximum number of parallel processes for performing conversion. "
|
||||||
"Converting images is system RAM heavy so it is possible to run out of memory "
|
"Converting images is system RAM heavy so it is possible to run out of memory "
|
||||||
|
|
@ -428,13 +431,7 @@ msgid ""
|
||||||
"your system. If singleprocess is enabled this setting will be ignored."
|
"your system. If singleprocess is enabled this setting will be ignored."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:688
|
#: lib/cli/args_extract_convert.py:693
|
||||||
msgid ""
|
|
||||||
"[LEGACY] This only needs to be selected if a legacy model is being loaded or "
|
|
||||||
"if there are multiple models in the model folder"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:697
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"Enable On-The-Fly Conversion. NOT recommended. You should generate a clean "
|
"Enable On-The-Fly Conversion. NOT recommended. You should generate a clean "
|
||||||
"alignments file for your destination video. However, if you wish you can "
|
"alignments file for your destination video. However, if you wish you can "
|
||||||
|
|
@ -443,16 +440,16 @@ msgid ""
|
||||||
"alignments file is found, this option will be ignored."
|
"alignments file is found, this option will be ignored."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:709
|
#: lib/cli/args_extract_convert.py:705
|
||||||
msgid ""
|
msgid ""
|
||||||
"When used with --frame-ranges outputs the unchanged frames that are not "
|
"When used with --frame-ranges outputs the unchanged frames that are not "
|
||||||
"processed instead of discarding them."
|
"processed instead of discarding them."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:717
|
#: lib/cli/args_extract_convert.py:713
|
||||||
msgid "Swap the model. Instead converting from of A -> B, converts B -> A"
|
msgid "Swap the model. Instead converting from of A -> B, converts B -> A"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:723
|
#: lib/cli/args_extract_convert.py:719
|
||||||
msgid "Disable multiprocessing. Slower but less resource intensive."
|
msgid "Disable multiprocessing. Slower but less resource intensive."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -7,8 +7,8 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-03-28 18:11+0000\n"
|
"POT-Creation-Date: 2024-04-12 11:56+0100\n"
|
||||||
"PO-Revision-Date: 2024-03-28 18:22+0000\n"
|
"PO-Revision-Date: 2024-04-12 11:59+0100\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: \n"
|
"Language-Team: \n"
|
||||||
"Language: ru\n"
|
"Language: ru\n"
|
||||||
|
|
@ -21,7 +21,7 @@ msgstr ""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:46 lib/cli/args_extract_convert.py:56
|
#: lib/cli/args_extract_convert.py:46 lib/cli/args_extract_convert.py:56
|
||||||
#: lib/cli/args_extract_convert.py:64 lib/cli/args_extract_convert.py:122
|
#: lib/cli/args_extract_convert.py:64 lib/cli/args_extract_convert.py:122
|
||||||
#: lib/cli/args_extract_convert.py:479 lib/cli/args_extract_convert.py:488
|
#: lib/cli/args_extract_convert.py:483 lib/cli/args_extract_convert.py:492
|
||||||
msgid "Data"
|
msgid "Data"
|
||||||
msgstr "Данные"
|
msgstr "Данные"
|
||||||
|
|
||||||
|
|
@ -65,12 +65,12 @@ msgstr ""
|
||||||
"несколько видео и/или папок с изображениями, из которых вы хотите извлечь "
|
"несколько видео и/или папок с изображениями, из которых вы хотите извлечь "
|
||||||
"изображение. Лица будут выведены в отдельные вложенные папки в output_dir."
|
"изображение. Лица будут выведены в отдельные вложенные папки в output_dir."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:133 lib/cli/args_extract_convert.py:150
|
#: lib/cli/args_extract_convert.py:133 lib/cli/args_extract_convert.py:152
|
||||||
#: lib/cli/args_extract_convert.py:163 lib/cli/args_extract_convert.py:202
|
#: lib/cli/args_extract_convert.py:167 lib/cli/args_extract_convert.py:206
|
||||||
#: lib/cli/args_extract_convert.py:220 lib/cli/args_extract_convert.py:233
|
#: lib/cli/args_extract_convert.py:224 lib/cli/args_extract_convert.py:237
|
||||||
#: lib/cli/args_extract_convert.py:243 lib/cli/args_extract_convert.py:253
|
#: lib/cli/args_extract_convert.py:247 lib/cli/args_extract_convert.py:257
|
||||||
#: lib/cli/args_extract_convert.py:499 lib/cli/args_extract_convert.py:525
|
#: lib/cli/args_extract_convert.py:503 lib/cli/args_extract_convert.py:529
|
||||||
#: lib/cli/args_extract_convert.py:564
|
#: lib/cli/args_extract_convert.py:568
|
||||||
msgid "Plugins"
|
msgid "Plugins"
|
||||||
msgstr "Плагины"
|
msgstr "Плагины"
|
||||||
|
|
||||||
|
|
@ -84,7 +84,9 @@ msgid ""
|
||||||
"than other GPU detectors but can often return more false positives.\n"
|
"than other GPU detectors but can often return more false positives.\n"
|
||||||
"L|s3fd: Best detector. Slow on CPU, faster on GPU. Can detect more faces and "
|
"L|s3fd: Best detector. Slow on CPU, faster on GPU. Can detect more faces and "
|
||||||
"fewer false positives than other GPU detectors, but is a lot more resource "
|
"fewer false positives than other GPU detectors, but is a lot more resource "
|
||||||
"intensive."
|
"intensive.\n"
|
||||||
|
"L|external: Import a face detection bounding box from a json file. "
|
||||||
|
"(configurable in Detect settings)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"R|Детектор для использования. Некоторые из них имеют настраиваемые параметры "
|
"R|Детектор для использования. Некоторые из них имеют настраиваемые параметры "
|
||||||
"в '/config/extract.ini' или 'Settings > Configure Extract 'Plugins':\n"
|
"в '/config/extract.ini' или 'Settings > Configure Extract 'Plugins':\n"
|
||||||
|
|
@ -96,22 +98,29 @@ msgstr ""
|
||||||
"ложных срабатываний.\n"
|
"ложных срабатываний.\n"
|
||||||
"L|s3fd: Лучший детектор. Медленный на CPU, более быстрый на GPU. Может "
|
"L|s3fd: Лучший детектор. Медленный на CPU, более быстрый на GPU. Может "
|
||||||
"обнаружить больше лиц и меньше ложных срабатываний, чем другие детекторы на "
|
"обнаружить больше лиц и меньше ложных срабатываний, чем другие детекторы на "
|
||||||
"GPU, но требует гораздо больше ресурсов."
|
"GPU, но требует гораздо больше ресурсов.\n"
|
||||||
|
"L|external: импортируйте ограничивающую коробку обнаружения лица из файла "
|
||||||
|
"JSON. (настраивается в настройках обнаружения)"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:152
|
#: lib/cli/args_extract_convert.py:154
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|Aligner to use.\n"
|
"R|Aligner to use.\n"
|
||||||
"L|cv2-dnn: A CPU only landmark detector. Faster, less resource intensive, "
|
"L|cv2-dnn: A CPU only landmark detector. Faster, less resource intensive, "
|
||||||
"but less accurate. Only use this if not using a GPU and time is important.\n"
|
"but less accurate. Only use this if not using a GPU and time is important.\n"
|
||||||
"L|fan: Best aligner. Fast on GPU, slow on CPU."
|
"L|fan: Best aligner. Fast on GPU, slow on CPU.\n"
|
||||||
|
"L|external: Import 68 point 2D landmarks or an aligned bounding box from a "
|
||||||
|
"json file. (configurable in Align settings)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"R|Выравниватель для использования.\n"
|
"R|Выравниватель для использования.\n"
|
||||||
"L|cv2-dnn: Детектор ориентиров только для процессора. Быстрее, менее "
|
"L|cv2-dnn: Детектор ориентиров только для процессора. Быстрее, менее "
|
||||||
"ресурсоемкий, но менее точный. Используйте его, только если не используется "
|
"ресурсоемкий, но менее точный. Используйте его, только если не используется "
|
||||||
"GPU и важно время.\n"
|
"GPU и важно время.\n"
|
||||||
"L|fan: Лучший выравниватель. Быстрый на GPU, медленный на CPU."
|
"L|fan: Лучший выравниватель. Быстрый на GPU, медленный на CPU.\n"
|
||||||
|
"L|external: импорт 68 баллов 2D достопримечательности или выровненная "
|
||||||
|
"ограничивающая коробка из файла JSON. (настраивается в настройках "
|
||||||
|
"выравнивания)"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:165
|
#: lib/cli/args_extract_convert.py:169
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|Additional Masker(s) to use. The masks generated here will all take up GPU "
|
"R|Additional Masker(s) to use. The masks generated here will all take up GPU "
|
||||||
"RAM. You can select none, one or multiple masks, but the extraction may take "
|
"RAM. You can select none, one or multiple masks, but the extraction may take "
|
||||||
|
|
@ -178,7 +187,7 @@ msgstr ""
|
||||||
"и маска расширяется вверх на лоб.\n"
|
"и маска расширяется вверх на лоб.\n"
|
||||||
"(например: `-M unet-dfl vgg-clear`, `--masker vgg-obstructed`)"
|
"(например: `-M unet-dfl vgg-clear`, `--masker vgg-obstructed`)"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:204
|
#: lib/cli/args_extract_convert.py:208
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|Performing normalization can help the aligner better align faces with "
|
"R|Performing normalization can help the aligner better align faces with "
|
||||||
"difficult lighting conditions at an extraction speed cost. Different methods "
|
"difficult lighting conditions at an extraction speed cost. Different methods "
|
||||||
|
|
@ -200,7 +209,7 @@ msgstr ""
|
||||||
"L|hist: Уравнять гистограммы в каналах RGB.\n"
|
"L|hist: Уравнять гистограммы в каналах RGB.\n"
|
||||||
"L|mean: Нормализовать цвета лица к среднему значению."
|
"L|mean: Нормализовать цвета лица к среднему значению."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:222
|
#: lib/cli/args_extract_convert.py:226
|
||||||
msgid ""
|
msgid ""
|
||||||
"The number of times to re-feed the detected face into the aligner. Each time "
|
"The number of times to re-feed the detected face into the aligner. Each time "
|
||||||
"the face is re-fed into the aligner the bounding box is adjusted by a small "
|
"the face is re-fed into the aligner the bounding box is adjusted by a small "
|
||||||
|
|
@ -217,7 +226,7 @@ msgstr ""
|
||||||
"в выравниватель, тем меньше микро-дрожание, но тем больше времени займет "
|
"в выравниватель, тем меньше микро-дрожание, но тем больше времени займет "
|
||||||
"извлечение."
|
"извлечение."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:235
|
#: lib/cli/args_extract_convert.py:239
|
||||||
msgid ""
|
msgid ""
|
||||||
"Re-feed the initially found aligned face through the aligner. Can help "
|
"Re-feed the initially found aligned face through the aligner. Can help "
|
||||||
"produce better alignments for faces that are rotated beyond 45 degrees in "
|
"produce better alignments for faces that are rotated beyond 45 degrees in "
|
||||||
|
|
@ -228,7 +237,7 @@ msgstr ""
|
||||||
"в кадре более чем на 45 градусов или расположенных под экстремальными "
|
"в кадре более чем на 45 градусов или расположенных под экстремальными "
|
||||||
"углами. Замедляет извлечение."
|
"углами. Замедляет извлечение."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:245
|
#: lib/cli/args_extract_convert.py:249
|
||||||
msgid ""
|
msgid ""
|
||||||
"If a face isn't found, rotate the images to try to find a face. Can find "
|
"If a face isn't found, rotate the images to try to find a face. Can find "
|
||||||
"more faces at the cost of extraction speed. Pass in a single number to use "
|
"more faces at the cost of extraction speed. Pass in a single number to use "
|
||||||
|
|
@ -240,7 +249,7 @@ msgstr ""
|
||||||
"число, чтобы использовать приращения этого размера до 360, или передайте "
|
"число, чтобы использовать приращения этого размера до 360, или передайте "
|
||||||
"список чисел, чтобы перечислить, какие именно углы нужно проверить."
|
"список чисел, чтобы перечислить, какие именно углы нужно проверить."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:255
|
#: lib/cli/args_extract_convert.py:259
|
||||||
msgid ""
|
msgid ""
|
||||||
"Obtain and store face identity encodings from VGGFace2. Slows down extract a "
|
"Obtain and store face identity encodings from VGGFace2. Slows down extract a "
|
||||||
"little, but will save time if using 'sort by face'"
|
"little, but will save time if using 'sort by face'"
|
||||||
|
|
@ -249,15 +258,15 @@ msgstr ""
|
||||||
"замедляет извлечение, но экономит время при использовании \"сортировки по "
|
"замедляет извлечение, но экономит время при использовании \"сортировки по "
|
||||||
"лицам\"."
|
"лицам\"."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:265 lib/cli/args_extract_convert.py:276
|
#: lib/cli/args_extract_convert.py:269 lib/cli/args_extract_convert.py:280
|
||||||
#: lib/cli/args_extract_convert.py:289 lib/cli/args_extract_convert.py:303
|
#: lib/cli/args_extract_convert.py:293 lib/cli/args_extract_convert.py:307
|
||||||
#: lib/cli/args_extract_convert.py:610 lib/cli/args_extract_convert.py:619
|
#: lib/cli/args_extract_convert.py:614 lib/cli/args_extract_convert.py:623
|
||||||
#: lib/cli/args_extract_convert.py:634 lib/cli/args_extract_convert.py:647
|
#: lib/cli/args_extract_convert.py:638 lib/cli/args_extract_convert.py:651
|
||||||
#: lib/cli/args_extract_convert.py:661
|
#: lib/cli/args_extract_convert.py:665
|
||||||
msgid "Face Processing"
|
msgid "Face Processing"
|
||||||
msgstr "Обработка лиц"
|
msgstr "Обработка лиц"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:267
|
#: lib/cli/args_extract_convert.py:271
|
||||||
msgid ""
|
msgid ""
|
||||||
"Filters out faces detected below this size. Length, in pixels across the "
|
"Filters out faces detected below this size. Length, in pixels across the "
|
||||||
"diagonal of the bounding box. Set to 0 for off"
|
"diagonal of the bounding box. Set to 0 for off"
|
||||||
|
|
@ -265,7 +274,7 @@ msgstr ""
|
||||||
"Отфильтровывает лица, обнаруженные ниже этого размера. Длина в пикселях по "
|
"Отфильтровывает лица, обнаруженные ниже этого размера. Длина в пикселях по "
|
||||||
"диагонали ограничивающего поля. Установите значение 0, чтобы выключить"
|
"диагонали ограничивающего поля. Установите значение 0, чтобы выключить"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:278
|
#: lib/cli/args_extract_convert.py:282
|
||||||
msgid ""
|
msgid ""
|
||||||
"Optionally filter out people who you do not wish to extract by passing in "
|
"Optionally filter out people who you do not wish to extract by passing in "
|
||||||
"images of those people. Should be a small variety of images at different "
|
"images of those people. Should be a small variety of images at different "
|
||||||
|
|
@ -278,7 +287,7 @@ msgstr ""
|
||||||
"необходимые изображения, или несколько файлов изображений, разделенных "
|
"необходимые изображения, или несколько файлов изображений, разделенных "
|
||||||
"пробелами."
|
"пробелами."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:291
|
#: lib/cli/args_extract_convert.py:295
|
||||||
msgid ""
|
msgid ""
|
||||||
"Optionally select people you wish to extract by passing in images of that "
|
"Optionally select people you wish to extract by passing in images of that "
|
||||||
"person. Should be a small variety of images at different angles and in "
|
"person. Should be a small variety of images at different angles and in "
|
||||||
|
|
@ -290,7 +299,7 @@ msgstr ""
|
||||||
"углами и в разных условиях. Можно выбрать папку, содержащую необходимые "
|
"углами и в разных условиях. Можно выбрать папку, содержащую необходимые "
|
||||||
"изображения, или несколько файлов изображений, разделенных пробелами."
|
"изображения, или несколько файлов изображений, разделенных пробелами."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:305
|
#: lib/cli/args_extract_convert.py:309
|
||||||
msgid ""
|
msgid ""
|
||||||
"For use with the optional nfilter/filter files. Threshold for positive face "
|
"For use with the optional nfilter/filter files. Threshold for positive face "
|
||||||
"recognition. Higher values are stricter."
|
"recognition. Higher values are stricter."
|
||||||
|
|
@ -299,12 +308,12 @@ msgstr ""
|
||||||
"положительного распознавания лица. Более высокие значения являются более "
|
"положительного распознавания лица. Более высокие значения являются более "
|
||||||
"строгими."
|
"строгими."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:314 lib/cli/args_extract_convert.py:327
|
#: lib/cli/args_extract_convert.py:318 lib/cli/args_extract_convert.py:331
|
||||||
#: lib/cli/args_extract_convert.py:340 lib/cli/args_extract_convert.py:352
|
#: lib/cli/args_extract_convert.py:344 lib/cli/args_extract_convert.py:356
|
||||||
msgid "output"
|
msgid "output"
|
||||||
msgstr "вывод"
|
msgstr "вывод"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:316
|
#: lib/cli/args_extract_convert.py:320
|
||||||
msgid ""
|
msgid ""
|
||||||
"The output size of extracted faces. Make sure that the model you intend to "
|
"The output size of extracted faces. Make sure that the model you intend to "
|
||||||
"train supports your required size. This will only need to be changed for hi-"
|
"train supports your required size. This will only need to be changed for hi-"
|
||||||
|
|
@ -314,7 +323,7 @@ msgstr ""
|
||||||
"собираетесь тренировать, поддерживает требуемый размер. Это необходимо "
|
"собираетесь тренировать, поддерживает требуемый размер. Это необходимо "
|
||||||
"изменить только для моделей высокого разрешения."
|
"изменить только для моделей высокого разрешения."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:329
|
#: lib/cli/args_extract_convert.py:333
|
||||||
msgid ""
|
msgid ""
|
||||||
"Extract every 'nth' frame. This option will skip frames when extracting "
|
"Extract every 'nth' frame. This option will skip frames when extracting "
|
||||||
"faces. For example a value of 1 will extract faces from every frame, a value "
|
"faces. For example a value of 1 will extract faces from every frame, a value "
|
||||||
|
|
@ -324,7 +333,7 @@ msgstr ""
|
||||||
"лиц. Например, значение 1 будет извлекать лица из каждого кадра, значение 10 "
|
"лиц. Например, значение 1 будет извлекать лица из каждого кадра, значение 10 "
|
||||||
"будет извлекать лица из каждого 10-го кадра."
|
"будет извлекать лица из каждого 10-го кадра."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:342
|
#: lib/cli/args_extract_convert.py:346
|
||||||
msgid ""
|
msgid ""
|
||||||
"Automatically save the alignments file after a set amount of frames. By "
|
"Automatically save the alignments file after a set amount of frames. By "
|
||||||
"default the alignments file is only saved at the end of the extraction "
|
"default the alignments file is only saved at the end of the extraction "
|
||||||
|
|
@ -340,19 +349,18 @@ msgstr ""
|
||||||
"ПРЕДУПРЕЖДЕНИЕ: Не прерывайте работу скрипта при записи файла, так как он "
|
"ПРЕДУПРЕЖДЕНИЕ: Не прерывайте работу скрипта при записи файла, так как он "
|
||||||
"может быть поврежден. Установите значение 0, чтобы отключить"
|
"может быть поврежден. Установите значение 0, чтобы отключить"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:353
|
#: lib/cli/args_extract_convert.py:357
|
||||||
msgid "Draw landmarks on the ouput faces for debugging purposes."
|
msgid "Draw landmarks on the ouput faces for debugging purposes."
|
||||||
msgstr "Нарисуйте ориентиры на выходящих гранях для отладки."
|
msgstr "Нарисуйте ориентиры на выходящих гранях для отладки."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:359 lib/cli/args_extract_convert.py:369
|
#: lib/cli/args_extract_convert.py:363 lib/cli/args_extract_convert.py:373
|
||||||
#: lib/cli/args_extract_convert.py:377 lib/cli/args_extract_convert.py:384
|
#: lib/cli/args_extract_convert.py:381 lib/cli/args_extract_convert.py:388
|
||||||
#: lib/cli/args_extract_convert.py:674 lib/cli/args_extract_convert.py:686
|
#: lib/cli/args_extract_convert.py:678 lib/cli/args_extract_convert.py:691
|
||||||
#: lib/cli/args_extract_convert.py:695 lib/cli/args_extract_convert.py:716
|
#: lib/cli/args_extract_convert.py:712 lib/cli/args_extract_convert.py:718
|
||||||
#: lib/cli/args_extract_convert.py:722
|
|
||||||
msgid "settings"
|
msgid "settings"
|
||||||
msgstr "настройки"
|
msgstr "настройки"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:361
|
#: lib/cli/args_extract_convert.py:365
|
||||||
msgid ""
|
msgid ""
|
||||||
"Don't run extraction in parallel. Will run each part of the extraction "
|
"Don't run extraction in parallel. Will run each part of the extraction "
|
||||||
"process separately (one after the other) rather than all at the same time. "
|
"process separately (one after the other) rather than all at the same time. "
|
||||||
|
|
@ -362,7 +370,7 @@ msgstr ""
|
||||||
"выполняться отдельно (одна за другой), а не одновременно. Полезно, если "
|
"выполняться отдельно (одна за другой), а не одновременно. Полезно, если "
|
||||||
"память VRAM ограничена."
|
"память VRAM ограничена."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:371
|
#: lib/cli/args_extract_convert.py:375
|
||||||
msgid ""
|
msgid ""
|
||||||
"Skips frames that have already been extracted and exist in the alignments "
|
"Skips frames that have already been extracted and exist in the alignments "
|
||||||
"file"
|
"file"
|
||||||
|
|
@ -370,17 +378,17 @@ msgstr ""
|
||||||
"Пропускает кадры, которые уже были извлечены и существуют в файле "
|
"Пропускает кадры, которые уже были извлечены и существуют в файле "
|
||||||
"выравнивания"
|
"выравнивания"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:378
|
#: lib/cli/args_extract_convert.py:382
|
||||||
msgid "Skip frames that already have detected faces in the alignments file"
|
msgid "Skip frames that already have detected faces in the alignments file"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Пропустить кадры, в которых уже есть обнаруженные лица в файле выравнивания"
|
"Пропустить кадры, в которых уже есть обнаруженные лица в файле выравнивания"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:385
|
#: lib/cli/args_extract_convert.py:389
|
||||||
msgid "Skip saving the detected faces to disk. Just create an alignments file"
|
msgid "Skip saving the detected faces to disk. Just create an alignments file"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Не сохранять обнаруженные лица на диск. Просто создать файл выравнивания"
|
"Не сохранять обнаруженные лица на диск. Просто создать файл выравнивания"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:459
|
#: lib/cli/args_extract_convert.py:463
|
||||||
msgid ""
|
msgid ""
|
||||||
"Swap the original faces in a source video/images to your final faces.\n"
|
"Swap the original faces in a source video/images to your final faces.\n"
|
||||||
"Conversion plugins can be configured in the 'Settings' Menu"
|
"Conversion plugins can be configured in the 'Settings' Menu"
|
||||||
|
|
@ -388,7 +396,7 @@ msgstr ""
|
||||||
"Поменять исходные лица в исходном видео/изображении на ваши конечные лица.\n"
|
"Поменять исходные лица в исходном видео/изображении на ваши конечные лица.\n"
|
||||||
"Плагины конвертирования можно настроить в меню \"Настройки\""
|
"Плагины конвертирования можно настроить в меню \"Настройки\""
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:481
|
#: lib/cli/args_extract_convert.py:485
|
||||||
msgid ""
|
msgid ""
|
||||||
"Only required if converting from images to video. Provide The original video "
|
"Only required if converting from images to video. Provide The original video "
|
||||||
"that the source frames were extracted from (for extracting the fps and "
|
"that the source frames were extracted from (for extracting the fps and "
|
||||||
|
|
@ -398,7 +406,7 @@ msgstr ""
|
||||||
"исходное видео, из которого были извлечены исходные кадры (для извлечения "
|
"исходное видео, из которого были извлечены исходные кадры (для извлечения "
|
||||||
"кадров в секунду и звука)."
|
"кадров в секунду и звука)."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:490
|
#: lib/cli/args_extract_convert.py:494
|
||||||
msgid ""
|
msgid ""
|
||||||
"Model directory. The directory containing the trained model you wish to use "
|
"Model directory. The directory containing the trained model you wish to use "
|
||||||
"for conversion."
|
"for conversion."
|
||||||
|
|
@ -406,7 +414,7 @@ msgstr ""
|
||||||
"Папка модели. Папка, содержащая обученную модель, которую вы хотите "
|
"Папка модели. Папка, содержащая обученную модель, которую вы хотите "
|
||||||
"использовать для преобразования."
|
"использовать для преобразования."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:501
|
#: lib/cli/args_extract_convert.py:505
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|Performs color adjustment to the swapped face. Some of these options have "
|
"R|Performs color adjustment to the swapped face. Some of these options have "
|
||||||
"configurable settings in '/config/convert.ini' or 'Settings > Configure "
|
"configurable settings in '/config/convert.ini' or 'Settings > Configure "
|
||||||
|
|
@ -446,7 +454,7 @@ msgstr ""
|
||||||
"Обычно дает не очень удовлетворительные результаты.\n"
|
"Обычно дает не очень удовлетворительные результаты.\n"
|
||||||
"L|none: Не выполнять коррекцию цвета."
|
"L|none: Не выполнять коррекцию цвета."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:527
|
#: lib/cli/args_extract_convert.py:531
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|Masker to use. NB: The mask you require must exist within the alignments "
|
"R|Masker to use. NB: The mask you require must exist within the alignments "
|
||||||
"file. You can add additional masks with the Mask Tool.\n"
|
"file. You can add additional masks with the Mask Tool.\n"
|
||||||
|
|
@ -517,7 +525,7 @@ msgstr ""
|
||||||
"L|predicted: Если во время обучения была включена опция 'Изучить Маску', то "
|
"L|predicted: Если во время обучения была включена опция 'Изучить Маску', то "
|
||||||
"будет использоваться маска, созданная обученной моделью."
|
"будет использоваться маска, созданная обученной моделью."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:566
|
#: lib/cli/args_extract_convert.py:570
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|The plugin to use to output the converted images. The writers are "
|
"R|The plugin to use to output the converted images. The writers are "
|
||||||
"configurable in '/config/convert.ini' or 'Settings > Configure Convert "
|
"configurable in '/config/convert.ini' or 'Settings > Configure Convert "
|
||||||
|
|
@ -550,12 +558,12 @@ msgstr ""
|
||||||
"L|pillow: [изображения] Медленнее, чем opencv, но имеет больше опций и "
|
"L|pillow: [изображения] Медленнее, чем opencv, но имеет больше опций и "
|
||||||
"поддерживает больше форматов."
|
"поддерживает больше форматов."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:587 lib/cli/args_extract_convert.py:596
|
#: lib/cli/args_extract_convert.py:591 lib/cli/args_extract_convert.py:600
|
||||||
#: lib/cli/args_extract_convert.py:707
|
#: lib/cli/args_extract_convert.py:703
|
||||||
msgid "Frame Processing"
|
msgid "Frame Processing"
|
||||||
msgstr "Обработка лиц"
|
msgstr "Обработка лиц"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:589
|
#: lib/cli/args_extract_convert.py:593
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"Scale the final output frames by this amount. 100%% will output the frames "
|
"Scale the final output frames by this amount. 100%% will output the frames "
|
||||||
|
|
@ -565,7 +573,7 @@ msgstr ""
|
||||||
"кадры в исходном размере. 50%% при половинном размере 200%% при двойном "
|
"кадры в исходном размере. 50%% при половинном размере 200%% при двойном "
|
||||||
"размере"
|
"размере"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:598
|
#: lib/cli/args_extract_convert.py:602
|
||||||
msgid ""
|
msgid ""
|
||||||
"Frame ranges to apply transfer to e.g. For frames 10 to 50 and 90 to 100 use "
|
"Frame ranges to apply transfer to e.g. For frames 10 to 50 and 90 to 100 use "
|
||||||
"--frame-ranges 10-50 90-100. Frames falling outside of the selected range "
|
"--frame-ranges 10-50 90-100. Frames falling outside of the selected range "
|
||||||
|
|
@ -578,7 +586,7 @@ msgstr ""
|
||||||
"keep-unchanged). Примечание: Если вы конвертируете из изображений, то имена "
|
"keep-unchanged). Примечание: Если вы конвертируете из изображений, то имена "
|
||||||
"файлов должны заканчиваться номером кадра!"
|
"файлов должны заканчиваться номером кадра!"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:612
|
#: lib/cli/args_extract_convert.py:616
|
||||||
msgid ""
|
msgid ""
|
||||||
"Scale the swapped face by this percentage. Positive values will enlarge the "
|
"Scale the swapped face by this percentage. Positive values will enlarge the "
|
||||||
"face, Negative values will shrink the face."
|
"face, Negative values will shrink the face."
|
||||||
|
|
@ -586,7 +594,7 @@ msgstr ""
|
||||||
"Увеличить масштаб нового лица на этот процент. Положительные значения "
|
"Увеличить масштаб нового лица на этот процент. Положительные значения "
|
||||||
"увеличат лицо, в то время как отрицательные значения уменьшат его."
|
"увеличат лицо, в то время как отрицательные значения уменьшат его."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:621
|
#: lib/cli/args_extract_convert.py:625
|
||||||
msgid ""
|
msgid ""
|
||||||
"If you have not cleansed your alignments file, then you can filter out faces "
|
"If you have not cleansed your alignments file, then you can filter out faces "
|
||||||
"by defining a folder here that contains the faces extracted from your input "
|
"by defining a folder here that contains the faces extracted from your input "
|
||||||
|
|
@ -602,7 +610,7 @@ msgstr ""
|
||||||
"Если оставить этот параметр пустым, будут преобразованы все лица, "
|
"Если оставить этот параметр пустым, будут преобразованы все лица, "
|
||||||
"существующие в файле выравнивания."
|
"существующие в файле выравнивания."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:636
|
#: lib/cli/args_extract_convert.py:640
|
||||||
msgid ""
|
msgid ""
|
||||||
"Optionally filter out people who you do not wish to process by passing in an "
|
"Optionally filter out people who you do not wish to process by passing in an "
|
||||||
"image of that person. Should be a front portrait with a single person in the "
|
"image of that person. Should be a front portrait with a single person in the "
|
||||||
|
|
@ -616,7 +624,7 @@ msgstr ""
|
||||||
"разделенных пробелами. Примечание: Использование фильтра лиц значительно "
|
"разделенных пробелами. Примечание: Использование фильтра лиц значительно "
|
||||||
"снизит скорость извлечения, а его точность не гарантируется."
|
"снизит скорость извлечения, а его точность не гарантируется."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:649
|
#: lib/cli/args_extract_convert.py:653
|
||||||
msgid ""
|
msgid ""
|
||||||
"Optionally select people you wish to process by passing in an image of that "
|
"Optionally select people you wish to process by passing in an image of that "
|
||||||
"person. Should be a front portrait with a single person in the image. "
|
"person. Should be a front portrait with a single person in the image. "
|
||||||
|
|
@ -630,7 +638,7 @@ msgstr ""
|
||||||
"Примечание: Использование фильтра лиц значительно снизит скорость "
|
"Примечание: Использование фильтра лиц значительно снизит скорость "
|
||||||
"извлечения, а его точность не гарантируется."
|
"извлечения, а его точность не гарантируется."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:663
|
#: lib/cli/args_extract_convert.py:667
|
||||||
msgid ""
|
msgid ""
|
||||||
"For use with the optional nfilter/filter files. Threshold for positive face "
|
"For use with the optional nfilter/filter files. Threshold for positive face "
|
||||||
"recognition. Lower values are stricter. NB: Using face filter will "
|
"recognition. Lower values are stricter. NB: Using face filter will "
|
||||||
|
|
@ -642,7 +650,7 @@ msgstr ""
|
||||||
"строгими. Примечание: Использование фильтра лиц значительно снизит скорость "
|
"строгими. Примечание: Использование фильтра лиц значительно снизит скорость "
|
||||||
"извлечения, а его точность не гарантируется."
|
"извлечения, а его точность не гарантируется."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:676
|
#: lib/cli/args_extract_convert.py:680
|
||||||
msgid ""
|
msgid ""
|
||||||
"The maximum number of parallel processes for performing conversion. "
|
"The maximum number of parallel processes for performing conversion. "
|
||||||
"Converting images is system RAM heavy so it is possible to run out of memory "
|
"Converting images is system RAM heavy so it is possible to run out of memory "
|
||||||
|
|
@ -660,16 +668,7 @@ msgstr ""
|
||||||
"процессов, чем доступно в вашей системе. Если включена однопоточная "
|
"процессов, чем доступно в вашей системе. Если включена однопоточная "
|
||||||
"обработка, этот параметр будет проигнорирован."
|
"обработка, этот параметр будет проигнорирован."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:688
|
#: lib/cli/args_extract_convert.py:693
|
||||||
msgid ""
|
|
||||||
"[LEGACY] This only needs to be selected if a legacy model is being loaded or "
|
|
||||||
"if there are multiple models in the model folder"
|
|
||||||
msgstr ""
|
|
||||||
"[ОТБРОШЕН] Этот параметр необходимо выбрать только в том случае, если "
|
|
||||||
"загружается устаревшая модель или если в папке моделей имеется несколько "
|
|
||||||
"моделей"
|
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:697
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"Enable On-The-Fly Conversion. NOT recommended. You should generate a clean "
|
"Enable On-The-Fly Conversion. NOT recommended. You should generate a clean "
|
||||||
"alignments file for your destination video. However, if you wish you can "
|
"alignments file for your destination video. However, if you wish you can "
|
||||||
|
|
@ -684,7 +683,7 @@ msgstr ""
|
||||||
"приведет к некачественным результатам. Если файл выравнивания найден, этот "
|
"приведет к некачественным результатам. Если файл выравнивания найден, этот "
|
||||||
"параметр будет проигнорирован."
|
"параметр будет проигнорирован."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:709
|
#: lib/cli/args_extract_convert.py:705
|
||||||
msgid ""
|
msgid ""
|
||||||
"When used with --frame-ranges outputs the unchanged frames that are not "
|
"When used with --frame-ranges outputs the unchanged frames that are not "
|
||||||
"processed instead of discarding them."
|
"processed instead of discarding them."
|
||||||
|
|
@ -692,12 +691,20 @@ msgstr ""
|
||||||
"При использовании с --frame-ranges выводит неизмененные кадры, которые не "
|
"При использовании с --frame-ranges выводит неизмененные кадры, которые не "
|
||||||
"были обработаны, вместо того, чтобы отбрасывать их."
|
"были обработаны, вместо того, чтобы отбрасывать их."
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:717
|
#: lib/cli/args_extract_convert.py:713
|
||||||
msgid "Swap the model. Instead converting from of A -> B, converts B -> A"
|
msgid "Swap the model. Instead converting from of A -> B, converts B -> A"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Поменять модель местами. Вместо преобразования из A -> B, преобразуется B -> "
|
"Поменять модель местами. Вместо преобразования из A -> B, преобразуется B -> "
|
||||||
"A"
|
"A"
|
||||||
|
|
||||||
#: lib/cli/args_extract_convert.py:723
|
#: lib/cli/args_extract_convert.py:719
|
||||||
msgid "Disable multiprocessing. Slower but less resource intensive."
|
msgid "Disable multiprocessing. Slower but less resource intensive."
|
||||||
msgstr "Отключение многопоточной обработки. Медленнее, но менее ресурсоемко."
|
msgstr "Отключение многопоточной обработки. Медленнее, но менее ресурсоемко."
|
||||||
|
|
||||||
|
#~ msgid ""
|
||||||
|
#~ "[LEGACY] This only needs to be selected if a legacy model is being loaded "
|
||||||
|
#~ "or if there are multiple models in the model folder"
|
||||||
|
#~ msgstr ""
|
||||||
|
#~ "[ОТБРОШЕН] Этот параметр необходимо выбрать только в том случае, если "
|
||||||
|
#~ "загружается устаревшая модель или если в папке моделей имеется несколько "
|
||||||
|
#~ "моделей"
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -7,8 +7,8 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-03-28 23:49+0000\n"
|
"POT-Creation-Date: 2024-04-12 12:10+0100\n"
|
||||||
"PO-Revision-Date: 2024-03-29 00:08+0000\n"
|
"PO-Revision-Date: 2024-04-12 12:13+0100\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: \n"
|
"Language-Team: \n"
|
||||||
"Language: ru\n"
|
"Language: ru\n"
|
||||||
|
|
@ -65,17 +65,23 @@ msgstr ""
|
||||||
msgid " Use the output option (-o) to process results."
|
msgid " Use the output option (-o) to process results."
|
||||||
msgstr " Используйте опцию вывода (-o) для обработки результатов."
|
msgstr " Используйте опцию вывода (-o) для обработки результатов."
|
||||||
|
|
||||||
#: tools/alignments/cli.py:57 tools/alignments/cli.py:97
|
#: tools/alignments/cli.py:58 tools/alignments/cli.py:104
|
||||||
msgid "processing"
|
msgid "processing"
|
||||||
msgstr "обработка"
|
msgstr "обработка"
|
||||||
|
|
||||||
#: tools/alignments/cli.py:60
|
#: tools/alignments/cli.py:61
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|Choose which action you want to perform. NB: All actions require an "
|
"R|Choose which action you want to perform. NB: All actions require an "
|
||||||
"alignments file (-a) to be passed in.\n"
|
"alignments file (-a) to be passed in.\n"
|
||||||
"L|'draw': Draw landmarks on frames in the selected folder/video. A subfolder "
|
"L|'draw': Draw landmarks on frames in the selected folder/video. A subfolder "
|
||||||
"will be created within the frames folder to hold the output.{0}\n"
|
"will be created within the frames folder to hold the output.{0}\n"
|
||||||
|
"L|'export': Export the contents of an alignments file to a json file. Can be "
|
||||||
|
"used for editing alignment information in external tools and then re-"
|
||||||
|
"importing by using Faceswap's Extract 'Import' plugins. Note: masks and "
|
||||||
|
"identity vectors will not be included in the exported file, so will be re-"
|
||||||
|
"generated when the json file is imported back into Faceswap. All data is "
|
||||||
|
"exported with the origin (0, 0) at the top left of the canvas.\n"
|
||||||
"L|'extract': Re-extract faces from the source frames/video based on "
|
"L|'extract': Re-extract faces from the source frames/video based on "
|
||||||
"alignment data. This is a lot quicker than re-detecting faces. Can pass in "
|
"alignment data. This is a lot quicker than re-detecting faces. Can pass in "
|
||||||
"the '-een' (--extract-every-n) parameter to only extract every nth frame."
|
"the '-een' (--extract-every-n) parameter to only extract every nth frame."
|
||||||
|
|
@ -109,6 +115,13 @@ msgstr ""
|
||||||
"требуют передачи файла выравнивания (-a).\n"
|
"требуют передачи файла выравнивания (-a).\n"
|
||||||
"L|'draw': Нарисовать ориентиры на кадрах в выбранной папке/видео. В папке "
|
"L|'draw': Нарисовать ориентиры на кадрах в выбранной папке/видео. В папке "
|
||||||
"frames будет создана подпапка для хранения результатов.\n"
|
"frames будет создана подпапка для хранения результатов.\n"
|
||||||
|
"L|'export': экспортировать содержимое файла выравнивания в файл JSON. Может "
|
||||||
|
"использоваться для редактирования информации о выравнивании во внешних "
|
||||||
|
"инструментах, а затем повторно импортируется с помощью плагинов Faceswap "
|
||||||
|
"Extract 'Import'. ПРИМЕЧАНИЕ. Маски и векторы идентификации не будут "
|
||||||
|
"включены в экспортированный файл, поэтому будут повторно сгенерированы, "
|
||||||
|
"когда файл JSON будет импортирован обратно в Faceswap. Все данные "
|
||||||
|
"экспортируются с началом координат (0, 0) в верхнем левом углу холста.\n"
|
||||||
"L|'extract': Повторное извлечение лиц из исходных кадров/видео на основе "
|
"L|'extract': Повторное извлечение лиц из исходных кадров/видео на основе "
|
||||||
"данных о выравнивании. Это намного быстрее, чем повторное обнаружение лиц. "
|
"данных о выравнивании. Это намного быстрее, чем повторное обнаружение лиц. "
|
||||||
"Можно передать параметр '-een' (--extract-every-n), чтобы извлекать только "
|
"Можно передать параметр '-een' (--extract-every-n), чтобы извлекать только "
|
||||||
|
|
@ -139,7 +152,7 @@ msgstr ""
|
||||||
"L|'spatial': Выполнить пространственную и временную фильтрацию для "
|
"L|'spatial': Выполнить пространственную и временную фильтрацию для "
|
||||||
"сглаживания выравниваний (ЭКСПЕРИМЕНТАЛЬНО!)."
|
"сглаживания выравниваний (ЭКСПЕРИМЕНТАЛЬНО!)."
|
||||||
|
|
||||||
#: tools/alignments/cli.py:100
|
#: tools/alignments/cli.py:107
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|How to output discovered items ('faces' and 'frames' only):\n"
|
"R|How to output discovered items ('faces' and 'frames' only):\n"
|
||||||
"L|'console': Print the list of frames to the screen. (DEFAULT)\n"
|
"L|'console': Print the list of frames to the screen. (DEFAULT)\n"
|
||||||
|
|
@ -154,12 +167,12 @@ msgstr ""
|
||||||
"каталоге).\n"
|
"каталоге).\n"
|
||||||
"L|'move': Переместить обнаруженные элементы в подпапку в исходном каталоге."
|
"L|'move': Переместить обнаруженные элементы в подпапку в исходном каталоге."
|
||||||
|
|
||||||
#: tools/alignments/cli.py:111 tools/alignments/cli.py:134
|
#: tools/alignments/cli.py:118 tools/alignments/cli.py:141
|
||||||
#: tools/alignments/cli.py:141
|
#: tools/alignments/cli.py:148
|
||||||
msgid "data"
|
msgid "data"
|
||||||
msgstr "данные"
|
msgstr "данные"
|
||||||
|
|
||||||
#: tools/alignments/cli.py:118
|
#: tools/alignments/cli.py:125
|
||||||
msgid ""
|
msgid ""
|
||||||
"Full path to the alignments file to be processed. If you have input a "
|
"Full path to the alignments file to be processed. If you have input a "
|
||||||
"'frames_dir' and don't provide this option, the process will try to find the "
|
"'frames_dir' and don't provide this option, the process will try to find the "
|
||||||
|
|
@ -173,11 +186,11 @@ msgstr ""
|
||||||
"задания 'from-faces', когда файл выравнивания будет создан в указанной папке "
|
"задания 'from-faces', когда файл выравнивания будет создан в указанной папке "
|
||||||
"с лицами."
|
"с лицами."
|
||||||
|
|
||||||
#: tools/alignments/cli.py:135
|
#: tools/alignments/cli.py:142
|
||||||
msgid "Directory containing source frames that faces were extracted from."
|
msgid "Directory containing source frames that faces were extracted from."
|
||||||
msgstr "Папка, содержащая исходные кадры, из которых были извлечены лица."
|
msgstr "Папка, содержащая исходные кадры, из которых были извлечены лица."
|
||||||
|
|
||||||
#: tools/alignments/cli.py:143
|
#: tools/alignments/cli.py:150
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|Run the aligmnents tool on multiple sources. The following jobs support "
|
"R|Run the aligmnents tool on multiple sources. The following jobs support "
|
||||||
"batch mode:\n"
|
"batch mode:\n"
|
||||||
|
|
@ -220,12 +233,12 @@ msgstr ""
|
||||||
"выравнивания должен существовать в месте по умолчанию. Для всех остальных "
|
"выравнивания должен существовать в месте по умолчанию. Для всех остальных "
|
||||||
"заданий этот параметр игнорируется."
|
"заданий этот параметр игнорируется."
|
||||||
|
|
||||||
#: tools/alignments/cli.py:169 tools/alignments/cli.py:181
|
#: tools/alignments/cli.py:176 tools/alignments/cli.py:188
|
||||||
#: tools/alignments/cli.py:191
|
#: tools/alignments/cli.py:198
|
||||||
msgid "extract"
|
msgid "extract"
|
||||||
msgstr "извлечение"
|
msgstr "извлечение"
|
||||||
|
|
||||||
#: tools/alignments/cli.py:171
|
#: tools/alignments/cli.py:178
|
||||||
msgid ""
|
msgid ""
|
||||||
"[Extract only] Extract every 'nth' frame. This option will skip frames when "
|
"[Extract only] Extract every 'nth' frame. This option will skip frames when "
|
||||||
"extracting faces. For example a value of 1 will extract faces from every "
|
"extracting faces. For example a value of 1 will extract faces from every "
|
||||||
|
|
@ -235,11 +248,11 @@ msgstr ""
|
||||||
"кадры при извлечении лиц. Например, значение 1 будет извлекать лица из "
|
"кадры при извлечении лиц. Например, значение 1 будет извлекать лица из "
|
||||||
"каждого кадра, значение 10 будет извлекать лица из каждого 10-го кадра."
|
"каждого кадра, значение 10 будет извлекать лица из каждого 10-го кадра."
|
||||||
|
|
||||||
#: tools/alignments/cli.py:182
|
#: tools/alignments/cli.py:189
|
||||||
msgid "[Extract only] The output size of extracted faces."
|
msgid "[Extract only] The output size of extracted faces."
|
||||||
msgstr "[Только извлечение] Выходной размер извлеченных лиц."
|
msgstr "[Только извлечение] Выходной размер извлеченных лиц."
|
||||||
|
|
||||||
#: tools/alignments/cli.py:193
|
#: tools/alignments/cli.py:200
|
||||||
msgid ""
|
msgid ""
|
||||||
"[Extract only] Only extract faces that have been resized by this percent or "
|
"[Extract only] Only extract faces that have been resized by this percent or "
|
||||||
"more to meet the specified extract size (`-sz`, `--size`). Useful for "
|
"more to meet the specified extract size (`-sz`, `--size`). Useful for "
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-03-28 23:49+0000\n"
|
"POT-Creation-Date: 2024-04-12 12:10+0100\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
|
@ -53,17 +53,23 @@ msgstr ""
|
||||||
msgid " Use the output option (-o) to process results."
|
msgid " Use the output option (-o) to process results."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: tools/alignments/cli.py:57 tools/alignments/cli.py:97
|
#: tools/alignments/cli.py:58 tools/alignments/cli.py:104
|
||||||
msgid "processing"
|
msgid "processing"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: tools/alignments/cli.py:60
|
#: tools/alignments/cli.py:61
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|Choose which action you want to perform. NB: All actions require an "
|
"R|Choose which action you want to perform. NB: All actions require an "
|
||||||
"alignments file (-a) to be passed in.\n"
|
"alignments file (-a) to be passed in.\n"
|
||||||
"L|'draw': Draw landmarks on frames in the selected folder/video. A subfolder "
|
"L|'draw': Draw landmarks on frames in the selected folder/video. A subfolder "
|
||||||
"will be created within the frames folder to hold the output.{0}\n"
|
"will be created within the frames folder to hold the output.{0}\n"
|
||||||
|
"L|'export': Export the contents of an alignments file to a json file. Can be "
|
||||||
|
"used for editing alignment information in external tools and then re-"
|
||||||
|
"importing by using Faceswap's Extract 'Import' plugins. Note: masks and "
|
||||||
|
"identity vectors will not be included in the exported file, so will be re-"
|
||||||
|
"generated when the json file is imported back into Faceswap. All data is "
|
||||||
|
"exported with the origin (0, 0) at the top left of the canvas.\n"
|
||||||
"L|'extract': Re-extract faces from the source frames/video based on "
|
"L|'extract': Re-extract faces from the source frames/video based on "
|
||||||
"alignment data. This is a lot quicker than re-detecting faces. Can pass in "
|
"alignment data. This is a lot quicker than re-detecting faces. Can pass in "
|
||||||
"the '-een' (--extract-every-n) parameter to only extract every nth frame."
|
"the '-een' (--extract-every-n) parameter to only extract every nth frame."
|
||||||
|
|
@ -94,7 +100,7 @@ msgid ""
|
||||||
"(EXPERIMENTAL!)"
|
"(EXPERIMENTAL!)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: tools/alignments/cli.py:100
|
#: tools/alignments/cli.py:107
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|How to output discovered items ('faces' and 'frames' only):\n"
|
"R|How to output discovered items ('faces' and 'frames' only):\n"
|
||||||
"L|'console': Print the list of frames to the screen. (DEFAULT)\n"
|
"L|'console': Print the list of frames to the screen. (DEFAULT)\n"
|
||||||
|
|
@ -104,12 +110,12 @@ msgid ""
|
||||||
"directory."
|
"directory."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: tools/alignments/cli.py:111 tools/alignments/cli.py:134
|
#: tools/alignments/cli.py:118 tools/alignments/cli.py:141
|
||||||
#: tools/alignments/cli.py:141
|
#: tools/alignments/cli.py:148
|
||||||
msgid "data"
|
msgid "data"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: tools/alignments/cli.py:118
|
#: tools/alignments/cli.py:125
|
||||||
msgid ""
|
msgid ""
|
||||||
"Full path to the alignments file to be processed. If you have input a "
|
"Full path to the alignments file to be processed. If you have input a "
|
||||||
"'frames_dir' and don't provide this option, the process will try to find the "
|
"'frames_dir' and don't provide this option, the process will try to find the "
|
||||||
|
|
@ -118,11 +124,11 @@ msgid ""
|
||||||
"generated in the specified faces folder."
|
"generated in the specified faces folder."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: tools/alignments/cli.py:135
|
#: tools/alignments/cli.py:142
|
||||||
msgid "Directory containing source frames that faces were extracted from."
|
msgid "Directory containing source frames that faces were extracted from."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: tools/alignments/cli.py:143
|
#: tools/alignments/cli.py:150
|
||||||
msgid ""
|
msgid ""
|
||||||
"R|Run the aligmnents tool on multiple sources. The following jobs support "
|
"R|Run the aligmnents tool on multiple sources. The following jobs support "
|
||||||
"batch mode:\n"
|
"batch mode:\n"
|
||||||
|
|
@ -144,23 +150,23 @@ msgid ""
|
||||||
"ignored."
|
"ignored."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: tools/alignments/cli.py:169 tools/alignments/cli.py:181
|
#: tools/alignments/cli.py:176 tools/alignments/cli.py:188
|
||||||
#: tools/alignments/cli.py:191
|
#: tools/alignments/cli.py:198
|
||||||
msgid "extract"
|
msgid "extract"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: tools/alignments/cli.py:171
|
#: tools/alignments/cli.py:178
|
||||||
msgid ""
|
msgid ""
|
||||||
"[Extract only] Extract every 'nth' frame. This option will skip frames when "
|
"[Extract only] Extract every 'nth' frame. This option will skip frames when "
|
||||||
"extracting faces. For example a value of 1 will extract faces from every "
|
"extracting faces. For example a value of 1 will extract faces from every "
|
||||||
"frame, a value of 10 will extract faces from every 10th frame."
|
"frame, a value of 10 will extract faces from every 10th frame."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: tools/alignments/cli.py:182
|
#: tools/alignments/cli.py:189
|
||||||
msgid "[Extract only] The output size of extracted faces."
|
msgid "[Extract only] The output size of extracted faces."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: tools/alignments/cli.py:193
|
#: tools/alignments/cli.py:200
|
||||||
msgid ""
|
msgid ""
|
||||||
"[Extract only] Only extract faces that have been resized by this percent or "
|
"[Extract only] Only extract faces that have been resized by this percent or "
|
||||||
"more to meet the specified extract size (`-sz`, `--size`). Useful for "
|
"more to meet the specified extract size (`-sz`, `--size`). Useful for "
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,32 @@ from ._base import Adjustment
|
||||||
class Color(Adjustment):
|
class Color(Adjustment):
|
||||||
""" Adjust the mean of the color channels to be the same for the swap and old frame """
|
""" Adjust the mean of the color channels to be the same for the swap and old frame """
|
||||||
|
|
||||||
def process(self, old_face, new_face, raw_mask):
|
def process(self,
|
||||||
|
old_face: np.ndarray,
|
||||||
|
new_face: np.ndarray,
|
||||||
|
raw_mask: np.ndarray) -> np.ndarray:
|
||||||
|
""" Adjust the mean of the original face and the new face to be the same
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
old_face: :class:`numpy.ndarray`
|
||||||
|
The original face
|
||||||
|
new_face: :class:`numpy.ndarray`
|
||||||
|
The Faceswap generated face
|
||||||
|
raw_mask: :class:`numpy.ndarray`
|
||||||
|
A raw mask for including the face area only
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
:class:`numpy.ndarray`
|
||||||
|
The adjusted face patch
|
||||||
|
"""
|
||||||
for _ in [0, 1]:
|
for _ in [0, 1]:
|
||||||
diff = old_face - new_face
|
diff = old_face - new_face
|
||||||
avg_diff = np.sum(diff * raw_mask, axis=(0, 1))
|
if np.any(raw_mask):
|
||||||
adjustment = avg_diff / np.sum(raw_mask, axis=(0, 1))
|
avg_diff = np.sum(diff * raw_mask, axis=(0, 1))
|
||||||
|
adjustment = avg_diff / np.sum(raw_mask, axis=(0, 1))
|
||||||
|
else:
|
||||||
|
adjustment = diff
|
||||||
new_face += adjustment
|
new_face += adjustment
|
||||||
return new_face
|
return new_face
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
""" Package for Faceswap's extraction pipeline """
|
||||||
|
from .extract_media import ExtractMedia
|
||||||
|
from .pipeline import Extractor
|
||||||
|
|
@ -15,7 +15,7 @@ from lib.multithreading import MultiThread
|
||||||
from lib.queue_manager import queue_manager
|
from lib.queue_manager import queue_manager
|
||||||
from lib.utils import GetModel, FaceswapError
|
from lib.utils import GetModel, FaceswapError
|
||||||
from ._config import Config
|
from ._config import Config
|
||||||
from .pipeline import ExtractMedia
|
from . import ExtractMedia
|
||||||
|
|
||||||
if T.TYPE_CHECKING:
|
if T.TYPE_CHECKING:
|
||||||
from collections.abc import Callable, Generator, Sequence
|
from collections.abc import Callable, Generator, Sequence
|
||||||
|
|
@ -86,6 +86,18 @@ class ExtractorBatch:
|
||||||
prediction: np.ndarray = np.array([])
|
prediction: np.ndarray = np.array([])
|
||||||
data: list[dict[str, T.Any]] = field(default_factory=list)
|
data: list[dict[str, T.Any]] = field(default_factory=list)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
""" Prettier repr for debug printing """
|
||||||
|
data = [{k: (v.shape, v.dtype) if isinstance(v, np.ndarray) else v for k, v in dat.items()}
|
||||||
|
for dat in self.data]
|
||||||
|
return (f"{self.__class__.__name__}("
|
||||||
|
f"image={[(img.shape, img.dtype) for img in self.image]}, "
|
||||||
|
f"detected_faces={self.detected_faces}, "
|
||||||
|
f"filename={self.filename}, "
|
||||||
|
f"feed={[(f.shape, f.dtype) for f in self.feed]}, "
|
||||||
|
f"prediction=({self.prediction.shape}, {self.prediction.dtype}), "
|
||||||
|
f"data={data}")
|
||||||
|
|
||||||
|
|
||||||
class Extractor():
|
class Extractor():
|
||||||
""" Extractor Plugin Object
|
""" Extractor Plugin Object
|
||||||
|
|
@ -197,7 +209,7 @@ class Extractor():
|
||||||
""" list: Internal threads for this plugin """
|
""" list: Internal threads for this plugin """
|
||||||
|
|
||||||
self._extract_media: dict[str, ExtractMedia] = {}
|
self._extract_media: dict[str, ExtractMedia] = {}
|
||||||
""" dict: The :class:`plugins.extract.pipeline.ExtractMedia` objects currently being
|
""" dict: The :class:`~plugins.extract.extract_media.ExtractMedia` objects currently being
|
||||||
processed. Stored at input for pairing back up on output of extractor process """
|
processed. Stored at input for pairing back up on output of extractor process """
|
||||||
|
|
||||||
# << THE FOLLOWING PROTECTED ATTRIBUTES ARE SET IN PLUGIN TYPE _base.py >>> #
|
# << THE FOLLOWING PROTECTED ATTRIBUTES ARE SET IN PLUGIN TYPE _base.py >>> #
|
||||||
|
|
@ -276,6 +288,11 @@ class Extractor():
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def on_completion(self) -> None:
|
||||||
|
""" Override to perform an action when the extract process has completed. By default, no
|
||||||
|
action is undertaken """
|
||||||
|
return
|
||||||
|
|
||||||
def _predict(self, batch: BatchType) -> BatchType:
|
def _predict(self, batch: BatchType) -> BatchType:
|
||||||
""" **Override method** (at `<plugin_type>` level)
|
""" **Override method** (at `<plugin_type>` level)
|
||||||
|
|
||||||
|
|
@ -362,7 +379,7 @@ class Extractor():
|
||||||
:mod:`plugins.extract.detect._base`, :mod:`plugins.extract.align._base` or
|
:mod:`plugins.extract.detect._base`, :mod:`plugins.extract.align._base` or
|
||||||
:mod:`plugins.extract.mask._base`) and should not be overridden within plugins themselves.
|
:mod:`plugins.extract.mask._base`) and should not be overridden within plugins themselves.
|
||||||
|
|
||||||
Get :class:`~plugins.extract.pipeline.ExtractMedia` items from the queue in batches of
|
Get :class:`~plugins.extract.extract_media.ExtractMedia` items from the queue in batches of
|
||||||
:attr:`batchsize`
|
:attr:`batchsize`
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
|
@ -409,11 +426,11 @@ class Extractor():
|
||||||
----------
|
----------
|
||||||
queue: :class:`queue.Queue`
|
queue: :class:`queue.Queue`
|
||||||
The input queue to the aligner. Should contain
|
The input queue to the aligner. Should contain
|
||||||
:class:`~plugins.extract.pipeline.ExtractMedia` objects
|
:class:`~plugins.extract.extract_media.ExtractMedia` objects
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
:class:`~plugins.extract.pipeline.ExtractMedia` or EOF
|
:class:`~plugins.extract.extract_media.ExtractMedia` or EOF
|
||||||
The next extract media object, or EOF if pipe has ended
|
The next extract media object, or EOF if pipe has ended
|
||||||
"""
|
"""
|
||||||
if self._rollover is not None:
|
if self._rollover is not None:
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
All Aligner Plugins should inherit from this class.
|
All Aligner Plugins should inherit from this class.
|
||||||
See the override methods for which methods are required.
|
See the override methods for which methods are required.
|
||||||
|
|
||||||
The plugin will receive a :class:`~plugins.extract.pipeline.ExtractMedia` object.
|
The plugin will receive a :class:`~plugins.extract.extract_media.ExtractMedia` object.
|
||||||
|
|
||||||
For each source item, the plugin must pass a dict to finalize containing:
|
For each source item, the plugin must pass a dict to finalize containing:
|
||||||
|
|
||||||
|
|
@ -24,8 +24,10 @@ import numpy as np
|
||||||
|
|
||||||
from tensorflow.python.framework import errors_impl as tf_errors # pylint:disable=no-name-in-module # noqa
|
from tensorflow.python.framework import errors_impl as tf_errors # pylint:disable=no-name-in-module # noqa
|
||||||
|
|
||||||
|
from lib.align import LandmarkType
|
||||||
from lib.utils import FaceswapError
|
from lib.utils import FaceswapError
|
||||||
from plugins.extract._base import BatchType, Extractor, ExtractMedia, ExtractorBatch
|
from plugins.extract import ExtractMedia
|
||||||
|
from plugins.extract._base import BatchType, ExtractorBatch, Extractor
|
||||||
from .processing import AlignedFilter, ReAlign
|
from .processing import AlignedFilter, ReAlign
|
||||||
|
|
||||||
if T.TYPE_CHECKING:
|
if T.TYPE_CHECKING:
|
||||||
|
|
@ -81,20 +83,13 @@ class AlignerBatch(ExtractorBatch):
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
""" Prettier repr for debug printing """
|
""" Prettier repr for debug printing """
|
||||||
data = [{k: v.shape if isinstance(v, np.ndarray) else v for k, v in dat.items()}
|
retval = super().__repr__()
|
||||||
for dat in self.data]
|
retval += (f", batch_id={self.batch_id}, "
|
||||||
return ("AlignerBatch("
|
f"landmarks=[({self.landmarks.shape}, {self.landmarks.dtype})], "
|
||||||
f"batch_id={self.batch_id}, "
|
f"refeeds={[(f.shape, f.dtype) for f in self.refeeds]}, "
|
||||||
f"image={[img.shape for img in self.image]}, "
|
f"second_pass={self.second_pass}, "
|
||||||
f"detected_faces={self.detected_faces}, "
|
f"second_pass_masks={self.second_pass_masks})")
|
||||||
f"filename={self.filename}, "
|
return retval
|
||||||
f"feed={self.feed.shape}, "
|
|
||||||
f"prediction={self.prediction.shape}, "
|
|
||||||
f"data={data}, "
|
|
||||||
f"landmarks={self.landmarks.shape}, "
|
|
||||||
f"refeeds={[feed.shape for feed in self.refeeds]}, "
|
|
||||||
f"second_pass={self.second_pass}, "
|
|
||||||
f"second_pass_masks={self.second_pass_masks})")
|
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
""" Make sure that we have been given a non-zero ID """
|
""" Make sure that we have been given a non-zero ID """
|
||||||
|
|
@ -157,6 +152,10 @@ class Aligner(Extractor): # pylint:disable=abstract-method
|
||||||
**kwargs)
|
**kwargs)
|
||||||
self._plugin_type = "align"
|
self._plugin_type = "align"
|
||||||
self.realign_centering: CenteringType = "face" # overide for plugin specific centering
|
self.realign_centering: CenteringType = "face" # overide for plugin specific centering
|
||||||
|
|
||||||
|
# Override for specific landmark type:
|
||||||
|
self.landmark_type = LandmarkType.LM_2D_68
|
||||||
|
|
||||||
self._eof_seen = False
|
self._eof_seen = False
|
||||||
self._normalize_method: T.Literal["clahe", "hist", "mean"] | None = None
|
self._normalize_method: T.Literal["clahe", "hist", "mean"] | None = None
|
||||||
self._re_feed = re_feed
|
self._re_feed = re_feed
|
||||||
|
|
@ -244,8 +243,8 @@ class Aligner(Extractor): # pylint:disable=abstract-method
|
||||||
Items are returned from the ``queue`` in batches of
|
Items are returned from the ``queue`` in batches of
|
||||||
:attr:`~plugins.extract._base.Extractor.batchsize`
|
:attr:`~plugins.extract._base.Extractor.batchsize`
|
||||||
|
|
||||||
Items are received as :class:`~plugins.extract.pipeline.ExtractMedia` objects and converted
|
Items are received as :class:`~plugins.extract.extract_media.ExtractMedia` objects and
|
||||||
to ``dict`` for internal processing.
|
converted to ``dict`` for internal processing.
|
||||||
|
|
||||||
To ensure consistent batch sizes for aligner the items are split into separate items for
|
To ensure consistent batch sizes for aligner the items are split into separate items for
|
||||||
each :class:`~lib.align.DetectedFace` object.
|
each :class:`~lib.align.DetectedFace` object.
|
||||||
|
|
@ -317,10 +316,6 @@ class Aligner(Extractor): # pylint:disable=abstract-method
|
||||||
else:
|
else:
|
||||||
logger.debug(item)
|
logger.debug(item)
|
||||||
|
|
||||||
# TODO Move to end of process not beginning
|
|
||||||
if exhausted:
|
|
||||||
self._filter.output_counts()
|
|
||||||
|
|
||||||
return exhausted, batch
|
return exhausted, batch
|
||||||
|
|
||||||
def faces_to_feed(self, faces: np.ndarray) -> np.ndarray:
|
def faces_to_feed(self, faces: np.ndarray) -> np.ndarray:
|
||||||
|
|
@ -354,7 +349,7 @@ class Aligner(Extractor): # pylint:disable=abstract-method
|
||||||
|
|
||||||
Yields
|
Yields
|
||||||
------
|
------
|
||||||
:class:`~plugins.extract.pipeline.ExtractMedia`
|
:class:`~plugins.extract.extract_media.ExtractMedia`
|
||||||
The :attr:`DetectedFaces` list will be populated for this class with the bounding boxes
|
The :attr:`DetectedFaces` list will be populated for this class with the bounding boxes
|
||||||
and landmarks for the detected faces found in the frame.
|
and landmarks for the detected faces found in the frame.
|
||||||
"""
|
"""
|
||||||
|
|
@ -388,6 +383,10 @@ class Aligner(Extractor): # pylint:disable=abstract-method
|
||||||
yield output
|
yield output
|
||||||
self._re_align.untrack_batch(batch.batch_id)
|
self._re_align.untrack_batch(batch.batch_id)
|
||||||
|
|
||||||
|
def on_completion(self) -> None:
|
||||||
|
""" Output the filter counts when process has completed """
|
||||||
|
self._filter.output_counts()
|
||||||
|
|
||||||
# <<< PROTECTED METHODS >>> #
|
# <<< PROTECTED METHODS >>> #
|
||||||
# << PROCESS_INPUT WRAPPER >>
|
# << PROCESS_INPUT WRAPPER >>
|
||||||
def _get_adjusted_boxes(self, original_boxes: np.ndarray) -> np.ndarray:
|
def _get_adjusted_boxes(self, original_boxes: np.ndarray) -> np.ndarray:
|
||||||
|
|
@ -584,7 +583,7 @@ class Aligner(Extractor): # pylint:disable=abstract-method
|
||||||
if not all_filtered:
|
if not all_filtered:
|
||||||
feed = batch.refeeds[selected_idx]
|
feed = batch.refeeds[selected_idx]
|
||||||
pred = batch.prediction[selected_idx]
|
pred = batch.prediction[selected_idx]
|
||||||
data = batch.data[selected_idx]
|
data = batch.data[selected_idx] if batch.data else {}
|
||||||
selected_idx += 1
|
selected_idx += 1
|
||||||
else: # All resuts have been filtered out
|
else: # All resuts have been filtered out
|
||||||
feed = pred = np.array([])
|
feed = pred = np.array([])
|
||||||
|
|
@ -604,14 +603,15 @@ class Aligner(Extractor): # pylint:disable=abstract-method
|
||||||
|
|
||||||
retval.append(subbatch)
|
retval.append(subbatch)
|
||||||
else:
|
else:
|
||||||
for feed, pred, data in zip(batch.refeeds, batch.prediction, batch.data):
|
b_data = batch.data if batch.data else [{}]
|
||||||
|
for feed, pred, dat in zip(batch.refeeds, batch.prediction, b_data):
|
||||||
subbatch = AlignerBatch(batch_id=batch.batch_id,
|
subbatch = AlignerBatch(batch_id=batch.batch_id,
|
||||||
image=batch.image,
|
image=batch.image,
|
||||||
detected_faces=batch.detected_faces,
|
detected_faces=batch.detected_faces,
|
||||||
filename=batch.filename,
|
filename=batch.filename,
|
||||||
feed=feed,
|
feed=feed,
|
||||||
prediction=pred,
|
prediction=pred,
|
||||||
data=[data],
|
data=[dat],
|
||||||
second_pass=batch.second_pass)
|
second_pass=batch.second_pass)
|
||||||
self.process_output(subbatch)
|
self.process_output(subbatch)
|
||||||
retval.append(subbatch)
|
retval.append(subbatch)
|
||||||
|
|
|
||||||
277
plugins/extract/align/external.py
Normal file
277
plugins/extract/align/external.py
Normal file
|
|
@ -0,0 +1,277 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
""" Import 68 point landmarks or ROI boxes from a json file """
|
||||||
|
import logging
|
||||||
|
import typing as T
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from lib.align import EXTRACT_RATIOS, LandmarkType
|
||||||
|
from lib.utils import FaceswapError, IMAGE_EXTENSIONS
|
||||||
|
|
||||||
|
from ._base import BatchType, Aligner, AlignerBatch
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Align(Aligner):
|
||||||
|
""" Import face detection bounding boxes from an external json file """
|
||||||
|
def __init__(self, **kwargs) -> None:
|
||||||
|
kwargs["normalize_method"] = None # Disable normalization
|
||||||
|
kwargs["re_feed"] = 0 # Disable re-feed
|
||||||
|
kwargs["re_align"] = False # Disablle re-align
|
||||||
|
kwargs["disable_filter"] = True # Disable aligner filters
|
||||||
|
super().__init__(git_model_id=None, model_filename=None, **kwargs)
|
||||||
|
|
||||||
|
self.name = "External"
|
||||||
|
self.batchsize = 16
|
||||||
|
|
||||||
|
self._origin: T.Literal["top-left",
|
||||||
|
"bottom-left",
|
||||||
|
"top-right",
|
||||||
|
"bottom-right"] = self.config["origin"]
|
||||||
|
|
||||||
|
self._re_frame_no: re.Pattern = re.compile(r"\d+$")
|
||||||
|
self._is_video: bool = False
|
||||||
|
self._imported: dict[str | int, tuple[int, np.ndarray]] = {}
|
||||||
|
"""dict[str | int, tuple[int, np.ndarray]]: filename as key, value of [number of faces
|
||||||
|
remaining for the frame, all landmarks in the frame] """
|
||||||
|
|
||||||
|
self._missing: list[str] = []
|
||||||
|
self._roll: dict[T.Literal["bottom-left", "top-right", "bottom-right"], int] = {
|
||||||
|
"bottom-left": 3, "top-right": 1, "bottom-right": 2}
|
||||||
|
"""dict[Literal["bottom-left", "top-right", "bottom-right"], int]: Amount to roll the
|
||||||
|
points by for different origins when 4 Point ROI landmarks are provided """
|
||||||
|
|
||||||
|
centering = self.config["4_point_centering"]
|
||||||
|
self._adjustment: float = 1. if centering is None else 1. - EXTRACT_RATIOS[centering]
|
||||||
|
"""float: The amount to adjust 4 point ROI landmarks to standardize the points for a
|
||||||
|
'head' sized extracted face """
|
||||||
|
|
||||||
|
def init_model(self) -> None:
|
||||||
|
""" No initialization to perform """
|
||||||
|
logger.debug("No aligner model to initialize")
|
||||||
|
|
||||||
|
def _check_for_video(self, filename: str) -> None:
|
||||||
|
""" Check a sample filename from the import file for a file extension to set
|
||||||
|
:attr:`_is_video`
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
filename: str
|
||||||
|
A sample file name from the imported data
|
||||||
|
"""
|
||||||
|
logger.debug("Checking for video from '%s'", filename)
|
||||||
|
ext = os.path.splitext(filename)[-1]
|
||||||
|
if ext.lower() not in IMAGE_EXTENSIONS:
|
||||||
|
self._is_video = True
|
||||||
|
logger.debug("Set is_video to %s from extension '%s'", self._is_video, ext)
|
||||||
|
|
||||||
|
def _get_key(self, key: str) -> str | int:
|
||||||
|
""" Obtain the key for the item in the lookup table. If the input are images, the key will
|
||||||
|
be the image filename. If the input is a video, the key will be the frame number
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
key: str
|
||||||
|
The initial key value from import data or an import image/frame
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str | int
|
||||||
|
The filename is the input data is images, otherwise the frame number of a video
|
||||||
|
"""
|
||||||
|
if not self._is_video:
|
||||||
|
return key
|
||||||
|
original_name = os.path.splitext(key)[0]
|
||||||
|
matches = self._re_frame_no.findall(original_name)
|
||||||
|
if not matches or len(matches) > 1:
|
||||||
|
raise FaceswapError(f"Invalid import name: '{key}'. For video files, the key should "
|
||||||
|
"end with the frame number.")
|
||||||
|
retval = int(matches[0])
|
||||||
|
logger.trace("Obtained frame number %s from key '%s'", # type:ignore[attr-defined]
|
||||||
|
retval, key)
|
||||||
|
return retval
|
||||||
|
|
||||||
|
def _import_face(self, face: dict[str, list[int] | list[list[float]]]) -> np.ndarray:
|
||||||
|
""" Import the landmarks from a single face
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
face: dict[str, list[int] | list[list[float]]]
|
||||||
|
An import dictionary item for a face
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
:class:`numpy.ndarray`
|
||||||
|
The landmark data imported from the json file
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
FaceSwapError
|
||||||
|
If the landmarks_2d key does not exist or the landmarks are in an incorrect format
|
||||||
|
"""
|
||||||
|
landmarks = face.get("landmarks_2d")
|
||||||
|
if landmarks is None:
|
||||||
|
raise FaceswapError("The provided import file is the required key 'landmarks_2d")
|
||||||
|
if len(landmarks) not in (4, 68):
|
||||||
|
raise FaceswapError("Imported 'landmarks_2d' should be either 68 facial feature "
|
||||||
|
"landmarks or 4 ROI corner locations")
|
||||||
|
retval = np.array(landmarks, dtype="float32")
|
||||||
|
if retval.shape[-1] != 2:
|
||||||
|
raise FaceswapError("Imported 'landmarks_2d' should be formatted as a list of (x, y) "
|
||||||
|
"co-ordinates")
|
||||||
|
if retval.shape[0] == 4: # Adjust ROI landmarks based on centering selected
|
||||||
|
center = np.mean(retval, axis=0)
|
||||||
|
retval = (retval - center) * self._adjustment + center
|
||||||
|
|
||||||
|
return retval
|
||||||
|
|
||||||
|
def import_data(self, data: dict[str, list[dict[str, list[int] | list[list[float]]]]]) -> None:
|
||||||
|
""" Import the aligner data from the json import file and set to :attr:`_imported`
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
data: dict[str, list[dict[str, list[int] | list[list[float]]]]]
|
||||||
|
The data to be imported
|
||||||
|
"""
|
||||||
|
logger.debug("Data length: %s", len(data))
|
||||||
|
self._check_for_video(list(data)[0])
|
||||||
|
for key, faces in data.items():
|
||||||
|
try:
|
||||||
|
lms = np.array([self._import_face(face) for face in faces], dtype="float32")
|
||||||
|
if not np.any(lms):
|
||||||
|
logger.trace("Skipping frame '%s' with no faces") # type:ignore[attr-defined]
|
||||||
|
continue
|
||||||
|
|
||||||
|
store_key = self._get_key(key)
|
||||||
|
self._imported[store_key] = (lms.shape[0], lms)
|
||||||
|
except FaceswapError as err:
|
||||||
|
logger.error(str(err))
|
||||||
|
msg = f"The imported frame key that failed was '{key}'"
|
||||||
|
raise FaceswapError(msg) from err
|
||||||
|
lm_shape = set(v[1].shape[1:] for v in self._imported.values() if v[0] > 0)
|
||||||
|
if len(lm_shape) > 1:
|
||||||
|
raise FaceswapError("All external data should have the same number of landmarks. "
|
||||||
|
f"Found landmarks of shape: {lm_shape}")
|
||||||
|
if (4, 2) in lm_shape:
|
||||||
|
self.landmark_type = LandmarkType.LM_2D_4
|
||||||
|
|
||||||
|
def process_input(self, batch: BatchType) -> None:
|
||||||
|
""" Put the filenames and original frame dimensions into `batch.feed` so they can be
|
||||||
|
collected for mapping in `.predict`
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
batch: :class:`~plugins.extract.detect._base.AlignerBatch`
|
||||||
|
The batch to be processed by the plugin
|
||||||
|
"""
|
||||||
|
batch.feed = np.array([(self._get_key(os.path.basename(f)), i.shape[:2])
|
||||||
|
for f, i in zip(batch.filename, batch.image)], dtype="object")
|
||||||
|
|
||||||
|
def faces_to_feed(self, faces: np.ndarray) -> np.ndarray:
|
||||||
|
""" No action required for import plugin
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
faces: :class:`numpy.ndarray`
|
||||||
|
The batch of faces in UINT8 format
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
class: `numpy.ndarray`
|
||||||
|
the original batch of faces
|
||||||
|
"""
|
||||||
|
return faces
|
||||||
|
|
||||||
|
def _adjust_for_origin(self, landmarks: np.ndarray, frame_dims: tuple[int, int]) -> np.ndarray:
|
||||||
|
""" Adjust the landmarks to be top-left orientated based on the selected import origin
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
landmarks: :class:`np.ndarray`
|
||||||
|
The imported facial landmarks box at original (0, 0) origin
|
||||||
|
frame_dims: tuple[int, int]
|
||||||
|
The (rows, columns) dimensions of the original frame
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
:class:`numpy.ndarray`
|
||||||
|
The adjusted landmarks box for a top-left origin
|
||||||
|
"""
|
||||||
|
if not np.any(landmarks) or self._origin == "top-left":
|
||||||
|
return landmarks
|
||||||
|
|
||||||
|
if LandmarkType.from_shape(landmarks.shape) == LandmarkType.LM_2D_4:
|
||||||
|
landmarks = np.roll(landmarks, self._roll[self._origin], axis=0)
|
||||||
|
|
||||||
|
if self._origin.startswith("bottom"):
|
||||||
|
landmarks[:, 1] = frame_dims[0] - landmarks[:, 1]
|
||||||
|
if self._origin.endswith("right"):
|
||||||
|
landmarks[:, 0] = frame_dims[1] - landmarks[:, 0]
|
||||||
|
|
||||||
|
return landmarks
|
||||||
|
|
||||||
|
def predict(self, feed: np.ndarray) -> np.ndarray:
|
||||||
|
""" Pair the input filenames to the import file
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
feed: :class:`numpy.ndarray`
|
||||||
|
The filenames in the batch to return imported alignments for
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
:class:`numpy.ndarray`
|
||||||
|
The predictions for the given filenames
|
||||||
|
"""
|
||||||
|
preds = []
|
||||||
|
for key, frame_dims in feed:
|
||||||
|
if key not in self._imported:
|
||||||
|
self._missing.append(key)
|
||||||
|
continue
|
||||||
|
|
||||||
|
remaining, all_lms = self._imported[key]
|
||||||
|
preds.append(self._adjust_for_origin(all_lms[all_lms.shape[0] - remaining],
|
||||||
|
frame_dims))
|
||||||
|
|
||||||
|
if remaining == 1:
|
||||||
|
del self._imported[key]
|
||||||
|
else:
|
||||||
|
self._imported[key] = (remaining - 1, all_lms)
|
||||||
|
|
||||||
|
return np.array(preds, dtype="float32")
|
||||||
|
|
||||||
|
def process_output(self, batch: BatchType) -> None:
|
||||||
|
""" Process the imported data to the landmarks attribute
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
batch: :class:`AlignerBatch`
|
||||||
|
The current batch from the model with :attr:`predictions` populated
|
||||||
|
"""
|
||||||
|
assert isinstance(batch, AlignerBatch)
|
||||||
|
batch.landmarks = batch.prediction
|
||||||
|
logger.trace("Imported landmarks: %s", batch.landmarks) # type:ignore[attr-defined]
|
||||||
|
|
||||||
|
def on_completion(self) -> None:
|
||||||
|
""" Output information if:
|
||||||
|
- Imported items were not matched in input data
|
||||||
|
- Input data was not matched in imported items
|
||||||
|
"""
|
||||||
|
super().on_completion()
|
||||||
|
|
||||||
|
if self._missing:
|
||||||
|
logger.warning("[ALIGN] %s input frames could not be matched in the import file "
|
||||||
|
"'%s'. Run in verbose mode for a list of frames.",
|
||||||
|
len(self._missing), self.config["file_name"])
|
||||||
|
logger.verbose( # type:ignore[attr-defined]
|
||||||
|
"[ALIGN] Input frames not in import file: %s", self._missing)
|
||||||
|
|
||||||
|
if self._imported:
|
||||||
|
logger.warning("[ALIGN] %s items in the import file '%s' could not be matched to any "
|
||||||
|
"input frames. Run in verbose mode for a list of items.",
|
||||||
|
len(self._imported), self.config["file_name"])
|
||||||
|
logger.verbose( # type:ignore[attr-defined]
|
||||||
|
"[ALIGN] import file items not in input frames: %s", list(self._imported))
|
||||||
97
plugins/extract/align/external_defaults.py
Normal file
97
plugins/extract/align/external_defaults.py
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
The default options for the faceswap Import Alignments plugin.
|
||||||
|
|
||||||
|
Defaults files should be named <plugin_name>_defaults.py
|
||||||
|
Any items placed into this file will automatically get added to the relevant config .ini files
|
||||||
|
within the faceswap/config folder.
|
||||||
|
|
||||||
|
The following variables should be defined:
|
||||||
|
_HELPTEXT: A string describing what this plugin does
|
||||||
|
_DEFAULTS: A dictionary containing the options, defaults and meta information. The
|
||||||
|
dictionary should be defined as:
|
||||||
|
{<option_name>: {<metadata>}}
|
||||||
|
|
||||||
|
<option_name> should always be lower text.
|
||||||
|
<metadata> dictionary requirements are listed below.
|
||||||
|
|
||||||
|
The following keys are expected for the _DEFAULTS <metadata> dict:
|
||||||
|
datatype: [required] A python type class. This limits the type of data that can be
|
||||||
|
provided in the .ini file and ensures that the value is returned in the
|
||||||
|
correct type to faceswap. Valid data types are: <class 'int'>, <class 'float'>,
|
||||||
|
<class 'str'>, <class 'bool'>.
|
||||||
|
default: [required] The default value for this option.
|
||||||
|
info: [required] A string describing what this option does.
|
||||||
|
group: [optional]. A group for grouping options together in the GUI. If not
|
||||||
|
provided this will not group this option with any others.
|
||||||
|
choices: [optional] If this option's datatype is of <class 'str'> then valid
|
||||||
|
selections can be defined here. This validates the option and also enables
|
||||||
|
a combobox / radio option in the GUI.
|
||||||
|
gui_radio: [optional] If <choices> are defined, this indicates that the GUI should use
|
||||||
|
radio buttons rather than a combobox to display this option.
|
||||||
|
min_max: [partial] For <class 'int'> and <class 'float'> data types this is required
|
||||||
|
otherwise it is ignored. Should be a tuple of min and max accepted values.
|
||||||
|
This is used for controlling the GUI slider range. Values are not enforced.
|
||||||
|
rounding: [partial] For <class 'int'> and <class 'float'> data types this is
|
||||||
|
required otherwise it is ignored. Used for the GUI slider. For floats, this
|
||||||
|
is the number of decimal places to display. For ints this is the step size.
|
||||||
|
fixed: [optional] [train only]. Training configurations are fixed when the model is
|
||||||
|
created, and then reloaded from the state file. Marking an item as fixed=False
|
||||||
|
indicates that this value can be changed for existing models, and will override
|
||||||
|
the value saved in the state file with the updated value in config. If not
|
||||||
|
provided this will default to True.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
_HELPTEXT = (
|
||||||
|
"Import Aligner options.\n"
|
||||||
|
"Imports either 68 point 2D landmarks or an aligned bounding box from an external .json file."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_DEFAULTS = {
|
||||||
|
"file_name": {
|
||||||
|
"default": "import.json",
|
||||||
|
"info": "The import file should be stored in the same folder as the video (if extracting "
|
||||||
|
"from a video file) or inside the folder of images (if importing from a folder of images)",
|
||||||
|
"datatype": str,
|
||||||
|
"choices": [],
|
||||||
|
"group": "settings",
|
||||||
|
"gui_radio": False,
|
||||||
|
"fixed": True,
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"default": "top-left",
|
||||||
|
"info": "The origin (0, 0) location of the co-ordinates system used. "
|
||||||
|
"\n\t top-left: The origin (0, 0) of the canvas is at the top left "
|
||||||
|
"corner."
|
||||||
|
"\n\t bottom-left: The origin (0, 0) of the canvas is at the bottom "
|
||||||
|
"left corner."
|
||||||
|
"\n\t top-right: The origin (0, 0) of the canvas is at the top right "
|
||||||
|
"corner."
|
||||||
|
"\n\t bottom-right: The origin (0, 0) of the canvas is at the bottom "
|
||||||
|
"right corner.",
|
||||||
|
"datatype": str,
|
||||||
|
"choices": ["top-left", "bottom-left", "top-right", "bottom-right"],
|
||||||
|
"group": "input",
|
||||||
|
"gui_radio": True
|
||||||
|
},
|
||||||
|
"4_point_centering": {
|
||||||
|
"default": "head",
|
||||||
|
"info": "4 point ROI landmarks only. The approximate centering for the location of the "
|
||||||
|
"corner points to be imported. Default faceswap extracts are generated at 'head' "
|
||||||
|
"centering, but it is possible to pass in ROI points at a tighter centering. "
|
||||||
|
"Refer to https://github.com/deepfakes/faceswap/pull/1095 for a visual guide"
|
||||||
|
"\n\t head: The ROI points represent a loose crop enclosing the whole head."
|
||||||
|
"\n\t face: The ROI points represent a medium crop enclosing the face."
|
||||||
|
"\n\t legacy: The ROI points represent a tight crop enclosing the central face "
|
||||||
|
"area."
|
||||||
|
"\n\t none: Only required if importing 4 point ROI landmarks back into faceswap "
|
||||||
|
"having generated them from the 'alignments' tool 'export' job.",
|
||||||
|
"datatype": str,
|
||||||
|
"choices": ["head", "face", "legacy", "none"],
|
||||||
|
"group": "input",
|
||||||
|
"gui_radio": True
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
All Detector Plugins should inherit from this class.
|
All Detector Plugins should inherit from this class.
|
||||||
See the override methods for which methods are required.
|
See the override methods for which methods are required.
|
||||||
|
|
||||||
The plugin will receive a :class:`~plugins.extract.pipeline.ExtractMedia` object.
|
The plugin will receive a :class:`~plugins.extract.extract_media.ExtractMedia` object.
|
||||||
|
|
||||||
For each source frame, the plugin must pass a dict to finalize containing:
|
For each source frame, the plugin must pass a dict to finalize containing:
|
||||||
|
|
||||||
|
|
@ -30,7 +30,7 @@ from lib.align import DetectedFace
|
||||||
from lib.utils import FaceswapError
|
from lib.utils import FaceswapError
|
||||||
|
|
||||||
from plugins.extract._base import BatchType, Extractor, ExtractorBatch
|
from plugins.extract._base import BatchType, Extractor, ExtractorBatch
|
||||||
from plugins.extract.pipeline import ExtractMedia
|
from plugins.extract import ExtractMedia
|
||||||
|
|
||||||
if T.TYPE_CHECKING:
|
if T.TYPE_CHECKING:
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
|
|
@ -62,6 +62,15 @@ class DetectorBatch(ExtractorBatch):
|
||||||
pad: list[tuple[int, int]] = field(default_factory=list)
|
pad: list[tuple[int, int]] = field(default_factory=list)
|
||||||
initial_feed: np.ndarray = np.array([])
|
initial_feed: np.ndarray = np.array([])
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
""" Prettier repr for debug printing """
|
||||||
|
retval = super().__repr__()
|
||||||
|
retval += (f", rotation_matrix={self.rotation_matrix}, "
|
||||||
|
f"scale={self.scale}, "
|
||||||
|
f"pad={self.pad}, "
|
||||||
|
f"initial_feed=({self.initial_feed.shape}, {self.initial_feed.dtype})")
|
||||||
|
return retval
|
||||||
|
|
||||||
|
|
||||||
class Detector(Extractor): # pylint:disable=abstract-method
|
class Detector(Extractor): # pylint:disable=abstract-method
|
||||||
""" Detector Object
|
""" Detector Object
|
||||||
|
|
@ -123,8 +132,8 @@ class Detector(Extractor): # pylint:disable=abstract-method
|
||||||
def get_batch(self, queue: Queue) -> tuple[bool, DetectorBatch]:
|
def get_batch(self, queue: Queue) -> tuple[bool, DetectorBatch]:
|
||||||
""" Get items for inputting to the detector plugin in batches
|
""" Get items for inputting to the detector plugin in batches
|
||||||
|
|
||||||
Items are received as :class:`~plugins.extract.pipeline.ExtractMedia` objects and converted
|
Items are received as :class:`~plugins.extract.extract_media.ExtractMedia` objects and
|
||||||
to ``dict`` for internal processing.
|
converted to ``dict`` for internal processing.
|
||||||
|
|
||||||
Items are returned from the ``queue`` in batches of
|
Items are returned from the ``queue`` in batches of
|
||||||
:attr:`~plugins.extract._base.Extractor.batchsize`
|
:attr:`~plugins.extract._base.Extractor.batchsize`
|
||||||
|
|
@ -199,7 +208,7 @@ class Detector(Extractor): # pylint:disable=abstract-method
|
||||||
|
|
||||||
Yields
|
Yields
|
||||||
------
|
------
|
||||||
:class:`~plugins.extract.pipeline.ExtractMedia`
|
:class:`~plugins.extract.extract_media.ExtractMedia`
|
||||||
The :attr:`DetectedFaces` list will be populated for this class with the bounding boxes
|
The :attr:`DetectedFaces` list will be populated for this class with the bounding boxes
|
||||||
for the detected faces found in the frame.
|
for the detected faces found in the frame.
|
||||||
"""
|
"""
|
||||||
|
|
@ -342,7 +351,7 @@ class Detector(Extractor): # pylint:disable=abstract-method
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
item: :class:`plugins.extract.pipeline.ExtractMedia`
|
item: :class:`~plugins.extract.extract_media.ExtractMedia`
|
||||||
The input item from the pipeline
|
The input item from the pipeline
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
|
|
|
||||||
353
plugins/extract/detect/external.py
Normal file
353
plugins/extract/detect/external.py
Normal file
|
|
@ -0,0 +1,353 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
""" Import face detection ROI boxes from a json file """
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import typing as T
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from lib.align import AlignedFace
|
||||||
|
from lib.utils import FaceswapError, IMAGE_EXTENSIONS
|
||||||
|
|
||||||
|
from ._base import Detector
|
||||||
|
|
||||||
|
if T.TYPE_CHECKING:
|
||||||
|
from lib.align import DetectedFace
|
||||||
|
from plugins.extract import ExtractMedia
|
||||||
|
from ._base import BatchType
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Detect(Detector):
|
||||||
|
""" Import face detection bounding boxes from an external json file """
|
||||||
|
def __init__(self, **kwargs) -> None:
|
||||||
|
kwargs["rotation"] = None # Disable rotation
|
||||||
|
kwargs["min_size"] = 0 # Disable min_size
|
||||||
|
super().__init__(git_model_id=None, model_filename=None, **kwargs)
|
||||||
|
|
||||||
|
self.name = "External"
|
||||||
|
self.batchsize = 16
|
||||||
|
|
||||||
|
self._origin: T.Literal["top-left",
|
||||||
|
"bottom-left",
|
||||||
|
"top-right",
|
||||||
|
"bottom-right"] = self.config["origin"]
|
||||||
|
|
||||||
|
self._re_frame_no: re.Pattern = re.compile(r"\d+$")
|
||||||
|
self._missing: list[str] = []
|
||||||
|
self._log_once = True
|
||||||
|
self._is_video = False
|
||||||
|
self._imported: dict[str | int, np.ndarray] = {}
|
||||||
|
"""dict[str | int, np.ndarray]: The imported data from external .json file"""
|
||||||
|
|
||||||
|
def init_model(self) -> None:
|
||||||
|
""" No initialization to perform """
|
||||||
|
logger.debug("No detector model to initialize")
|
||||||
|
|
||||||
|
def _compile_detection_image(self, item: ExtractMedia
|
||||||
|
) -> tuple[np.ndarray, float, tuple[int, int]]:
|
||||||
|
""" Override _compile_detection_image method, to obtain the source frame dimensions
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
item: :class:`~plugins.extract.extract_media.ExtractMedia`
|
||||||
|
The input item from the pipeline
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
image: :class:`numpy.ndarray`
|
||||||
|
dummy empty array
|
||||||
|
scale: float
|
||||||
|
The scaling factor for the image (1.0)
|
||||||
|
pad: int
|
||||||
|
The amount of padding applied to the image (0, 0)
|
||||||
|
"""
|
||||||
|
return np.array(item.image_shape[:2], dtype="int64"), 1.0, (0, 0)
|
||||||
|
|
||||||
|
def _check_for_video(self, filename: str) -> None:
|
||||||
|
""" Check a sample filename from the import file for a file extension to set
|
||||||
|
:attr:`_is_video`
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
filename: str
|
||||||
|
A sample file name from the imported data
|
||||||
|
"""
|
||||||
|
logger.debug("Checking for video from '%s'", filename)
|
||||||
|
ext = os.path.splitext(filename)[-1]
|
||||||
|
if ext.lower() not in IMAGE_EXTENSIONS:
|
||||||
|
self._is_video = True
|
||||||
|
logger.debug("Set is_video to %s from extension '%s'", self._is_video, ext)
|
||||||
|
|
||||||
|
def _get_key(self, key: str) -> str | int:
|
||||||
|
""" Obtain the key for the item in the lookup table. If the input are images, the key will
|
||||||
|
be the image filename. If the input is a video, the key will be the frame number
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
key: str
|
||||||
|
The initial key value from import data or an import image/frame
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str | int
|
||||||
|
The filename is the input data is images, otherwise the frame number of a video
|
||||||
|
"""
|
||||||
|
if not self._is_video:
|
||||||
|
return key
|
||||||
|
original_name = os.path.splitext(key)[0]
|
||||||
|
matches = self._re_frame_no.findall(original_name)
|
||||||
|
if not matches or len(matches) > 1:
|
||||||
|
raise FaceswapError(f"Invalid import name: '{key}'. For video files, the key should "
|
||||||
|
"end with the frame number.")
|
||||||
|
retval = int(matches[0])
|
||||||
|
logger.trace("Obtained frame number %s from key '%s'", # type:ignore[attr-defined]
|
||||||
|
retval, key)
|
||||||
|
return retval
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _bbox_from_detected(cls, bounding_box: list[int]) -> np.ndarray:
|
||||||
|
""" Import the detected face roi from a `detected` item in the import file
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
bounding_box: list[int]
|
||||||
|
a bounding box contained within the import file
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
:class:`numpy.ndarray`
|
||||||
|
The "left", "top", "right", "bottom" bounding box for the face
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
FaceSwapError
|
||||||
|
If the number of bounding box co-ordinates is incorrect
|
||||||
|
"""
|
||||||
|
if len(bounding_box) != 4:
|
||||||
|
raise FaceswapError("Imported 'detected' bounding boxes should be a list of 4 numbers "
|
||||||
|
"representing the 'left', 'top', 'right', `bottom` of a face.")
|
||||||
|
return np.rint(bounding_box)
|
||||||
|
|
||||||
|
def _validate_landmarks(self, landmarks: list[list[float]]) -> np.ndarray:
|
||||||
|
""" Validate that the there are 4 or 68 landmarks and are a complete list of (x, y)
|
||||||
|
co-ordinates
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
landmarks: list[float]
|
||||||
|
The 4 point ROI or 68 point 2D landmarks that are being imported
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
:class:`numpy.ndarray`
|
||||||
|
The original landmarks as a numpy array
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
FaceSwapError
|
||||||
|
If the landmarks being imported are not correct
|
||||||
|
"""
|
||||||
|
if len(landmarks) not in (4, 68):
|
||||||
|
raise FaceswapError("Imported 'landmarks_2d' should be either 68 facial feature "
|
||||||
|
"landmarks or 4 ROI corner locations")
|
||||||
|
retval = np.array(landmarks, dtype="float32")
|
||||||
|
if retval.shape[-1] != 2:
|
||||||
|
raise FaceswapError("Imported 'landmarks_2d' should be formatted as a list of (x, y) "
|
||||||
|
"co-ordinates")
|
||||||
|
return retval
|
||||||
|
|
||||||
|
def _bbox_from_landmarks2d(self, landmarks: list[list[float]]) -> np.ndarray:
|
||||||
|
""" Import the detected face roi by estimating from imported landmarks
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
landmarks: list[float]
|
||||||
|
The 4 point ROI or 68 point 2D landmarks that are being imported
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
:class:`numpy.ndarray`
|
||||||
|
The "left", "top", "right", "bottom" bounding box for the face
|
||||||
|
"""
|
||||||
|
n_landmarks = self._validate_landmarks(landmarks)
|
||||||
|
face = AlignedFace(n_landmarks, centering="legacy", coverage_ratio=0.75)
|
||||||
|
return np.concatenate([np.min(face.original_roi, axis=0),
|
||||||
|
np.max(face.original_roi, axis=0)])
|
||||||
|
|
||||||
|
def _import_frame_face(self,
|
||||||
|
face: dict[str, list[int] | list[list[float]]],
|
||||||
|
align_origin: T.Literal["top-left",
|
||||||
|
"bottom-left",
|
||||||
|
"top-right",
|
||||||
|
"bottom-right"] | None) -> np.ndarray:
|
||||||
|
""" Import a detected face ROI from the import file
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
face: dict[str, list[int] | list[list[float]]]
|
||||||
|
The data that exists within the import file for the frame
|
||||||
|
align_origin: Literal["top-left", "bottom-left", "top-right", "bottom-right"] | None
|
||||||
|
The origin of the imported aligner data. Used if the detected ROI is being estimated
|
||||||
|
from imported aligner data
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
:class:`numpy.ndarray`
|
||||||
|
The "left", "top", "right", "bottom" bounding box for the face
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
FaceSwapError
|
||||||
|
If the required keys for the bounding boxes are not present for the face
|
||||||
|
"""
|
||||||
|
if "detected" in face:
|
||||||
|
return self._bbox_from_detected(T.cast(list[int], face["detected"]))
|
||||||
|
if "landmarks_2d" in face:
|
||||||
|
if self._log_once and align_origin is None:
|
||||||
|
logger.warning("You are importing Detection data, but have only provided "
|
||||||
|
"Alignment data. This is most likely incorrect and will lead "
|
||||||
|
"to poor results")
|
||||||
|
self._log_once = False
|
||||||
|
|
||||||
|
if self._log_once and align_origin is not None and align_origin != self._origin:
|
||||||
|
logger.info("Updating Detect origin from Aligner config to '%s'", align_origin)
|
||||||
|
self._origin = align_origin
|
||||||
|
self._log_once = False
|
||||||
|
|
||||||
|
return self._bbox_from_landmarks2d(T.cast(list[list[float]], face["landmarks_2d"]))
|
||||||
|
|
||||||
|
raise FaceswapError("The provided import file is missing both of the required keys "
|
||||||
|
"'detected' and 'landmarks_2d")
|
||||||
|
|
||||||
|
def import_data(self,
|
||||||
|
data: dict[str, list[dict[str, list[int] | list[list[float]]]]],
|
||||||
|
align_origin: T.Literal["top-left",
|
||||||
|
"bottom-left",
|
||||||
|
"top-right",
|
||||||
|
"bottom-right"] | None) -> None:
|
||||||
|
""" Import the detection data from the json import file and set to :attr:`_imported`
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
data: dict[str, list[dict[str, list[int] | list[list[float]]]]]
|
||||||
|
The data to be imported
|
||||||
|
align_origin: Literal["top-left", "bottom-left", "top-right", "bottom-right"] | None
|
||||||
|
The origin of the imported aligner data. Used if the detected ROI is being estimated
|
||||||
|
from imported aligner data
|
||||||
|
"""
|
||||||
|
logger.debug("Data length: %s, align_origin: %s", len(data), align_origin)
|
||||||
|
self._check_for_video(list(data)[0])
|
||||||
|
for key, faces in data.items():
|
||||||
|
try:
|
||||||
|
store_key = self._get_key(key)
|
||||||
|
self._imported[store_key] = np.array([self._import_frame_face(face, align_origin)
|
||||||
|
for face in faces], dtype="int32")
|
||||||
|
except FaceswapError as err:
|
||||||
|
logger.error(str(err))
|
||||||
|
msg = f"The imported frame key that failed was '{key}'"
|
||||||
|
raise FaceswapError(msg) from err
|
||||||
|
|
||||||
|
def process_input(self, batch: BatchType) -> None:
|
||||||
|
""" Put the lookup key into `batch.feed` so they can be collected for mapping in `.predict`
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
batch: :class:`~plugins.extract.detect._base.DetectorBatch`
|
||||||
|
The batch to be processed by the plugin
|
||||||
|
"""
|
||||||
|
batch.feed = np.array([(self._get_key(os.path.basename(f)), i)
|
||||||
|
for f, i in zip(batch.filename, batch.image)], dtype="object")
|
||||||
|
|
||||||
|
def _adjust_for_origin(self, box: np.ndarray, frame_dims: tuple[int, int]) -> np.ndarray:
|
||||||
|
""" Adjust the bounding box to be top-left orientated based on the selected import origin
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
box: :class:`np.ndarray`
|
||||||
|
The imported bounding box at original (0, 0) origin
|
||||||
|
frame_dims: tuple[int, int]
|
||||||
|
The (rows, columns) dimensions of the original frame
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
:class:`numpy.ndarray`
|
||||||
|
The adjusted bounding box for a top-left origin
|
||||||
|
"""
|
||||||
|
if not np.any(box) or self._origin == "top-left":
|
||||||
|
return box
|
||||||
|
if self._origin.startswith("bottom"):
|
||||||
|
box[:, [1, 3]] = frame_dims[0] - box[:, [1, 3]]
|
||||||
|
if self._origin.endswith("right"):
|
||||||
|
box[:, [0, 2]] = frame_dims[1] - box[:, [0, 2]]
|
||||||
|
|
||||||
|
return box
|
||||||
|
|
||||||
|
def predict(self, feed: np.ndarray) -> list[np.ndarray]: # type:ignore[override]
|
||||||
|
""" Pair the input filenames to the import file
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
feed: :class:`numpy.ndarray`
|
||||||
|
The filenames with original frame dimensions to obtain the imported bounding boxes for
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
list[]:class:`numpy.ndarray`]
|
||||||
|
The bounding boxes for the given filenames
|
||||||
|
"""
|
||||||
|
self._missing.extend(f[0] for f in feed if f[0] not in self._imported)
|
||||||
|
return [self._adjust_for_origin(self._imported.pop(f[0], np.array([], dtype="int32")),
|
||||||
|
f[1])
|
||||||
|
for f in feed]
|
||||||
|
|
||||||
|
def process_output(self, batch: BatchType) -> None:
|
||||||
|
""" No output processing required for import plugin
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
batch: :class:`~plugins.extract.detect._base.DetectorBatch`
|
||||||
|
The batch to be processed by the plugin
|
||||||
|
"""
|
||||||
|
logger.trace("No output processing for import plugin") # type:ignore[attr-defined]
|
||||||
|
|
||||||
|
def _remove_zero_sized_faces(self, batch_faces: list[list[DetectedFace]]
|
||||||
|
) -> list[list[DetectedFace]]:
|
||||||
|
""" Override _remove_zero_sized_faces to just return the faces that have been imported
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
batch_faces: list[list[DetectedFace]
|
||||||
|
List of detected face objects
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
list[list[DetectedFace]
|
||||||
|
Original list of detected face objects
|
||||||
|
"""
|
||||||
|
return batch_faces
|
||||||
|
|
||||||
|
def on_completion(self) -> None:
|
||||||
|
""" Output information if:
|
||||||
|
- Imported items were not matched in input data
|
||||||
|
- Input data was not matched in imported items
|
||||||
|
"""
|
||||||
|
super().on_completion()
|
||||||
|
|
||||||
|
if self._missing:
|
||||||
|
logger.warning("[DETECT] %s input frames could not be matched in the import file "
|
||||||
|
"'%s'. Run in verbose mode for a list of frames.",
|
||||||
|
len(self._missing), self.config["file_name"])
|
||||||
|
logger.verbose( # type:ignore[attr-defined]
|
||||||
|
"[DETECT] Input frames not in import file: %s", self._missing)
|
||||||
|
|
||||||
|
if self._imported:
|
||||||
|
logger.warning("[DETECT] %s items in the import file '%s' could not be matched to any "
|
||||||
|
"input frames. Run in verbose mode for a list of items.",
|
||||||
|
len(self._imported), self.config["file_name"])
|
||||||
|
logger.verbose( # type:ignore[attr-defined]
|
||||||
|
"[DETECT] import file items not in input frames: %s", list(self._imported))
|
||||||
79
plugins/extract/detect/external_defaults.py
Normal file
79
plugins/extract/detect/external_defaults.py
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
The default options for the faceswap Import Alignments plugin.
|
||||||
|
|
||||||
|
Defaults files should be named <plugin_name>_defaults.py
|
||||||
|
Any items placed into this file will automatically get added to the relevant config .ini files
|
||||||
|
within the faceswap/config folder.
|
||||||
|
|
||||||
|
The following variables should be defined:
|
||||||
|
_HELPTEXT: A string describing what this plugin does
|
||||||
|
_DEFAULTS: A dictionary containing the options, defaults and meta information. The
|
||||||
|
dictionary should be defined as:
|
||||||
|
{<option_name>: {<metadata>}}
|
||||||
|
|
||||||
|
<option_name> should always be lower text.
|
||||||
|
<metadata> dictionary requirements are listed below.
|
||||||
|
|
||||||
|
The following keys are expected for the _DEFAULTS <metadata> dict:
|
||||||
|
datatype: [required] A python type class. This limits the type of data that can be
|
||||||
|
provided in the .ini file and ensures that the value is returned in the
|
||||||
|
correct type to faceswap. Valid data types are: <class 'int'>, <class 'float'>,
|
||||||
|
<class 'str'>, <class 'bool'>.
|
||||||
|
default: [required] The default value for this option.
|
||||||
|
info: [required] A string describing what this option does.
|
||||||
|
group: [optional]. A group for grouping options together in the GUI. If not
|
||||||
|
provided this will not group this option with any others.
|
||||||
|
choices: [optional] If this option's datatype is of <class 'str'> then valid
|
||||||
|
selections can be defined here. This validates the option and also enables
|
||||||
|
a combobox / radio option in the GUI.
|
||||||
|
gui_radio: [optional] If <choices> are defined, this indicates that the GUI should use
|
||||||
|
radio buttons rather than a combobox to display this option.
|
||||||
|
min_max: [partial] For <class 'int'> and <class 'float'> data types this is required
|
||||||
|
otherwise it is ignored. Should be a tuple of min and max accepted values.
|
||||||
|
This is used for controlling the GUI slider range. Values are not enforced.
|
||||||
|
rounding: [partial] For <class 'int'> and <class 'float'> data types this is
|
||||||
|
required otherwise it is ignored. Used for the GUI slider. For floats, this
|
||||||
|
is the number of decimal places to display. For ints this is the step size.
|
||||||
|
fixed: [optional] [train only]. Training configurations are fixed when the model is
|
||||||
|
created, and then reloaded from the state file. Marking an item as fixed=False
|
||||||
|
indicates that this value can be changed for existing models, and will override
|
||||||
|
the value saved in the state file with the updated value in config. If not
|
||||||
|
provided this will default to True.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
_HELPTEXT = (
|
||||||
|
"Import Detector options.\n"
|
||||||
|
"Imports a detected face bounding box from an external .json file.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_DEFAULTS = {
|
||||||
|
"file_name": {
|
||||||
|
"default": "import.json",
|
||||||
|
"info": "The import file should be stored in the same folder as the video (if extracting "
|
||||||
|
"from a video file) or inside the folder of images (if importing from a folder of images)",
|
||||||
|
"datatype": str,
|
||||||
|
"choices": [],
|
||||||
|
"group": "settings",
|
||||||
|
"gui_radio": False,
|
||||||
|
"fixed": True,
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"default": "top-left",
|
||||||
|
"info": "The origin (0, 0) location of the co-ordinates system used. "
|
||||||
|
"\n\t top-left: The origin (0, 0) of the canvas is at the top left "
|
||||||
|
"corner."
|
||||||
|
"\n\t bottom-left: The origin (0, 0) of the canvas is at the bottom "
|
||||||
|
"left corner."
|
||||||
|
"\n\t top-right: The origin (0, 0) of the canvas is at the top right "
|
||||||
|
"corner."
|
||||||
|
"\n\t bottom-right: The origin (0, 0) of the canvas is at the bottom "
|
||||||
|
"right corner.",
|
||||||
|
"datatype": str,
|
||||||
|
"choices": ["top-left", "bottom-left", "top-right", "bottom-right"],
|
||||||
|
"group": "output",
|
||||||
|
"gui_radio": True
|
||||||
|
}
|
||||||
|
}
|
||||||
210
plugins/extract/extract_media.py
Normal file
210
plugins/extract/extract_media.py
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
""" Object for holding and manipulating media passing through a faceswap extraction pipeline """
|
||||||
|
from __future__ import annotations
|
||||||
|
import logging
|
||||||
|
import typing as T
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
|
||||||
|
from lib.logger import parse_class_init
|
||||||
|
|
||||||
|
if T.TYPE_CHECKING:
|
||||||
|
import numpy as np
|
||||||
|
from lib.align.alignments import PNGHeaderSourceDict
|
||||||
|
from lib.align.detected_face import DetectedFace
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ExtractMedia:
|
||||||
|
""" An object that passes through the :class:`~plugins.extract.pipeline.Extractor` pipeline.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
filename: str
|
||||||
|
The base name of the original frame's filename
|
||||||
|
image: :class:`numpy.ndarray`
|
||||||
|
The original frame or a faceswap aligned face image
|
||||||
|
detected_faces: list, optional
|
||||||
|
A list of :class:`~lib.align.DetectedFace` objects. Detected faces can be added
|
||||||
|
later with :func:`add_detected_faces`. Setting ``None`` will default to an empty list.
|
||||||
|
Default: ``None``
|
||||||
|
is_aligned: bool, optional
|
||||||
|
``True`` if the :attr:`image` is an aligned faceswap image otherwise ``False``. Used for
|
||||||
|
face filtering with vggface2. Aligned faceswap images will automatically skip detection,
|
||||||
|
alignment and masking. Default: ``False``
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
filename: str,
|
||||||
|
image: np.ndarray,
|
||||||
|
detected_faces: list[DetectedFace] | None = None,
|
||||||
|
is_aligned: bool = False) -> None:
|
||||||
|
logger.trace(parse_class_init(locals())) # type:ignore[attr-defined]
|
||||||
|
self._filename = filename
|
||||||
|
self._image: np.ndarray | None = image
|
||||||
|
self._image_shape = T.cast(tuple[int, int, int], image.shape)
|
||||||
|
self._detected_faces: list[DetectedFace] = ([] if detected_faces is None
|
||||||
|
else detected_faces)
|
||||||
|
self._is_aligned = is_aligned
|
||||||
|
self._frame_metadata: PNGHeaderSourceDict | None = None
|
||||||
|
self._sub_folders: list[str | None] = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filename(self) -> str:
|
||||||
|
""" str: The base name of the :attr:`image` filename. """
|
||||||
|
return self._filename
|
||||||
|
|
||||||
|
@property
|
||||||
|
def image(self) -> np.ndarray:
|
||||||
|
""" :class:`numpy.ndarray`: The source frame for this object. """
|
||||||
|
assert self._image is not None
|
||||||
|
return self._image
|
||||||
|
|
||||||
|
@property
|
||||||
|
def image_shape(self) -> tuple[int, int, int]:
|
||||||
|
""" tuple: The shape of the stored :attr:`image`. """
|
||||||
|
return self._image_shape
|
||||||
|
|
||||||
|
@property
|
||||||
|
def image_size(self) -> tuple[int, int]:
|
||||||
|
""" tuple: The (`height`, `width`) of the stored :attr:`image`. """
|
||||||
|
return self._image_shape[:2]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def detected_faces(self) -> list[DetectedFace]:
|
||||||
|
"""list: A list of :class:`~lib.align.DetectedFace` objects in the :attr:`image`. """
|
||||||
|
return self._detected_faces
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_aligned(self) -> bool:
|
||||||
|
""" bool. ``True`` if :attr:`image` is an aligned faceswap image otherwise ``False`` """
|
||||||
|
return self._is_aligned
|
||||||
|
|
||||||
|
@property
|
||||||
|
def frame_metadata(self) -> PNGHeaderSourceDict:
|
||||||
|
""" dict: The frame metadata that has been added from an aligned image. This property
|
||||||
|
should only be called after :func:`add_frame_metadata` has been called when processing
|
||||||
|
an aligned face. For all other instances an assertion error will be raised.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
AssertionError
|
||||||
|
If frame metadata has not been populated from an aligned image
|
||||||
|
"""
|
||||||
|
assert self._frame_metadata is not None
|
||||||
|
return self._frame_metadata
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sub_folders(self) -> list[str | None]:
|
||||||
|
""" list: The sub_folders that the faces should be output to. Used when binning filter
|
||||||
|
output is enabled. The list corresponds to the list of detected faces
|
||||||
|
"""
|
||||||
|
return self._sub_folders
|
||||||
|
|
||||||
|
def get_image_copy(self, color_format: T.Literal["BGR", "RGB", "GRAY"]) -> np.ndarray:
|
||||||
|
""" Get a copy of the image in the requested color format.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
color_format: ['BGR', 'RGB', 'GRAY']
|
||||||
|
The requested color format of :attr:`image`
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
:class:`numpy.ndarray`:
|
||||||
|
A copy of :attr:`image` in the requested :attr:`color_format`
|
||||||
|
"""
|
||||||
|
logger.trace("Requested color format '%s' for frame '%s'", # type:ignore[attr-defined]
|
||||||
|
color_format, self._filename)
|
||||||
|
image = getattr(self, f"_image_as_{color_format.lower()}")()
|
||||||
|
return image
|
||||||
|
|
||||||
|
def add_detected_faces(self, faces: list[DetectedFace]) -> None:
|
||||||
|
""" Add detected faces to the object. Called at the end of each extraction phase.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
faces: list
|
||||||
|
A list of :class:`~lib.align.DetectedFace` objects
|
||||||
|
"""
|
||||||
|
logger.trace("Adding detected faces for filename: '%s'. " # type:ignore[attr-defined]
|
||||||
|
"(faces: %s, lrtb: %s)", self._filename, faces,
|
||||||
|
[(face.left, face.right, face.top, face.bottom) for face in faces])
|
||||||
|
self._detected_faces = faces
|
||||||
|
|
||||||
|
def add_sub_folders(self, folders: list[str | None]) -> None:
|
||||||
|
""" Add detected faces to the object. Called at the end of each extraction phase.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
folders: list
|
||||||
|
A list of str sub folder names or ``None`` if no sub folder is required. Should
|
||||||
|
correspond to the detected faces list
|
||||||
|
"""
|
||||||
|
logger.trace("Adding sub folders for filename: '%s'. " # type:ignore[attr-defined]
|
||||||
|
"(folders: %s)", self._filename, folders,)
|
||||||
|
self._sub_folders = folders
|
||||||
|
|
||||||
|
def remove_image(self) -> None:
|
||||||
|
""" Delete the image and reset :attr:`image` to ``None``.
|
||||||
|
|
||||||
|
Required for multi-phase extraction to avoid the frames stacking RAM.
|
||||||
|
"""
|
||||||
|
logger.trace("Removing image for filename: '%s'", # type:ignore[attr-defined]
|
||||||
|
self._filename)
|
||||||
|
del self._image
|
||||||
|
self._image = None
|
||||||
|
|
||||||
|
def set_image(self, image: np.ndarray) -> None:
|
||||||
|
""" Add the image back into :attr:`image`
|
||||||
|
|
||||||
|
Required for multi-phase extraction adds the image back to this object.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
image: :class:`numpy.ndarry`
|
||||||
|
The original frame to be re-applied to for this :attr:`filename`
|
||||||
|
"""
|
||||||
|
logger.trace("Reapplying image: (filename: `%s`, " # type:ignore[attr-defined]
|
||||||
|
"image shape: %s)", self._filename, image.shape)
|
||||||
|
self._image = image
|
||||||
|
|
||||||
|
def add_frame_metadata(self, metadata: PNGHeaderSourceDict) -> None:
|
||||||
|
""" Add the source frame metadata from an aligned PNG's header data.
|
||||||
|
|
||||||
|
metadata: dict
|
||||||
|
The contents of the 'source' field in the PNG header
|
||||||
|
"""
|
||||||
|
logger.trace("Adding PNG Source data for '%s': %s", # type:ignore[attr-defined]
|
||||||
|
self._filename, metadata)
|
||||||
|
dims = T.cast(tuple[int, int], metadata["source_frame_dims"])
|
||||||
|
self._image_shape = (*dims, 3)
|
||||||
|
self._frame_metadata = metadata
|
||||||
|
|
||||||
|
def _image_as_bgr(self) -> np.ndarray:
|
||||||
|
""" Get a copy of the source frame in BGR format.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
:class:`numpy.ndarray`:
|
||||||
|
A copy of :attr:`image` in BGR color format """
|
||||||
|
return self.image[..., :3].copy()
|
||||||
|
|
||||||
|
def _image_as_rgb(self) -> np.ndarray:
|
||||||
|
""" Get a copy of the source frame in RGB format.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
:class:`numpy.ndarray`:
|
||||||
|
A copy of :attr:`image` in RGB color format """
|
||||||
|
return self.image[..., 2::-1].copy()
|
||||||
|
|
||||||
|
def _image_as_gray(self) -> np.ndarray:
|
||||||
|
""" Get a copy of the source frame in gray-scale format.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
:class:`numpy.ndarray`:
|
||||||
|
A copy of :attr:`image` in gray-scale color format """
|
||||||
|
return cv2.cvtColor(self.image.copy(), cv2.COLOR_BGR2GRAY)
|
||||||
|
|
@ -5,7 +5,7 @@ Plugins should inherit from this class
|
||||||
|
|
||||||
See the override methods for which methods are required.
|
See the override methods for which methods are required.
|
||||||
|
|
||||||
The plugin will receive a :class:`~plugins.extract.pipeline.ExtractMedia` object.
|
The plugin will receive a :class:`~plugins.extract.extract_media.ExtractMedia` object.
|
||||||
|
|
||||||
For each source item, the plugin must pass a dict to finalize containing:
|
For each source item, the plugin must pass a dict to finalize containing:
|
||||||
|
|
||||||
|
|
@ -23,9 +23,10 @@ import numpy as np
|
||||||
|
|
||||||
from tensorflow.python.framework import errors_impl as tf_errors # pylint:disable=no-name-in-module # noqa
|
from tensorflow.python.framework import errors_impl as tf_errors # pylint:disable=no-name-in-module # noqa
|
||||||
|
|
||||||
from lib.align import AlignedFace, transform_image
|
from lib.align import AlignedFace, LandmarkType, transform_image
|
||||||
from lib.utils import FaceswapError
|
from lib.utils import FaceswapError
|
||||||
from plugins.extract._base import BatchType, Extractor, ExtractorBatch, ExtractMedia
|
from plugins.extract import ExtractMedia
|
||||||
|
from plugins.extract._base import BatchType, ExtractorBatch, Extractor
|
||||||
|
|
||||||
if T.TYPE_CHECKING:
|
if T.TYPE_CHECKING:
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
|
|
@ -79,6 +80,8 @@ class Masker(Extractor): # pylint:disable=abstract-method
|
||||||
plugins.extract.align._base : Aligner parent class for extraction plugins.
|
plugins.extract.align._base : Aligner parent class for extraction plugins.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_logged_lm_count_once = False
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
git_model_id: int | None = None,
|
git_model_id: int | None = None,
|
||||||
model_filename: str | None = None,
|
model_filename: str | None = None,
|
||||||
|
|
@ -94,20 +97,41 @@ class Masker(Extractor): # pylint:disable=abstract-method
|
||||||
self.input_size = 256 # Override for model specific input_size
|
self.input_size = 256 # Override for model specific input_size
|
||||||
self.coverage_ratio = 1.0 # Override for model specific coverage_ratio
|
self.coverage_ratio = 1.0 # Override for model specific coverage_ratio
|
||||||
|
|
||||||
|
# Override if a specific type of landmark data is required:
|
||||||
|
self.landmark_type: LandmarkType | None = None
|
||||||
|
|
||||||
self._plugin_type = "mask"
|
self._plugin_type = "mask"
|
||||||
self._storage_name = self.__module__.rsplit(".", maxsplit=1)[-1].replace("_", "-")
|
self._storage_name = self.__module__.rsplit(".", maxsplit=1)[-1].replace("_", "-")
|
||||||
self._storage_centering: CenteringType = "face" # Centering to store the mask at
|
self._storage_centering: CenteringType = "face" # Centering to store the mask at
|
||||||
self._storage_size = 128 # Size to store masks at. Leave this at default
|
self._storage_size = 128 # Size to store masks at. Leave this at default
|
||||||
logger.debug("Initialized %s", self.__class__.__name__)
|
logger.debug("Initialized %s", self.__class__.__name__)
|
||||||
|
|
||||||
|
def _maybe_log_warning(self, face: AlignedFace) -> None:
|
||||||
|
""" Log a warning, once, if we do not have full facial landmarks
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
face: :class:`~lib.align.aligned_face.AlignedFace`
|
||||||
|
The aligned face object to test the landmark type for
|
||||||
|
"""
|
||||||
|
if face.landmark_type != LandmarkType.LM_2D_4 or self._logged_lm_count_once:
|
||||||
|
return
|
||||||
|
|
||||||
|
msg = "are likely to be sub-standard"
|
||||||
|
msg = "can not be be generated" if self.name in ("Components", "Extended") else msg
|
||||||
|
|
||||||
|
logger.warning("Extracted faces do not contain facial landmark data. '%s' masks %s.",
|
||||||
|
self.name, msg)
|
||||||
|
self._logged_lm_count_once = True
|
||||||
|
|
||||||
def get_batch(self, queue: Queue) -> tuple[bool, MaskerBatch]:
|
def get_batch(self, queue: Queue) -> tuple[bool, MaskerBatch]:
|
||||||
""" Get items for inputting into the masker from the queue in batches
|
""" Get items for inputting into the masker from the queue in batches
|
||||||
|
|
||||||
Items are returned from the ``queue`` in batches of
|
Items are returned from the ``queue`` in batches of
|
||||||
:attr:`~plugins.extract._base.Extractor.batchsize`
|
:attr:`~plugins.extract._base.Extractor.batchsize`
|
||||||
|
|
||||||
Items are received as :class:`~plugins.extract.pipeline.ExtractMedia` objects and converted
|
Items are received as :class:`~plugins.extract.extract_media.ExtractMedia` objects and
|
||||||
to ``dict`` for internal processing.
|
converted to ``dict`` for internal processing.
|
||||||
|
|
||||||
To ensure consistent batch sizes for masker the items are split into separate items for
|
To ensure consistent batch sizes for masker the items are split into separate items for
|
||||||
each :class:`~lib.align.DetectedFace` object.
|
each :class:`~lib.align.DetectedFace` object.
|
||||||
|
|
@ -163,6 +187,8 @@ class Masker(Extractor): # pylint:disable=abstract-method
|
||||||
dtype="float32",
|
dtype="float32",
|
||||||
is_aligned=item.is_aligned)
|
is_aligned=item.is_aligned)
|
||||||
|
|
||||||
|
self._maybe_log_warning(feed_face)
|
||||||
|
|
||||||
assert feed_face.face is not None
|
assert feed_face.face is not None
|
||||||
if not item.is_aligned:
|
if not item.is_aligned:
|
||||||
# Split roi mask from feed face alpha channel
|
# Split roi mask from feed face alpha channel
|
||||||
|
|
@ -240,7 +266,7 @@ class Masker(Extractor): # pylint:disable=abstract-method
|
||||||
|
|
||||||
Yields
|
Yields
|
||||||
------
|
------
|
||||||
:class:`~plugins.extract.pipeline.ExtractMedia`
|
:class:`~plugins.extract.extract_media.ExtractMedia`
|
||||||
The :attr:`DetectedFaces` list will be populated for this class with the bounding
|
The :attr:`DetectedFaces` list will be populated for this class with the bounding
|
||||||
boxes, landmarks and masks for the detected faces found in the frame.
|
boxes, landmarks and masks for the detected faces found in the frame.
|
||||||
"""
|
"""
|
||||||
|
|
@ -249,6 +275,10 @@ class Masker(Extractor): # pylint:disable=abstract-method
|
||||||
batch.detected_faces,
|
batch.detected_faces,
|
||||||
batch.feed_faces,
|
batch.feed_faces,
|
||||||
batch.roi_masks):
|
batch.roi_masks):
|
||||||
|
if self.name in ("Components", "Extended") and not np.any(mask):
|
||||||
|
# Components/Extended masks can return empty when called from the manual tool with
|
||||||
|
# 4 Point ROI landmarks
|
||||||
|
continue
|
||||||
self._crop_out_of_bounds(mask, roi_mask)
|
self._crop_out_of_bounds(mask, roi_mask)
|
||||||
face.add_mask(self._storage_name,
|
face.add_mask(self._storage_name,
|
||||||
mask,
|
mask,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import typing as T
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
from lib.align import LandmarkType
|
||||||
|
|
||||||
from ._base import BatchType, Masker
|
from ._base import BatchType, Masker
|
||||||
|
|
||||||
if T.TYPE_CHECKING:
|
if T.TYPE_CHECKING:
|
||||||
|
|
@ -26,6 +28,7 @@ class Mask(Masker):
|
||||||
self.vram = 0 # Doesn't use GPU
|
self.vram = 0 # Doesn't use GPU
|
||||||
self.vram_per_batch = 0
|
self.vram_per_batch = 0
|
||||||
self.batchsize = 1
|
self.batchsize = 1
|
||||||
|
self.landmark_type = LandmarkType.LM_2D_68
|
||||||
|
|
||||||
def init_model(self) -> None:
|
def init_model(self) -> None:
|
||||||
logger.debug("No mask model to initialize")
|
logger.debug("No mask model to initialize")
|
||||||
|
|
@ -40,6 +43,10 @@ class Mask(Masker):
|
||||||
faces: list[AlignedFace] = feed[1]
|
faces: list[AlignedFace] = feed[1]
|
||||||
feed = feed[0]
|
feed = feed[0]
|
||||||
for mask, face in zip(feed, faces):
|
for mask, face in zip(feed, faces):
|
||||||
|
if LandmarkType.from_shape(face.landmarks.shape) != self.landmark_type:
|
||||||
|
# Called from the manual tool. # TODO This will only work with BS1
|
||||||
|
feed = np.zeros_like(feed)
|
||||||
|
continue
|
||||||
parts = self.parse_parts(np.array(face.landmarks))
|
parts = self.parse_parts(np.array(face.landmarks))
|
||||||
for item in parts:
|
for item in parts:
|
||||||
a_item = np.rint(np.concatenate(item)).astype("int32")
|
a_item = np.rint(np.concatenate(item)).astype("int32")
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,9 @@ import typing as T
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
from lib.align import LandmarkType
|
||||||
|
|
||||||
from ._base import BatchType, Masker
|
from ._base import BatchType, Masker
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -25,6 +28,7 @@ class Mask(Masker):
|
||||||
self.vram = 0 # Doesn't use GPU
|
self.vram = 0 # Doesn't use GPU
|
||||||
self.vram_per_batch = 0
|
self.vram_per_batch = 0
|
||||||
self.batchsize = 1
|
self.batchsize = 1
|
||||||
|
self.landmark_type = LandmarkType.LM_2D_68
|
||||||
|
|
||||||
def init_model(self) -> None:
|
def init_model(self) -> None:
|
||||||
logger.debug("No mask model to initialize")
|
logger.debug("No mask model to initialize")
|
||||||
|
|
@ -39,6 +43,10 @@ class Mask(Masker):
|
||||||
faces: list[AlignedFace] = feed[1]
|
faces: list[AlignedFace] = feed[1]
|
||||||
feed = feed[0]
|
feed = feed[0]
|
||||||
for mask, face in zip(feed, faces):
|
for mask, face in zip(feed, faces):
|
||||||
|
if LandmarkType.from_shape(face.landmarks.shape) != self.landmark_type:
|
||||||
|
# Called from the manual tool. # TODO This will only work with BS1
|
||||||
|
feed = np.zeros_like(feed)
|
||||||
|
continue
|
||||||
parts = self.parse_parts(np.array(face.landmarks))
|
parts = self.parse_parts(np.array(face.landmarks))
|
||||||
for item in parts:
|
for item in parts:
|
||||||
a_item = np.rint(np.concatenate(item)).astype("int32")
|
a_item = np.rint(np.concatenate(item)).astype("int32")
|
||||||
|
|
|
||||||
|
|
@ -10,25 +10,27 @@ plugins either in parallel or in series, giving easy access to input and output.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import typing as T
|
import typing as T
|
||||||
|
|
||||||
import cv2
|
from lib.align import LandmarkType
|
||||||
|
|
||||||
from lib.gpu_stats import GPUStats
|
from lib.gpu_stats import GPUStats
|
||||||
|
from lib.logger import parse_class_init
|
||||||
from lib.queue_manager import EventQueue, queue_manager, QueueEmpty
|
from lib.queue_manager import EventQueue, queue_manager, QueueEmpty
|
||||||
from lib.utils import get_backend
|
from lib.serializer import get_serializer
|
||||||
|
from lib.utils import get_backend, FaceswapError
|
||||||
from plugins.plugin_loader import PluginLoader
|
from plugins.plugin_loader import PluginLoader
|
||||||
|
|
||||||
if T.TYPE_CHECKING:
|
if T.TYPE_CHECKING:
|
||||||
import numpy as np
|
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
from lib.align.alignments import PNGHeaderSourceDict
|
from ._base import Extractor as PluginExtractor
|
||||||
from lib.align.detected_face import DetectedFace
|
from .align._base import Aligner
|
||||||
from plugins.extract._base import Extractor as PluginExtractor
|
from .align.external import Align as AlignImport
|
||||||
from plugins.extract.detect._base import Detector
|
from .detect._base import Detector
|
||||||
from plugins.extract.align._base import Aligner
|
from .detect.external import Detect as DetectImport
|
||||||
from plugins.extract.mask._base import Masker
|
from .mask._base import Masker
|
||||||
from plugins.extract.recognition._base import Identity
|
from .recognition._base import Identity
|
||||||
|
from . import ExtractMedia
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
_INSTANCES = -1 # Tracking for multiple instances of pipeline
|
_INSTANCES = -1 # Tracking for multiple instances of pipeline
|
||||||
|
|
@ -110,12 +112,7 @@ class Extractor():
|
||||||
re_feed: int = 0,
|
re_feed: int = 0,
|
||||||
re_align: bool = False,
|
re_align: bool = False,
|
||||||
disable_filter: bool = False) -> None:
|
disable_filter: bool = False) -> None:
|
||||||
logger.debug("Initializing %s: (detector: %s, aligner: %s, masker: %s, recognition: %s, "
|
logger.debug(parse_class_init(locals()))
|
||||||
"configfile: %s, multiprocess: %s, exclude_gpus: %s, rotate_images: %s, "
|
|
||||||
"min_size: %s, normalize_method: %s, re_feed: %s, re_align: %s, "
|
|
||||||
"disable_filter: %s)", self.__class__.__name__, detector, aligner, masker,
|
|
||||||
recognition, configfile, multiprocess, exclude_gpus, rotate_images, min_size,
|
|
||||||
normalize_method, re_feed, re_align, disable_filter)
|
|
||||||
self._instance = _get_instance()
|
self._instance = _get_instance()
|
||||||
maskers = [T.cast(str | None,
|
maskers = [T.cast(str | None,
|
||||||
masker)] if not isinstance(masker, list) else T.cast(list[str | None],
|
masker)] if not isinstance(masker, list) else T.cast(list[str | None],
|
||||||
|
|
@ -128,7 +125,7 @@ class Extractor():
|
||||||
# TODO Calculate scaling for more plugins than currently exist in _parallel_scaling
|
# TODO Calculate scaling for more plugins than currently exist in _parallel_scaling
|
||||||
self._scaling_fallback = 0.4
|
self._scaling_fallback = 0.4
|
||||||
self._vram_stats = self._get_vram_stats()
|
self._vram_stats = self._get_vram_stats()
|
||||||
self._detect = self._load_detect(detector, rotate_images, min_size, configfile)
|
self._detect = self._load_detect(detector, aligner, rotate_images, min_size, configfile)
|
||||||
self._align = self._load_align(aligner,
|
self._align = self._load_align(aligner,
|
||||||
configfile,
|
configfile,
|
||||||
normalize_method,
|
normalize_method,
|
||||||
|
|
@ -212,7 +209,7 @@ class Extractor():
|
||||||
>>> extractor.input_queue.put(extract_media)
|
>>> extractor.input_queue.put(extract_media)
|
||||||
"""
|
"""
|
||||||
retval = self._phase_index == len(self._phases) - 1
|
retval = self._phase_index == len(self._phases) - 1
|
||||||
logger.trace(retval) # type: ignore
|
logger.trace(retval) # type:ignore[attr-defined]
|
||||||
return retval
|
return retval
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -266,7 +263,7 @@ class Extractor():
|
||||||
for phase in self._current_phase:
|
for phase in self._current_phase:
|
||||||
self._launch_plugin(phase)
|
self._launch_plugin(phase)
|
||||||
|
|
||||||
def detected_faces(self) -> Generator["ExtractMedia", None, None]:
|
def detected_faces(self) -> Generator[ExtractMedia, None, None]:
|
||||||
""" Generator that returns results, frame by frame from the extraction pipeline
|
""" Generator that returns results, frame by frame from the extraction pipeline
|
||||||
|
|
||||||
This is the exit point for the extraction pipeline and is used to obtain the output
|
This is the exit point for the extraction pipeline and is used to obtain the output
|
||||||
|
|
@ -274,7 +271,7 @@ class Extractor():
|
||||||
|
|
||||||
Yields
|
Yields
|
||||||
------
|
------
|
||||||
faces: :class:`ExtractMedia`
|
faces: :class:`~plugins.extract.extract_media.ExtractMedia`
|
||||||
The populated extracted media object.
|
The populated extracted media object.
|
||||||
|
|
||||||
Example
|
Example
|
||||||
|
|
@ -300,11 +297,89 @@ class Extractor():
|
||||||
|
|
||||||
self._join_threads()
|
self._join_threads()
|
||||||
if self.final_pass:
|
if self.final_pass:
|
||||||
|
for plugin in self._all_plugins:
|
||||||
|
plugin.on_completion()
|
||||||
logger.debug("Detection Complete")
|
logger.debug("Detection Complete")
|
||||||
else:
|
else:
|
||||||
self._phase_index += 1
|
self._phase_index += 1
|
||||||
logger.debug("Switching to phase: %s", self._current_phase)
|
logger.debug("Switching to phase: %s", self._current_phase)
|
||||||
|
|
||||||
|
def _disable_lm_maskers(self) -> None:
|
||||||
|
""" Disable any 68 point landmark based maskers if alignment data is not 2D 68
|
||||||
|
point landmarks and update the process flow/phases accordingly """
|
||||||
|
logger.warning("Alignment data is not 68 point 2D landmarks. Some Faceswap functionality "
|
||||||
|
"will be unavailable for these faces")
|
||||||
|
|
||||||
|
rem_maskers = [m.name for m in self._mask
|
||||||
|
if m is not None and m.landmark_type == LandmarkType.LM_2D_68]
|
||||||
|
self._mask = [m for m in self._mask if m is None or m.name not in rem_maskers]
|
||||||
|
|
||||||
|
self._flow = [
|
||||||
|
item for item in self._flow
|
||||||
|
if not item.startswith("mask")
|
||||||
|
or item.startswith("mask") and int(item.rsplit("_", maxsplit=1)[-1]) < len(self._mask)]
|
||||||
|
|
||||||
|
self._phases = [[s for s in p if s in self._flow] for p in self._phases
|
||||||
|
if any(t in p for t in self._flow)]
|
||||||
|
|
||||||
|
for queue in self._queues:
|
||||||
|
queue_manager.del_queue(queue)
|
||||||
|
del self._queues
|
||||||
|
self._queues = self._add_queues()
|
||||||
|
|
||||||
|
logger.warning("The following maskers have been disabled due to unsupported landmarks: %s",
|
||||||
|
rem_maskers)
|
||||||
|
|
||||||
|
def import_data(self, input_location: str) -> None:
|
||||||
|
""" Import json data to the detector and/or aligner if 'import' plugin has been selected
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
input_location: str
|
||||||
|
Full path to the input location for the extract process
|
||||||
|
"""
|
||||||
|
assert self._detect is not None
|
||||||
|
import_plugins: list[DetectImport | AlignImport] = [
|
||||||
|
p for p in (self._detect, self.aligner) # type:ignore[misc]
|
||||||
|
if T.cast(str, p.name).lower() == "external"]
|
||||||
|
|
||||||
|
if not import_plugins:
|
||||||
|
return
|
||||||
|
|
||||||
|
align_origin = None
|
||||||
|
assert self.aligner.name is not None
|
||||||
|
if self.aligner.name.lower() == "external":
|
||||||
|
align_origin = self.aligner.config["origin"]
|
||||||
|
|
||||||
|
logger.info("Importing external data for %s from json file...",
|
||||||
|
" and ".join([p.__class__.__name__ for p in import_plugins]))
|
||||||
|
|
||||||
|
folder = input_location
|
||||||
|
folder = folder if os.path.isdir(folder) else os.path.dirname(folder)
|
||||||
|
|
||||||
|
last_fname = ""
|
||||||
|
is_68_point = True
|
||||||
|
for plugin in import_plugins:
|
||||||
|
plugin_type = plugin.__class__.__name__
|
||||||
|
path = os.path.join(folder, plugin.config["file_name"])
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
raise FaceswapError(f"{plugin_type} import file could not be found at '{path}'")
|
||||||
|
|
||||||
|
if path != last_fname: # Different import file for aligner data
|
||||||
|
last_fname = path
|
||||||
|
data = get_serializer("json").load(path)
|
||||||
|
|
||||||
|
if plugin_type == "Detect":
|
||||||
|
plugin.import_data(data, align_origin) # type:ignore[call-arg]
|
||||||
|
else:
|
||||||
|
plugin.import_data(data) # type:ignore[call-arg]
|
||||||
|
is_68_point = plugin.landmark_type == LandmarkType.LM_2D_68 # type:ignore[union-attr] # noqa:E501 # pylint:disable="line-too-long"
|
||||||
|
|
||||||
|
if not is_68_point:
|
||||||
|
self._disable_lm_maskers()
|
||||||
|
|
||||||
|
logger.info("Imported external data")
|
||||||
|
|
||||||
# <<< INTERNAL METHODS >>> #
|
# <<< INTERNAL METHODS >>> #
|
||||||
@property
|
@property
|
||||||
def _parallel_scaling(self) -> dict[int, float]:
|
def _parallel_scaling(self) -> dict[int, float]:
|
||||||
|
|
@ -616,14 +691,40 @@ class Extractor():
|
||||||
|
|
||||||
def _load_detect(self,
|
def _load_detect(self,
|
||||||
detector: str | None,
|
detector: str | None,
|
||||||
|
aligner: str | None,
|
||||||
rotation: str | None,
|
rotation: str | None,
|
||||||
min_size: int,
|
min_size: int,
|
||||||
configfile: str | None) -> Detector | None:
|
configfile: str | None) -> Detector | None:
|
||||||
""" Set global arguments and load detector plugin """
|
""" Set global arguments and load detector plugin
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
detector: str | None
|
||||||
|
The name of the face detection plugin to use. ``None`` for no detection
|
||||||
|
aligner: str | None
|
||||||
|
The name of the face aligner plugin to use. ``None`` for no aligner
|
||||||
|
rotation: str | None
|
||||||
|
The rotation to perform on detection. ``None`` for no rotation
|
||||||
|
min_size: int
|
||||||
|
The minimum size of detected faces to accept
|
||||||
|
configfile: str | None
|
||||||
|
Full path to a custom config file to use. ``None`` for default config
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
:class:`~plugins.extract.detect._base.Detector` | None
|
||||||
|
The face detection plugin to use, or ``None`` if no detection to be performed
|
||||||
|
"""
|
||||||
if detector is None or detector.lower() == "none":
|
if detector is None or detector.lower() == "none":
|
||||||
logger.debug("No detector selected. Returning None")
|
logger.debug("No detector selected. Returning None")
|
||||||
return None
|
return None
|
||||||
detector_name = detector.replace("-", "_").lower()
|
detector_name = detector.replace("-", "_").lower()
|
||||||
|
|
||||||
|
if aligner == "external" and detector_name != "external":
|
||||||
|
logger.warning("Unsupported '%s' detector selected for 'External' aligner. Switching "
|
||||||
|
"detector to 'External'", detector_name)
|
||||||
|
detector_name = aligner
|
||||||
|
|
||||||
logger.debug("Loading Detector: '%s'", detector_name)
|
logger.debug("Loading Detector: '%s'", detector_name)
|
||||||
plugin = PluginLoader.get_detector(detector_name)(exclude_gpus=self._exclude_gpus,
|
plugin = PluginLoader.get_detector(detector_name)(exclude_gpus=self._exclude_gpus,
|
||||||
rotation=rotation,
|
rotation=rotation,
|
||||||
|
|
@ -775,198 +876,3 @@ class Extractor():
|
||||||
""" Check all threads for errors and raise if one occurs """
|
""" Check all threads for errors and raise if one occurs """
|
||||||
for plugin in self._active_plugins:
|
for plugin in self._active_plugins:
|
||||||
plugin.check_and_raise_error()
|
plugin.check_and_raise_error()
|
||||||
|
|
||||||
|
|
||||||
class ExtractMedia():
|
|
||||||
""" An object that passes through the :class:`~plugins.extract.pipeline.Extractor` pipeline.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
filename: str
|
|
||||||
The base name of the original frame's filename
|
|
||||||
image: :class:`numpy.ndarray`
|
|
||||||
The original frame or a faceswap aligned face image
|
|
||||||
detected_faces: list, optional
|
|
||||||
A list of :class:`~lib.align.DetectedFace` objects. Detected faces can be added
|
|
||||||
later with :func:`add_detected_faces`. Setting ``None`` will default to an empty list.
|
|
||||||
Default: ``None``
|
|
||||||
is_aligned: bool, optional
|
|
||||||
``True`` if the :attr:`image` is an aligned faceswap image otherwise ``False``. Used for
|
|
||||||
face filtering with vggface2. Aligned faceswap images will automatically skip detection,
|
|
||||||
alignment and masking. Default: ``False``
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self,
|
|
||||||
filename: str,
|
|
||||||
image: np.ndarray,
|
|
||||||
detected_faces: list[DetectedFace] | None = None,
|
|
||||||
is_aligned: bool = False) -> None:
|
|
||||||
logger.trace("Initializing %s: (filename: '%s', image shape: %s, " # type: ignore
|
|
||||||
"detected_faces: %s, is_aligned: %s)", self.__class__.__name__, filename,
|
|
||||||
image.shape, detected_faces, is_aligned)
|
|
||||||
self._filename = filename
|
|
||||||
self._image: np.ndarray | None = image
|
|
||||||
self._image_shape = T.cast(tuple[int, int, int], image.shape)
|
|
||||||
self._detected_faces: list[DetectedFace] = ([] if detected_faces is None
|
|
||||||
else detected_faces)
|
|
||||||
self._is_aligned = is_aligned
|
|
||||||
self._frame_metadata: PNGHeaderSourceDict | None = None
|
|
||||||
self._sub_folders: list[str | None] = []
|
|
||||||
|
|
||||||
@property
|
|
||||||
def filename(self) -> str:
|
|
||||||
""" str: The base name of the :attr:`image` filename. """
|
|
||||||
return self._filename
|
|
||||||
|
|
||||||
@property
|
|
||||||
def image(self) -> np.ndarray:
|
|
||||||
""" :class:`numpy.ndarray`: The source frame for this object. """
|
|
||||||
assert self._image is not None
|
|
||||||
return self._image
|
|
||||||
|
|
||||||
@property
|
|
||||||
def image_shape(self) -> tuple[int, int, int]:
|
|
||||||
""" tuple: The shape of the stored :attr:`image`. """
|
|
||||||
return self._image_shape
|
|
||||||
|
|
||||||
@property
|
|
||||||
def image_size(self) -> tuple[int, int]:
|
|
||||||
""" tuple: The (`height`, `width`) of the stored :attr:`image`. """
|
|
||||||
return self._image_shape[:2]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def detected_faces(self) -> list[DetectedFace]:
|
|
||||||
"""list: A list of :class:`~lib.align.DetectedFace` objects in the :attr:`image`. """
|
|
||||||
return self._detected_faces
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_aligned(self) -> bool:
|
|
||||||
""" bool. ``True`` if :attr:`image` is an aligned faceswap image otherwise ``False`` """
|
|
||||||
return self._is_aligned
|
|
||||||
|
|
||||||
@property
|
|
||||||
def frame_metadata(self) -> PNGHeaderSourceDict:
|
|
||||||
""" dict: The frame metadata that has been added from an aligned image. This property
|
|
||||||
should only be called after :func:`add_frame_metadata` has been called when processing
|
|
||||||
an aligned face. For all other instances an assertion error will be raised.
|
|
||||||
|
|
||||||
Raises
|
|
||||||
------
|
|
||||||
AssertionError
|
|
||||||
If frame metadata has not been populated from an aligned image
|
|
||||||
"""
|
|
||||||
assert self._frame_metadata is not None
|
|
||||||
return self._frame_metadata
|
|
||||||
|
|
||||||
@property
|
|
||||||
def sub_folders(self) -> list[str | None]:
|
|
||||||
""" list: The sub_folders that the faces should be output to. Used when binning filter
|
|
||||||
output is enabled. The list corresponds to the list of detected faces
|
|
||||||
"""
|
|
||||||
return self._sub_folders
|
|
||||||
|
|
||||||
def get_image_copy(self, color_format: T.Literal["BGR", "RGB", "GRAY"]) -> np.ndarray:
|
|
||||||
""" Get a copy of the image in the requested color format.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
color_format: ['BGR', 'RGB', 'GRAY']
|
|
||||||
The requested color format of :attr:`image`
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
:class:`numpy.ndarray`:
|
|
||||||
A copy of :attr:`image` in the requested :attr:`color_format`
|
|
||||||
"""
|
|
||||||
logger.trace("Requested color format '%s' for frame '%s'", # type: ignore
|
|
||||||
color_format, self._filename)
|
|
||||||
image = getattr(self, f"_image_as_{color_format.lower()}")()
|
|
||||||
return image
|
|
||||||
|
|
||||||
def add_detected_faces(self, faces: list[DetectedFace]) -> None:
|
|
||||||
""" Add detected faces to the object. Called at the end of each extraction phase.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
faces: list
|
|
||||||
A list of :class:`~lib.align.DetectedFace` objects
|
|
||||||
"""
|
|
||||||
logger.trace("Adding detected faces for filename: '%s'. " # type: ignore
|
|
||||||
"(faces: %s, lrtb: %s)", self._filename, faces,
|
|
||||||
[(face.left, face.right, face.top, face.bottom) for face in faces])
|
|
||||||
self._detected_faces = faces
|
|
||||||
|
|
||||||
def add_sub_folders(self, folders: list[str | None]) -> None:
|
|
||||||
""" Add detected faces to the object. Called at the end of each extraction phase.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
folders: list
|
|
||||||
A list of str sub folder names or ``None`` if no sub folder is required. Should
|
|
||||||
correspond to the detected faces list
|
|
||||||
"""
|
|
||||||
logger.trace("Adding sub folders for filename: '%s'. " # type: ignore
|
|
||||||
"(folders: %s)", self._filename, folders,)
|
|
||||||
self._sub_folders = folders
|
|
||||||
|
|
||||||
def remove_image(self) -> None:
|
|
||||||
""" Delete the image and reset :attr:`image` to ``None``.
|
|
||||||
|
|
||||||
Required for multi-phase extraction to avoid the frames stacking RAM.
|
|
||||||
"""
|
|
||||||
logger.trace("Removing image for filename: '%s'", self._filename) # type: ignore
|
|
||||||
del self._image
|
|
||||||
self._image = None
|
|
||||||
|
|
||||||
def set_image(self, image: np.ndarray) -> None:
|
|
||||||
""" Add the image back into :attr:`image`
|
|
||||||
|
|
||||||
Required for multi-phase extraction adds the image back to this object.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
image: :class:`numpy.ndarry`
|
|
||||||
The original frame to be re-applied to for this :attr:`filename`
|
|
||||||
"""
|
|
||||||
logger.trace("Reapplying image: (filename: `%s`, image shape: %s)", # type: ignore
|
|
||||||
self._filename, image.shape)
|
|
||||||
self._image = image
|
|
||||||
|
|
||||||
def add_frame_metadata(self, metadata: PNGHeaderSourceDict) -> None:
|
|
||||||
""" Add the source frame metadata from an aligned PNG's header data.
|
|
||||||
|
|
||||||
metadata: dict
|
|
||||||
The contents of the 'source' field in the PNG header
|
|
||||||
"""
|
|
||||||
logger.trace("Adding PNG Source data for '%s': %s", # type:ignore
|
|
||||||
self._filename, metadata)
|
|
||||||
dims = T.cast(tuple[int, int], metadata["source_frame_dims"])
|
|
||||||
self._image_shape = (*dims, 3)
|
|
||||||
self._frame_metadata = metadata
|
|
||||||
|
|
||||||
def _image_as_bgr(self) -> np.ndarray:
|
|
||||||
""" Get a copy of the source frame in BGR format.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
:class:`numpy.ndarray`:
|
|
||||||
A copy of :attr:`image` in BGR color format """
|
|
||||||
return self.image[..., :3].copy()
|
|
||||||
|
|
||||||
def _image_as_rgb(self) -> np.ndarray:
|
|
||||||
""" Get a copy of the source frame in RGB format.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
:class:`numpy.ndarray`:
|
|
||||||
A copy of :attr:`image` in RGB color format """
|
|
||||||
return self.image[..., 2::-1].copy()
|
|
||||||
|
|
||||||
def _image_as_gray(self) -> np.ndarray:
|
|
||||||
""" Get a copy of the source frame in gray-scale format.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
:class:`numpy.ndarray`:
|
|
||||||
A copy of :attr:`image` in gray-scale color format """
|
|
||||||
return cv2.cvtColor(self.image.copy(), cv2.COLOR_BGR2GRAY)
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
All Recognition Plugins should inherit from this class.
|
All Recognition Plugins should inherit from this class.
|
||||||
See the override methods for which methods are required.
|
See the override methods for which methods are required.
|
||||||
|
|
||||||
The plugin will receive a :class:`~plugins.extract.pipeline.ExtractMedia` object.
|
The plugin will receive a :class:`~plugins.extract.extract_media.ExtractMedia` object.
|
||||||
|
|
||||||
For each source frame, the plugin must pass a dict to finalize containing:
|
For each source frame, the plugin must pass a dict to finalize containing:
|
||||||
|
|
||||||
|
|
@ -24,11 +24,11 @@ from dataclasses import dataclass, field
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from tensorflow.python.framework import errors_impl as tf_errors # pylint:disable=no-name-in-module # noqa
|
from tensorflow.python.framework import errors_impl as tf_errors # pylint:disable=no-name-in-module # noqa
|
||||||
|
|
||||||
from lib.align import AlignedFace, DetectedFace
|
from lib.align import AlignedFace, DetectedFace, LandmarkType
|
||||||
from lib.image import read_image_meta
|
from lib.image import read_image_meta
|
||||||
from lib.utils import FaceswapError
|
from lib.utils import FaceswapError
|
||||||
from plugins.extract._base import BatchType, Extractor, ExtractorBatch
|
from plugins.extract import ExtractMedia
|
||||||
from plugins.extract.pipeline import ExtractMedia
|
from plugins.extract._base import BatchType, ExtractorBatch, Extractor
|
||||||
|
|
||||||
if T.TYPE_CHECKING:
|
if T.TYPE_CHECKING:
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
|
|
@ -75,6 +75,8 @@ class Identity(Extractor): # pylint:disable=abstract-method
|
||||||
plugins.extract.mask._base : Masker parent class for extraction plugins.
|
plugins.extract.mask._base : Masker parent class for extraction plugins.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_logged_lm_count_once = False
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
git_model_id: int | None = None,
|
git_model_id: int | None = None,
|
||||||
model_filename: str | None = None,
|
model_filename: str | None = None,
|
||||||
|
|
@ -101,7 +103,7 @@ class Identity(Extractor): # pylint:disable=abstract-method
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
item: :class:`~plugins.extract.pipeline.ExtractMedia`
|
item: :class:`~plugins.extract.extract_media.ExtractMedia`
|
||||||
The extract media to populate the detected face for
|
The extract media to populate the detected face for
|
||||||
"""
|
"""
|
||||||
detected_face = DetectedFace()
|
detected_face = DetectedFace()
|
||||||
|
|
@ -113,14 +115,28 @@ class Identity(Extractor): # pylint:disable=abstract-method
|
||||||
logger.debug("Obtained detected face: (filename: %s, detected_face: %s)",
|
logger.debug("Obtained detected face: (filename: %s, detected_face: %s)",
|
||||||
item.filename, item.detected_faces)
|
item.filename, item.detected_faces)
|
||||||
|
|
||||||
|
def _maybe_log_warning(self, face: AlignedFace) -> None:
|
||||||
|
""" Log a warning, once, if we do not have full facial landmarks
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
face: :class:`~lib.align.aligned_face.AlignedFace`
|
||||||
|
The aligned face object to test the landmark type for
|
||||||
|
"""
|
||||||
|
if face.landmark_type != LandmarkType.LM_2D_4 or self._logged_lm_count_once:
|
||||||
|
return
|
||||||
|
logger.warning("Extracted faces do not contain facial landmark data. '%s' "
|
||||||
|
"identity data is likely to be sub-standard.", self.name)
|
||||||
|
self._logged_lm_count_once = True
|
||||||
|
|
||||||
def get_batch(self, queue: Queue) -> tuple[bool, RecogBatch]:
|
def get_batch(self, queue: Queue) -> tuple[bool, RecogBatch]:
|
||||||
""" Get items for inputting into the recognition from the queue in batches
|
""" Get items for inputting into the recognition from the queue in batches
|
||||||
|
|
||||||
Items are returned from the ``queue`` in batches of
|
Items are returned from the ``queue`` in batches of
|
||||||
:attr:`~plugins.extract._base.Extractor.batchsize`
|
:attr:`~plugins.extract._base.Extractor.batchsize`
|
||||||
|
|
||||||
Items are received as :class:`~plugins.extract.pipeline.ExtractMedia` objects and converted
|
Items are received as :class:`~plugins.extract.extract_media.ExtractMedia` objects and
|
||||||
to :class:`RecogBatch` for internal processing.
|
converted to :class:`RecogBatch` for internal processing.
|
||||||
|
|
||||||
To ensure consistent batch sizes for masker the items are split into separate items for
|
To ensure consistent batch sizes for masker the items are split into separate items for
|
||||||
each :class:`~lib.align.DetectedFace` object.
|
each :class:`~lib.align.DetectedFace` object.
|
||||||
|
|
@ -173,6 +189,8 @@ class Identity(Extractor): # pylint:disable=abstract-method
|
||||||
dtype="float32",
|
dtype="float32",
|
||||||
is_aligned=item.is_aligned)
|
is_aligned=item.is_aligned)
|
||||||
|
|
||||||
|
self._maybe_log_warning(feed_face)
|
||||||
|
|
||||||
batch.detected_faces.append(face)
|
batch.detected_faces.append(face)
|
||||||
batch.feed_faces.append(feed_face)
|
batch.feed_faces.append(feed_face)
|
||||||
batch.filename.append(item.filename)
|
batch.filename.append(item.filename)
|
||||||
|
|
@ -234,7 +252,7 @@ class Identity(Extractor): # pylint:disable=abstract-method
|
||||||
|
|
||||||
Yields
|
Yields
|
||||||
------
|
------
|
||||||
:class:`~plugins.extract.pipeline.ExtractMedia`
|
:class:`~plugins.extract.extract_media.ExtractMedia`
|
||||||
The :attr:`DetectedFaces` list will be populated for this class with the bounding
|
The :attr:`DetectedFaces` list will be populated for this class with the bounding
|
||||||
boxes, landmarks and masks for the detected faces found in the frame.
|
boxes, landmarks and masks for the detected faces found in the frame.
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ from lib.image import read_image_meta_batch, ImagesLoader
|
||||||
from lib.multithreading import MultiThread, total_cpus
|
from lib.multithreading import MultiThread, total_cpus
|
||||||
from lib.queue_manager import queue_manager
|
from lib.queue_manager import queue_manager
|
||||||
from lib.utils import FaceswapError, get_folder, get_image_paths, handle_deprecated_cliopts
|
from lib.utils import FaceswapError, get_folder, get_image_paths, handle_deprecated_cliopts
|
||||||
from plugins.extract.pipeline import Extractor, ExtractMedia
|
from plugins.extract import ExtractMedia, Extractor
|
||||||
from plugins.plugin_loader import PluginLoader
|
from plugins.plugin_loader import PluginLoader
|
||||||
|
|
||||||
if T.TYPE_CHECKING:
|
if T.TYPE_CHECKING:
|
||||||
|
|
@ -44,7 +44,7 @@ class ConvertItem:
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
input: :class:`~plugins.extract.pipeline.ExtractMedia`
|
input: :class:`~plugins.extract.extract_media.ExtractMedia`
|
||||||
The ExtractMedia object holding the :attr:`filename`, :attr:`image` and attr:`list` of
|
The ExtractMedia object holding the :attr:`filename`, :attr:`image` and attr:`list` of
|
||||||
:class:`~lib.align.DetectedFace` objects loaded from disk
|
:class:`~lib.align.DetectedFace` objects loaded from disk
|
||||||
feed_faces: list, Optional
|
feed_faces: list, Optional
|
||||||
|
|
@ -702,6 +702,7 @@ class DiskIO():
|
||||||
# Write out preview image for the GUI every 10 frames if writing to stream
|
# Write out preview image for the GUI every 10 frames if writing to stream
|
||||||
if write_preview and idx % 10 == 0 and not os.path.exists(preview_image):
|
if write_preview and idx % 10 == 0 and not os.path.exists(preview_image):
|
||||||
logger.debug("Writing GUI Preview image: '%s'", preview_image)
|
logger.debug("Writing GUI Preview image: '%s'", preview_image)
|
||||||
|
assert isinstance(image, np.ndarray)
|
||||||
cv2.imwrite(preview_image, image)
|
cv2.imwrite(preview_image, image)
|
||||||
self._writer.write(filename, image)
|
self._writer.write(filename, image)
|
||||||
self._writer.close()
|
self._writer.close()
|
||||||
|
|
@ -1093,7 +1094,7 @@ class Predict():
|
||||||
logger.trace("Queued out batch. Batchsize: %s", len(batch)) # type:ignore
|
logger.trace("Queued out batch. Batchsize: %s", len(batch)) # type:ignore
|
||||||
|
|
||||||
|
|
||||||
class OptionalActions():
|
class OptionalActions(): # pylint:disable=too-few-public-methods
|
||||||
""" Process specific optional actions for Convert.
|
""" Process specific optional actions for Convert.
|
||||||
|
|
||||||
Currently only handles skip faces. This class should probably be (re)moved.
|
Currently only handles skip faces. This class should probably be (re)moved.
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ from lib.align.alignments import PNGHeaderDict
|
||||||
from lib.image import encode_image, generate_thumbnail, ImagesLoader, ImagesSaver, read_image_meta
|
from lib.image import encode_image, generate_thumbnail, ImagesLoader, ImagesSaver, read_image_meta
|
||||||
from lib.multithreading import MultiThread
|
from lib.multithreading import MultiThread
|
||||||
from lib.utils import get_folder, handle_deprecated_cliopts, IMAGE_EXTENSIONS, VIDEO_EXTENSIONS
|
from lib.utils import get_folder, handle_deprecated_cliopts, IMAGE_EXTENSIONS, VIDEO_EXTENSIONS
|
||||||
from plugins.extract.pipeline import Extractor, ExtractMedia
|
from plugins.extract import ExtractMedia, Extractor
|
||||||
from scripts.fsmedia import Alignments, PostProcess, finalize
|
from scripts.fsmedia import Alignments, PostProcess, finalize
|
||||||
|
|
||||||
if T.TYPE_CHECKING:
|
if T.TYPE_CHECKING:
|
||||||
|
|
@ -596,8 +596,8 @@ class PipelineLoader():
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
detected_faces: dict
|
detected_faces: dict
|
||||||
Dictionary of :class:`plugins.extract.pipeline.ExtractMedia` with the filename as the
|
Dictionary of :class:`~plugins.extract.extract_media.ExtractMedia` with the filename as
|
||||||
key for repopulating the image attribute.
|
the key for repopulating the image attribute.
|
||||||
"""
|
"""
|
||||||
logger.debug("Reload Images: Start. Detected Faces Count: %s", len(detected_faces))
|
logger.debug("Reload Images: Start. Detected Faces Count: %s", len(detected_faces))
|
||||||
load_queue = self._extractor.input_queue
|
load_queue = self._extractor.input_queue
|
||||||
|
|
@ -643,6 +643,7 @@ class _Extract():
|
||||||
|
|
||||||
self._alignments = Alignments(self._args, True, self._loader.is_video)
|
self._alignments = Alignments(self._args, True, self._loader.is_video)
|
||||||
self._extractor = extractor
|
self._extractor = extractor
|
||||||
|
self._extractor.import_data(self._args.input_dir)
|
||||||
|
|
||||||
self._existing_count = 0
|
self._existing_count = 0
|
||||||
self._set_skip_list()
|
self._set_skip_list()
|
||||||
|
|
@ -753,7 +754,7 @@ class _Extract():
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
extract_media: :class:`plugins.extract.pipeline.ExtractMedia`
|
extract_media: :class:`~plugins.extract.extract_media.ExtractMedia`
|
||||||
Output from :class:`plugins.extract.pipeline.Extractor`
|
Output from :class:`plugins.extract.pipeline.Extractor`
|
||||||
size: int
|
size: int
|
||||||
The size that the aligned face should be created at
|
The size that the aligned face should be created at
|
||||||
|
|
@ -785,7 +786,7 @@ class _Extract():
|
||||||
----------
|
----------
|
||||||
saver: :class:`lib.images.ImagesSaver` or ``None``
|
saver: :class:`lib.images.ImagesSaver` or ``None``
|
||||||
The background saver for saving the image or ``None`` if faces are not to be saved
|
The background saver for saving the image or ``None`` if faces are not to be saved
|
||||||
extract_media: :class:`~plugins.extract.pipeline.ExtractMedia`
|
extract_media: :class:`~plugins.extract.extract_media.ExtractMedia`
|
||||||
The output from :class:`~plugins.extract.Pipeline.Extractor`
|
The output from :class:`~plugins.extract.Pipeline.Extractor`
|
||||||
"""
|
"""
|
||||||
logger.trace("Outputting faces for %s", extract_media.filename) # type: ignore
|
logger.trace("Outputting faces for %s", extract_media.filename) # type: ignore
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ if T.TYPE_CHECKING:
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from lib.align import AlignedFace
|
from lib.align import AlignedFace
|
||||||
from plugins.extract.pipeline import ExtractMedia
|
from plugins.extract import ExtractMedia
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -414,14 +414,15 @@ class PostProcess():
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
extract_media: :class:`~plugins.extract.pipeline.ExtractMedia`
|
extract_media: :class:`~plugins.extract.extract_media.ExtractMedia`
|
||||||
The :class:`~plugins.extract.pipeline.ExtractMedia` object to perform the
|
The :class:`~plugins.extract.extract_media.ExtractMedia` object to perform the
|
||||||
action on.
|
action on.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
:class:`~plugins.extract.pipeline.ExtractMedia`
|
:class:`~plugins.extract.extract_media.ExtractMedia`
|
||||||
The original :class:`~plugins.extract.pipeline.ExtractMedia` with any actions applied
|
The original :class:`~plugins.extract.extract_media.ExtractMedia` with any actions
|
||||||
|
applied
|
||||||
"""
|
"""
|
||||||
for action in self._actions:
|
for action in self._actions:
|
||||||
logger.debug("Performing postprocess action: '%s'", action.__class__.__name__)
|
logger.debug("Performing postprocess action: '%s'", action.__class__.__name__)
|
||||||
|
|
@ -458,8 +459,8 @@ class PostProcessAction():
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
extract_media: :class:`~plugins.extract.pipeline.ExtractMedia`
|
extract_media: :class:`~plugins.extract.extract_media.ExtractMedia`
|
||||||
The :class:`~plugins.extract.pipeline.ExtractMedia` object to perform the
|
The :class:`~plugins.extract.extract_media.ExtractMedia` object to perform the
|
||||||
action on.
|
action on.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
@ -578,9 +579,9 @@ class DebugLandmarks(PostProcessAction):
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
extract_media: :class:`~plugins.extract.pipeline.ExtractMedia`
|
extract_media: :class:`~plugins.extract.extract_media.ExtractMedia`
|
||||||
The :class:`~plugins.extract.pipeline.ExtractMedia` object that contains the faces to
|
The :class:`~plugins.extract.extract_media.ExtractMedia` object that contains the faces
|
||||||
draw the landmarks on to
|
to draw the landmarks on to
|
||||||
"""
|
"""
|
||||||
frame = os.path.splitext(os.path.basename(extract_media.filename))[0]
|
frame = os.path.splitext(os.path.basename(extract_media.filename))[0]
|
||||||
for idx, face in enumerate(extract_media.detected_faces):
|
for idx, face in enumerate(extract_media.detected_faces):
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ from multiprocessing import Process
|
||||||
|
|
||||||
from lib.utils import FaceswapError, handle_deprecated_cliopts, VIDEO_EXTENSIONS
|
from lib.utils import FaceswapError, handle_deprecated_cliopts, VIDEO_EXTENSIONS
|
||||||
from .media import AlignmentData
|
from .media import AlignmentData
|
||||||
from .jobs import Check, Sort, Spatial # noqa pylint:disable=unused-import
|
from .jobs import Check, Export, Sort, Spatial # noqa pylint:disable=unused-import
|
||||||
from .jobs_faces import FromFaces, RemoveFaces, Rename # noqa pylint:disable=unused-import
|
from .jobs_faces import FromFaces, RemoveFaces, Rename # noqa pylint:disable=unused-import
|
||||||
from .jobs_frames import Draw, Extract # noqa pylint:disable=unused-import
|
from .jobs_frames import Draw, Extract # noqa pylint:disable=unused-import
|
||||||
|
|
||||||
|
|
@ -34,7 +34,7 @@ class Alignments():
|
||||||
"""
|
"""
|
||||||
def __init__(self, arguments: Namespace) -> None:
|
def __init__(self, arguments: Namespace) -> None:
|
||||||
logger.debug("Initializing %s: (arguments: %s)", self.__class__.__name__, arguments)
|
logger.debug("Initializing %s: (arguments: %s)", self.__class__.__name__, arguments)
|
||||||
self._requires_alignments = ["sort", "spatial"]
|
self._requires_alignments = ["export", "sort", "spatial"]
|
||||||
self._requires_faces = ["extract", "from-faces"]
|
self._requires_faces = ["extract", "from-faces"]
|
||||||
self._requires_frames = ["draw",
|
self._requires_frames = ["draw",
|
||||||
"extract",
|
"extract",
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,9 @@ class AlignmentsArgs(FaceSwapArgs):
|
||||||
"opts": ("-j", "--job"),
|
"opts": ("-j", "--job"),
|
||||||
"action": Radio,
|
"action": Radio,
|
||||||
"type": str,
|
"type": str,
|
||||||
"choices": ("draw", "extract", "from-faces", "missing-alignments", "missing-frames",
|
"choices": ("draw", "extract", "export", "from-faces", "missing-alignments",
|
||||||
"multi-faces", "no-faces", "remove-faces", "rename", "sort", "spatial"),
|
"missing-frames", "multi-faces", "no-faces", "remove-faces", "rename",
|
||||||
|
"sort", "spatial"),
|
||||||
"group": _("processing"),
|
"group": _("processing"),
|
||||||
"required": True,
|
"required": True,
|
||||||
"help": _(
|
"help": _(
|
||||||
|
|
@ -61,6 +62,12 @@ class AlignmentsArgs(FaceSwapArgs):
|
||||||
"alignments file (-a) to be passed in."
|
"alignments file (-a) to be passed in."
|
||||||
"\nL|'draw': Draw landmarks on frames in the selected folder/video. A "
|
"\nL|'draw': Draw landmarks on frames in the selected folder/video. A "
|
||||||
"subfolder will be created within the frames folder to hold the output.{0}"
|
"subfolder will be created within the frames folder to hold the output.{0}"
|
||||||
|
"\nL|'export': Export the contents of an alignments file to a json file. Can be "
|
||||||
|
"used for editing alignment information in external tools and then re-importing "
|
||||||
|
"by using Faceswap's Extract 'Import' plugins. Note: masks and identity vectors "
|
||||||
|
"will not be included in the exported file, so will be re-generated when the json "
|
||||||
|
"file is imported back into Faceswap. All data is exported with the origin (0, 0) "
|
||||||
|
"at the top left of the canvas."
|
||||||
"\nL|'extract': Re-extract faces from the source frames/video based on "
|
"\nL|'extract': Re-extract faces from the source frames/video based on "
|
||||||
"alignment data. This is a lot quicker than re-detecting faces. Can pass in "
|
"alignment data. This is a lot quicker than re-detecting faces. Can pass in "
|
||||||
"the '-een' (--extract-every-n) parameter to only extract every nth frame.{1}"
|
"the '-een' (--extract-every-n) parameter to only extract every nth frame.{1}"
|
||||||
|
|
|
||||||
|
|
@ -13,19 +13,23 @@ from scipy import signal
|
||||||
from sklearn import decomposition
|
from sklearn import decomposition
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
from lib.logger import parse_class_init
|
||||||
|
from lib.serializer import get_serializer
|
||||||
|
from lib.utils import FaceswapError
|
||||||
|
|
||||||
from .media import Faces, Frames
|
from .media import Faces, Frames
|
||||||
from .jobs_faces import FaceToFile
|
from .jobs_faces import FaceToFile
|
||||||
|
|
||||||
if T.TYPE_CHECKING:
|
if T.TYPE_CHECKING:
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from lib.align.alignments import PNGHeaderDict
|
from lib.align.alignments import AlignmentFileDict, PNGHeaderDict
|
||||||
from .media import AlignmentData
|
from .media import AlignmentData
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Check():
|
class Check:
|
||||||
""" Frames and faces checking tasks.
|
""" Frames and faces checking tasks.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
|
@ -36,7 +40,7 @@ class Check():
|
||||||
The command line arguments that have called this job
|
The command line arguments that have called this job
|
||||||
"""
|
"""
|
||||||
def __init__(self, alignments: AlignmentData, arguments: Namespace) -> None:
|
def __init__(self, alignments: AlignmentData, arguments: Namespace) -> None:
|
||||||
logger.debug("Initializing %s: (arguments: %s)", self.__class__.__name__, arguments)
|
logger.debug(parse_class_init(locals()))
|
||||||
self._alignments = alignments
|
self._alignments = alignments
|
||||||
self._job = arguments.job
|
self._job = arguments.job
|
||||||
self._type: T.Literal["faces", "frames"] | None = None
|
self._type: T.Literal["faces", "frames"] | None = None
|
||||||
|
|
@ -371,7 +375,81 @@ class Check():
|
||||||
os.rename(src, dst)
|
os.rename(src, dst)
|
||||||
|
|
||||||
|
|
||||||
class Sort():
|
class Export:
|
||||||
|
""" Export alignments from a Faceswap .fsa file to a json formatted file.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
alignments: :class:`tools.lib_alignments.media.AlignmentData`
|
||||||
|
The alignments data loaded from an alignments file for this rename job
|
||||||
|
arguments: :class:`argparse.Namespace`
|
||||||
|
The :mod:`argparse` arguments as passed in from :mod:`tools.py`. Unused
|
||||||
|
"""
|
||||||
|
def __init__(self,
|
||||||
|
alignments: AlignmentData,
|
||||||
|
arguments: Namespace) -> None: # pylint:disable=unused-argument
|
||||||
|
logger.debug(parse_class_init(locals()))
|
||||||
|
self._alignments = alignments
|
||||||
|
self._serializer = get_serializer("json")
|
||||||
|
self._output_file = self._get_output_file()
|
||||||
|
logger.debug("Initialized %s", self.__class__.__name__)
|
||||||
|
|
||||||
|
def _get_output_file(self) -> str:
|
||||||
|
""" Obtain the name of an output file. If a file of the request name exists, then append a
|
||||||
|
digit to the end until a unique filename is found
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
Full path to an output json file
|
||||||
|
"""
|
||||||
|
in_file = self._alignments.file
|
||||||
|
base_filename = f"{os.path.splitext(in_file)[0]}_export"
|
||||||
|
out_file = f"{base_filename}.json"
|
||||||
|
idx = 1
|
||||||
|
while True:
|
||||||
|
if not os.path.exists(out_file):
|
||||||
|
break
|
||||||
|
logger.debug("Output file exists: '%s'", out_file)
|
||||||
|
out_file = f"{base_filename}_{idx}.json"
|
||||||
|
idx += 1
|
||||||
|
logger.debug("Setting output file to '%s'", out_file)
|
||||||
|
return out_file
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _format_face(cls, face: AlignmentFileDict) -> dict[str, list[int] | list[list[float]]]:
|
||||||
|
""" Format the relevant keys from an alignment file's face into the correct format for
|
||||||
|
export/import
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
face: :class:`~lib.align.alignments.AlignmentFileDict`
|
||||||
|
The alignment dictionary for a face to process
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
dict[str, list[int] | list[list[float]]]
|
||||||
|
The face formatted for exporting to a json file
|
||||||
|
"""
|
||||||
|
lms = face["landmarks_xy"]
|
||||||
|
assert isinstance(lms, np.ndarray)
|
||||||
|
retval = {"detected": [int(round(face["x"], 0)),
|
||||||
|
int(round(face["y"], 0)),
|
||||||
|
int(round(face["x"] + face["w"], 0)),
|
||||||
|
int(round(face["y"] + face["h"], 0))],
|
||||||
|
"landmarks_2d": lms.tolist()}
|
||||||
|
return retval
|
||||||
|
|
||||||
|
def process(self) -> None:
|
||||||
|
""" Parse the imported alignments file and output relevant information to a json file """
|
||||||
|
logger.info("[EXPORTING ALIGNMENTS]") # Tidy up cli output
|
||||||
|
formatted = {key: [self._format_face(face) for face in val["faces"]]
|
||||||
|
for key, val in self._alignments.data.items()}
|
||||||
|
logger.info("Saving export alignments to '%s'...", self._output_file)
|
||||||
|
self._serializer.save(self._output_file, formatted)
|
||||||
|
|
||||||
|
|
||||||
|
class Sort:
|
||||||
""" Sort alignments' index by the order they appear in an image in left to right order.
|
""" Sort alignments' index by the order they appear in an image in left to right order.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
|
@ -379,10 +457,12 @@ class Sort():
|
||||||
alignments: :class:`tools.lib_alignments.media.AlignmentData`
|
alignments: :class:`tools.lib_alignments.media.AlignmentData`
|
||||||
The alignments data loaded from an alignments file for this rename job
|
The alignments data loaded from an alignments file for this rename job
|
||||||
arguments: :class:`argparse.Namespace`
|
arguments: :class:`argparse.Namespace`
|
||||||
The :mod:`argparse` arguments as passed in from :mod:`tools.py`
|
The :mod:`argparse` arguments as passed in from :mod:`tools.py`. Unused
|
||||||
"""
|
"""
|
||||||
def __init__(self, alignments: AlignmentData, arguments: Namespace) -> None:
|
def __init__(self,
|
||||||
logger.debug("Initializing %s: (arguments: %s)", self.__class__.__name__, arguments)
|
alignments: AlignmentData,
|
||||||
|
arguments: Namespace) -> None: # pylint:disable=unused-argument
|
||||||
|
logger.debug(parse_class_init(locals()))
|
||||||
self._alignments = alignments
|
self._alignments = alignments
|
||||||
logger.debug("Initialized %s", self.__class__.__name__)
|
logger.debug("Initialized %s", self.__class__.__name__)
|
||||||
|
|
||||||
|
|
@ -418,7 +498,7 @@ class Sort():
|
||||||
return reindexed
|
return reindexed
|
||||||
|
|
||||||
|
|
||||||
class Spatial():
|
class Spatial:
|
||||||
""" Apply spatial temporal filtering to landmarks
|
""" Apply spatial temporal filtering to landmarks
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
|
@ -433,7 +513,7 @@ class Spatial():
|
||||||
https://www.kaggle.com/selfishgene/animating-and-smoothing-3d-facial-keypoints/notebook
|
https://www.kaggle.com/selfishgene/animating-and-smoothing-3d-facial-keypoints/notebook
|
||||||
"""
|
"""
|
||||||
def __init__(self, alignments: AlignmentData, arguments: Namespace) -> None:
|
def __init__(self, alignments: AlignmentData, arguments: Namespace) -> None:
|
||||||
logger.debug("Initializing %s: (arguments: %s)", self.__class__.__name__, arguments)
|
logger.debug(parse_class_init(locals()))
|
||||||
self.arguments = arguments
|
self.arguments = arguments
|
||||||
self._alignments = alignments
|
self._alignments = alignments
|
||||||
self._mappings: dict[int, str] = {}
|
self._mappings: dict[int, str] = {}
|
||||||
|
|
@ -467,7 +547,7 @@ class Spatial():
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
shaped_im_coords: :class:`numpy.ndarray`
|
shaped_im_coords: :class:`numpy.ndarray`
|
||||||
The 68 point landmarks
|
The facial landmarks
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
|
|
@ -530,7 +610,15 @@ class Spatial():
|
||||||
""" Compile all original and normalized alignments """
|
""" Compile all original and normalized alignments """
|
||||||
logger.debug("Normalize")
|
logger.debug("Normalize")
|
||||||
count = sum(1 for val in self._alignments.data.values() if val["faces"])
|
count = sum(1 for val in self._alignments.data.values() if val["faces"])
|
||||||
landmarks_all = np.zeros((68, 2, int(count)))
|
|
||||||
|
sample_lm = next((val["faces"][0]["landmarks_xy"]
|
||||||
|
for val in self._alignments.data.values() if val["faces"]), 68)
|
||||||
|
assert isinstance(sample_lm, np.ndarray)
|
||||||
|
lm_count = sample_lm.shape[0]
|
||||||
|
if lm_count != 68:
|
||||||
|
raise FaceswapError("Spatial smoothing only supports 68 point facial landmarks")
|
||||||
|
|
||||||
|
landmarks_all = np.zeros((lm_count, 2, int(count)))
|
||||||
|
|
||||||
end = 0
|
end = 0
|
||||||
for key in tqdm(sorted(self._alignments.data.keys()), desc="Compiling", leave=False):
|
for key in tqdm(sorted(self._alignments.data.keys()), desc="Compiling", leave=False):
|
||||||
|
|
@ -539,7 +627,7 @@ class Spatial():
|
||||||
continue
|
continue
|
||||||
# We should only be normalizing a single face, so just take
|
# We should only be normalizing a single face, so just take
|
||||||
# the first landmarks found
|
# the first landmarks found
|
||||||
landmarks = np.array(val[0]["landmarks_xy"]).reshape((68, 2, 1))
|
landmarks = np.array(val[0]["landmarks_xy"]).reshape((lm_count, 2, 1))
|
||||||
start = end
|
start = end
|
||||||
end = start + landmarks.shape[2]
|
end = start + landmarks.shape[2]
|
||||||
# Store in one big array
|
# Store in one big array
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,10 @@ import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
from lib.align import DetectedFace, _EXTRACT_RATIOS
|
from lib.align import DetectedFace, EXTRACT_RATIOS, LANDMARK_PARTS, LandmarkType
|
||||||
from lib.align.alignments import _VERSION, PNGHeaderDict
|
from lib.align.alignments import _VERSION, PNGHeaderDict
|
||||||
from lib.image import encode_image, generate_thumbnail, ImagesSaver
|
from lib.image import encode_image, generate_thumbnail, ImagesSaver
|
||||||
from plugins.extract.pipeline import Extractor, ExtractMedia
|
from plugins.extract import ExtractMedia, Extractor
|
||||||
from .media import ExtractedFaces, Frames
|
from .media import ExtractedFaces, Frames
|
||||||
|
|
||||||
if T.TYPE_CHECKING:
|
if T.TYPE_CHECKING:
|
||||||
|
|
@ -41,14 +41,6 @@ class Draw():
|
||||||
self._alignments = alignments
|
self._alignments = alignments
|
||||||
self._frames = Frames(arguments.frames_dir)
|
self._frames = Frames(arguments.frames_dir)
|
||||||
self._output_folder = self._set_output()
|
self._output_folder = self._set_output()
|
||||||
self._mesh_areas = {"mouth": (48, 68),
|
|
||||||
"right_eyebrow": (17, 22),
|
|
||||||
"left_eyebrow": (22, 27),
|
|
||||||
"right_eye": (36, 42),
|
|
||||||
"left_eye": (42, 48),
|
|
||||||
"nose": (27, 36),
|
|
||||||
"jaw": (0, 17),
|
|
||||||
"chin": (8, 11)}
|
|
||||||
logger.debug("Initialized %s", self.__class__.__name__)
|
logger.debug("Initialized %s", self.__class__.__name__)
|
||||||
|
|
||||||
def _set_output(self) -> str:
|
def _set_output(self) -> str:
|
||||||
|
|
@ -121,12 +113,11 @@ class Draw():
|
||||||
image: :class:`numpy.ndarray`
|
image: :class:`numpy.ndarray`
|
||||||
The frame that extract boxes are to be annotated on to
|
The frame that extract boxes are to be annotated on to
|
||||||
landmarks: :class:`numpy.ndarray`
|
landmarks: :class:`numpy.ndarray`
|
||||||
The 68 point landmarks that are to be annotated onto the frame
|
The facial landmarks that are to be annotated onto the frame
|
||||||
"""
|
"""
|
||||||
# Mesh
|
# Mesh
|
||||||
for area, indices in self._mesh_areas.items():
|
for start, end, fill in LANDMARK_PARTS[LandmarkType.from_shape(landmarks.shape)].values():
|
||||||
fill = area in ("right_eye", "left_eye", "mouth")
|
cv2.polylines(image, [landmarks[start:end]], fill, (255, 255, 0), 1)
|
||||||
cv2.polylines(image, [landmarks[indices[0]:indices[1]]], fill, (255, 255, 0), 1)
|
|
||||||
# Landmarks
|
# Landmarks
|
||||||
for (pos_x, pos_y) in landmarks:
|
for (pos_x, pos_y) in landmarks:
|
||||||
cv2.circle(image, (pos_x, pos_y), 1, (0, 255, 255), -1)
|
cv2.circle(image, (pos_x, pos_y), 1, (0, 255, 255), -1)
|
||||||
|
|
@ -462,9 +453,9 @@ class Extract():
|
||||||
continue
|
continue
|
||||||
old_mask = mask.mask.astype("float32") / 255.0
|
old_mask = mask.mask.astype("float32") / 255.0
|
||||||
size = old_mask.shape[0]
|
size = old_mask.shape[0]
|
||||||
new_size = int(size + (size * _EXTRACT_RATIOS["face"]) / 2)
|
new_size = int(size + (size * EXTRACT_RATIOS["face"]) / 2)
|
||||||
|
|
||||||
shift = np.rint(offset * (size - (size * _EXTRACT_RATIOS["face"]))).astype("int32")
|
shift = np.rint(offset * (size - (size * EXTRACT_RATIOS["face"]))).astype("int32")
|
||||||
pos = np.array([(new_size // 2 - size // 2) - shift[1],
|
pos = np.array([(new_size // 2 - size // 2) - shift[1],
|
||||||
(new_size // 2) + (size // 2) - shift[1],
|
(new_size // 2) + (size // 2) - shift[1],
|
||||||
(new_size // 2 - size // 2) - shift[0],
|
(new_size // 2 - size // 2) - shift[0],
|
||||||
|
|
|
||||||
|
|
@ -684,7 +684,7 @@ class FaceUpdate():
|
||||||
width: int,
|
width: int,
|
||||||
pnt_y: int,
|
pnt_y: int,
|
||||||
height: int,
|
height: int,
|
||||||
aligner: T.Literal["cv2-dnn", "FAN"] = "FAN") -> None:
|
aligner: manual.TypeManualExtractor = "FAN") -> None:
|
||||||
""" Update the bounding box for the :class:`~lib.align.DetectedFace` object at the
|
""" Update the bounding box for the :class:`~lib.align.DetectedFace` object at the
|
||||||
given frame and face indices, with the given dimensions and update the 68 point landmarks
|
given frame and face indices, with the given dimensions and update the 68 point landmarks
|
||||||
from the :class:`~tools.manual.manual.Aligner` for the updated bounding box.
|
from the :class:`~tools.manual.manual.Aligner` for the updated bounding box.
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
""" The Faces Viewer Frame and Canvas for Faceswap's Manual Tool. """
|
""" The Faces Viewer Frame and Canvas for Faceswap's Manual Tool. """
|
||||||
|
from __future__ import annotations
|
||||||
import colorsys
|
import colorsys
|
||||||
import gettext
|
import gettext
|
||||||
import logging
|
import logging
|
||||||
import platform
|
import platform
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
|
import typing as T
|
||||||
from math import floor, ceil
|
from math import floor, ceil
|
||||||
from threading import Thread, Event
|
from threading import Thread, Event
|
||||||
|
|
||||||
|
|
@ -14,9 +16,15 @@ import numpy as np
|
||||||
from lib.gui.custom_widgets import RightClickMenu, Tooltip
|
from lib.gui.custom_widgets import RightClickMenu, Tooltip
|
||||||
from lib.gui.utils import get_config, get_images
|
from lib.gui.utils import get_config, get_images
|
||||||
from lib.image import hex_to_rgb, rgb_to_hex
|
from lib.image import hex_to_rgb, rgb_to_hex
|
||||||
|
from lib.logger import parse_class_init
|
||||||
|
|
||||||
from .viewport import Viewport
|
from .viewport import Viewport
|
||||||
|
|
||||||
|
if T.TYPE_CHECKING:
|
||||||
|
from tools.manual.detected_faces import DetectedFaces
|
||||||
|
from tools.manual.frameviewer.frame import DisplayFrame
|
||||||
|
from tools.manual.manual import TkGlobals
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# LOCALES
|
# LOCALES
|
||||||
|
|
@ -39,16 +47,18 @@ class FacesFrame(ttk.Frame): # pylint:disable=too-many-ancestors
|
||||||
display_frame: :class:`~tools.manual.frameviewer.frame.DisplayFrame`
|
display_frame: :class:`~tools.manual.frameviewer.frame.DisplayFrame`
|
||||||
The section of the Manual Tool that holds the frames viewer
|
The section of the Manual Tool that holds the frames viewer
|
||||||
"""
|
"""
|
||||||
def __init__(self, parent, tk_globals, detected_faces, display_frame):
|
def __init__(self,
|
||||||
logger.debug("Initializing %s: (parent: %s, tk_globals: %s, detected_faces: %s, "
|
parent: ttk.PanedWindow,
|
||||||
"display_frame: %s)", self.__class__.__name__, parent, tk_globals,
|
tk_globals: TkGlobals,
|
||||||
detected_faces, display_frame)
|
detected_faces: DetectedFaces,
|
||||||
|
display_frame: DisplayFrame) -> None:
|
||||||
|
logger.debug(parse_class_init(locals()))
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
|
self.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
|
||||||
self._actions_frame = FacesActionsFrame(self)
|
self._actions_frame = FacesActionsFrame(self)
|
||||||
|
|
||||||
self._faces_frame = ttk.Frame(self)
|
self._faces_frame = ttk.Frame(self)
|
||||||
self._faces_frame.pack_propagate(0)
|
self._faces_frame.pack_propagate(False)
|
||||||
self._faces_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
self._faces_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
self._event = Event()
|
self._event = Event()
|
||||||
self._canvas = FacesViewer(self._faces_frame,
|
self._canvas = FacesViewer(self._faces_frame,
|
||||||
|
|
@ -60,7 +70,7 @@ class FacesFrame(ttk.Frame): # pylint:disable=too-many-ancestors
|
||||||
self._add_scrollbar()
|
self._add_scrollbar()
|
||||||
logger.debug("Initialized %s", self.__class__.__name__)
|
logger.debug("Initialized %s", self.__class__.__name__)
|
||||||
|
|
||||||
def _add_scrollbar(self):
|
def _add_scrollbar(self) -> None:
|
||||||
""" Add a scrollbar to the faces frame """
|
""" Add a scrollbar to the faces frame """
|
||||||
logger.debug("Add Faces Viewer Scrollbar")
|
logger.debug("Add Faces Viewer Scrollbar")
|
||||||
scrollbar = ttk.Scrollbar(self._faces_frame, command=self._on_scroll)
|
scrollbar = ttk.Scrollbar(self._faces_frame, command=self._on_scroll)
|
||||||
|
|
@ -69,9 +79,8 @@ class FacesFrame(ttk.Frame): # pylint:disable=too-many-ancestors
|
||||||
self.bind("<Configure>", self._update_viewport)
|
self.bind("<Configure>", self._update_viewport)
|
||||||
logger.debug("Added Faces Viewer Scrollbar")
|
logger.debug("Added Faces Viewer Scrollbar")
|
||||||
self.update_idletasks() # Update so scrollbar width is correct
|
self.update_idletasks() # Update so scrollbar width is correct
|
||||||
return scrollbar.winfo_width()
|
|
||||||
|
|
||||||
def _on_scroll(self, *event):
|
def _on_scroll(self, *event: tk.Event) -> None:
|
||||||
""" Callback on scrollbar scroll. Updates the canvas location and displays/hides
|
""" Callback on scrollbar scroll. Updates the canvas location and displays/hides
|
||||||
thumbnail images.
|
thumbnail images.
|
||||||
|
|
||||||
|
|
@ -83,7 +92,7 @@ class FacesFrame(ttk.Frame): # pylint:disable=too-many-ancestors
|
||||||
self._canvas.yview(*event)
|
self._canvas.yview(*event)
|
||||||
self._canvas.viewport.update()
|
self._canvas.viewport.update()
|
||||||
|
|
||||||
def _update_viewport(self, event): # pylint:disable=unused-argument
|
def _update_viewport(self, event: tk.Event) -> None: # pylint:disable=unused-argument
|
||||||
""" Update the faces viewport and scrollbar.
|
""" Update the faces viewport and scrollbar.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
|
@ -94,7 +103,7 @@ class FacesFrame(ttk.Frame): # pylint:disable=too-many-ancestors
|
||||||
self._canvas.viewport.update()
|
self._canvas.viewport.update()
|
||||||
self._canvas.configure(scrollregion=self._canvas.bbox("backdrop"))
|
self._canvas.configure(scrollregion=self._canvas.bbox("backdrop"))
|
||||||
|
|
||||||
def canvas_scroll(self, direction):
|
def canvas_scroll(self, direction: T.Literal["up", "down", "page-up", "page-down"]) -> None:
|
||||||
""" Scroll the canvas on an up/down or page-up/page-down key press.
|
""" Scroll the canvas on an up/down or page-up/page-down key press.
|
||||||
|
|
||||||
Notes
|
Notes
|
||||||
|
|
@ -110,9 +119,11 @@ class FacesFrame(ttk.Frame): # pylint:disable=too-many-ancestors
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self._event.is_set():
|
if self._event.is_set():
|
||||||
logger.trace("Update already running. Aborting repeated keypress")
|
logger.trace("Update already running. " # type:ignore[attr-defined]
|
||||||
|
"Aborting repeated keypress")
|
||||||
return
|
return
|
||||||
logger.trace("Running update on received key press: %s", direction)
|
logger.trace("Running update on received key press: %s", # type:ignore[attr-defined]
|
||||||
|
direction)
|
||||||
|
|
||||||
amount = 1 if direction.endswith("down") else -1
|
amount = 1 if direction.endswith("down") else -1
|
||||||
units = "pages" if direction.startswith("page") else "units"
|
units = "pages" if direction.startswith("page") else "units"
|
||||||
|
|
@ -121,7 +132,7 @@ class FacesFrame(ttk.Frame): # pylint:disable=too-many-ancestors
|
||||||
args=(amount, units, self._event))
|
args=(amount, units, self._event))
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
def set_annotation_display(self, key):
|
def set_annotation_display(self, key: str) -> None:
|
||||||
""" Set the optional annotation overlay based on keyboard shortcut.
|
""" Set the optional annotation overlay based on keyboard shortcut.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
|
@ -140,33 +151,33 @@ class FacesActionsFrame(ttk.Frame): # pylint:disable=too-many-ancestors
|
||||||
parent: :class:`FacesFrame`
|
parent: :class:`FacesFrame`
|
||||||
The Faces frame that this actions frame reside in
|
The Faces frame that this actions frame reside in
|
||||||
"""
|
"""
|
||||||
def __init__(self, parent):
|
def __init__(self, parent: FacesFrame) -> None:
|
||||||
logger.debug("Initializing %s: (parent: %s)",
|
logger.debug(parse_class_init(locals()))
|
||||||
self.__class__.__name__, parent)
|
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.pack(side=tk.LEFT, fill=tk.Y, padx=(2, 4), pady=2)
|
self.pack(side=tk.LEFT, fill=tk.Y, padx=(2, 4), pady=2)
|
||||||
self._tk_vars = dict()
|
self._tk_vars: dict[T.Literal["mesh", "mask"], tk.BooleanVar] = {}
|
||||||
self._configure_styles()
|
self._configure_styles()
|
||||||
self._buttons = self._add_buttons()
|
self._buttons = self._add_buttons()
|
||||||
logger.debug("Initialized %s", self.__class__.__name__)
|
logger.debug("Initialized %s", self.__class__.__name__)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def key_bindings(self):
|
def key_bindings(self) -> dict[str, T.Literal["mask", "mesh"]]:
|
||||||
""" dict: The mapping of key presses to optional annotations to display. Keyboard shortcuts
|
""" dict: The mapping of key presses to optional annotations to display. Keyboard shortcuts
|
||||||
utilize the function keys. """
|
utilize the function keys. """
|
||||||
return {"F{}".format(idx + 9): display for idx, display in enumerate(("mesh", "mask"))}
|
return {f"F{idx + 9}": display
|
||||||
|
for idx, display in enumerate(T.get_args(T.Literal["mesh", "mask"]))}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _helptext(self):
|
def _helptext(self) -> dict[T.Literal["mask", "mesh"], str]:
|
||||||
""" dict: `button key`: `button helptext`. The help text to display for each button. """
|
""" dict: `button key`: `button helptext`. The help text to display for each button. """
|
||||||
inverse_keybindings = {val: key for key, val in self.key_bindings.items()}
|
inverse_keybindings = {val: key for key, val in self.key_bindings.items()}
|
||||||
retval = dict(mesh=_("Display the landmarks mesh"),
|
retval: dict[T.Literal["mask", "mesh"], str] = {"mesh": _('Display the landmarks mesh'),
|
||||||
mask=_("Display the mask"))
|
"mask": _('Display the mask')}
|
||||||
for item in retval:
|
for item in retval:
|
||||||
retval[item] += " ({})".format(inverse_keybindings[item])
|
retval[item] += f" ({inverse_keybindings[item]})"
|
||||||
return retval
|
return retval
|
||||||
|
|
||||||
def _configure_styles(self):
|
def _configure_styles(self) -> None:
|
||||||
""" Configure the background color for button frame and the button styles. """
|
""" Configure the background color for button frame and the button styles. """
|
||||||
style = ttk.Style()
|
style = ttk.Style()
|
||||||
style.configure("display.TFrame", background='#d3d3d3')
|
style.configure("display.TFrame", background='#d3d3d3')
|
||||||
|
|
@ -174,17 +185,17 @@ class FacesActionsFrame(ttk.Frame): # pylint:disable=too-many-ancestors
|
||||||
style.configure("display_deselected.TButton", relief="flat")
|
style.configure("display_deselected.TButton", relief="flat")
|
||||||
self.config(style="display.TFrame")
|
self.config(style="display.TFrame")
|
||||||
|
|
||||||
def _add_buttons(self):
|
def _add_buttons(self) -> dict[T.Literal["mesh", "mask"], ttk.Button]:
|
||||||
""" Add the display buttons to the Faces window.
|
""" Add the display buttons to the Faces window.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
dict
|
dict[Literal["mesh", "mask"], tk.Button]]
|
||||||
The display name and its associated button.
|
The display name and its associated button.
|
||||||
"""
|
"""
|
||||||
frame = ttk.Frame(self)
|
frame = ttk.Frame(self)
|
||||||
frame.pack(side=tk.TOP, fill=tk.Y)
|
frame.pack(side=tk.TOP, fill=tk.Y)
|
||||||
buttons = dict()
|
buttons = {}
|
||||||
for display in self.key_bindings.values():
|
for display in self.key_bindings.values():
|
||||||
var = tk.BooleanVar()
|
var = tk.BooleanVar()
|
||||||
var.set(False)
|
var.set(False)
|
||||||
|
|
@ -193,7 +204,7 @@ class FacesActionsFrame(ttk.Frame): # pylint:disable=too-many-ancestors
|
||||||
lookup = "landmarks" if display == "mesh" else display
|
lookup = "landmarks" if display == "mesh" else display
|
||||||
button = ttk.Button(frame,
|
button = ttk.Button(frame,
|
||||||
image=get_images().icons[lookup],
|
image=get_images().icons[lookup],
|
||||||
command=lambda t=display: self.on_click(t),
|
command=T.cast(T.Callable, lambda t=display: self.on_click(t)),
|
||||||
style="display_deselected.TButton")
|
style="display_deselected.TButton")
|
||||||
button.state(["!pressed", "!focus"])
|
button.state(["!pressed", "!focus"])
|
||||||
button.pack()
|
button.pack()
|
||||||
|
|
@ -201,13 +212,13 @@ class FacesActionsFrame(ttk.Frame): # pylint:disable=too-many-ancestors
|
||||||
buttons[display] = button
|
buttons[display] = button
|
||||||
return buttons
|
return buttons
|
||||||
|
|
||||||
def on_click(self, display):
|
def on_click(self, display: T.Literal["mesh", "mask"]) -> None:
|
||||||
""" Click event for the optional annotation buttons. Loads and unloads the annotations from
|
""" Click event for the optional annotation buttons. Loads and unloads the annotations from
|
||||||
the faces viewer.
|
the faces viewer.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
display: str
|
display: Literal["mesh", "mask"]
|
||||||
The display name for the button that has called this event as exists in
|
The display name for the button that has called this event as exists in
|
||||||
:attr:`_buttons`
|
:attr:`_buttons`
|
||||||
"""
|
"""
|
||||||
|
|
@ -239,16 +250,19 @@ class FacesViewer(tk.Canvas): # pylint:disable=too-many-ancestors
|
||||||
event: :class:`threading.Event`
|
event: :class:`threading.Event`
|
||||||
The threading event object for repeated key press protection
|
The threading event object for repeated key press protection
|
||||||
"""
|
"""
|
||||||
def __init__(self, parent, tk_globals, tk_action_vars, detected_faces, display_frame, event):
|
def __init__(self, parent: ttk.Frame,
|
||||||
logger.debug("Initializing %s: (parent: %s, tk_globals: %s, tk_action_vars: %s, "
|
tk_globals: TkGlobals,
|
||||||
"detected_faces: %s, display_frame: %s, event: %s)", self.__class__.__name__,
|
tk_action_vars: dict[T.Literal["mesh", "mask"], tk.BooleanVar],
|
||||||
parent, tk_globals, tk_action_vars, detected_faces, display_frame, event)
|
detected_faces: DetectedFaces,
|
||||||
|
display_frame: DisplayFrame,
|
||||||
|
event: Event) -> None:
|
||||||
|
logger.debug(parse_class_init(locals()))
|
||||||
super().__init__(parent,
|
super().__init__(parent,
|
||||||
bd=0,
|
bd=0,
|
||||||
highlightthickness=0,
|
highlightthickness=0,
|
||||||
bg=get_config().user_theme["group_panel"]["panel_background"])
|
bg=get_config().user_theme["group_panel"]["panel_background"])
|
||||||
self.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, anchor=tk.E)
|
self.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, anchor=tk.E)
|
||||||
self._sizes = dict(tiny=32, small=64, medium=96, large=128, extralarge=192)
|
self._sizes = {"tiny": 32, "small": 64, "medium": 96, "large": 128, "extralarge": 192}
|
||||||
|
|
||||||
self._globals = tk_globals
|
self._globals = tk_globals
|
||||||
self._tk_optional_annotations = tk_action_vars
|
self._tk_optional_annotations = tk_action_vars
|
||||||
|
|
@ -256,8 +270,8 @@ class FacesViewer(tk.Canvas): # pylint:disable=too-many-ancestors
|
||||||
self._display_frame = display_frame
|
self._display_frame = display_frame
|
||||||
self._grid = Grid(self, detected_faces)
|
self._grid = Grid(self, detected_faces)
|
||||||
self._view = Viewport(self, detected_faces.tk_edited)
|
self._view = Viewport(self, detected_faces.tk_edited)
|
||||||
self._annotation_colors = dict(mesh=self.get_muted_color("Mesh"),
|
self._annotation_colors = {"mesh": self.get_muted_color("Mesh"),
|
||||||
box=self.control_colors["ExtractBox"])
|
"box": self.control_colors["ExtractBox"]}
|
||||||
|
|
||||||
ContextMenu(self, detected_faces)
|
ContextMenu(self, detected_faces)
|
||||||
self._bind_mouse_wheel_scrolling()
|
self._bind_mouse_wheel_scrolling()
|
||||||
|
|
@ -265,7 +279,7 @@ class FacesViewer(tk.Canvas): # pylint:disable=too-many-ancestors
|
||||||
logger.debug("Initialized %s", self.__class__.__name__)
|
logger.debug("Initialized %s", self.__class__.__name__)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def face_size(self):
|
def face_size(self) -> int:
|
||||||
""" int: The currently selected thumbnail size in pixels """
|
""" int: The currently selected thumbnail size in pixels """
|
||||||
scaling = get_config().scaling_factor
|
scaling = get_config().scaling_factor
|
||||||
size = self._sizes[self._globals.tk_faces_size.get().lower().replace(" ", "")]
|
size = self._sizes[self._globals.tk_faces_size.get().lower().replace(" ", "")]
|
||||||
|
|
@ -273,58 +287,65 @@ class FacesViewer(tk.Canvas): # pylint:disable=too-many-ancestors
|
||||||
return int(round(scaled / 2) * 2)
|
return int(round(scaled / 2) * 2)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def viewport(self):
|
def viewport(self) -> Viewport:
|
||||||
""" :class:`~tools.manual.faceviewer.viewport.Viewport`: The viewport area of the
|
""" :class:`~tools.manual.faceviewer.viewport.Viewport`: The viewport area of the
|
||||||
faces viewer. """
|
faces viewer. """
|
||||||
return self._view
|
return self._view
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def grid(self):
|
def layout(self) -> Grid:
|
||||||
""" :class:`Grid`: The grid for the current :class:`FacesViewer`. """
|
""" :class:`Grid`: The grid for the current :class:`FacesViewer`. """
|
||||||
return self._grid
|
return self._grid
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def optional_annotations(self):
|
def optional_annotations(self) -> dict[T.Literal["mesh", "mask"], bool]:
|
||||||
""" dict: The values currently set for the selectable optional annotations. """
|
""" dict[Literal["mesh", "mask"], bool]: The values currently set for the
|
||||||
|
selectable optional annotations. """
|
||||||
return {opt: val.get() for opt, val in self._tk_optional_annotations.items()}
|
return {opt: val.get() for opt, val in self._tk_optional_annotations.items()}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def selected_mask(self):
|
def selected_mask(self) -> str:
|
||||||
""" str: The currently selected mask from the display frame control panel. """
|
""" str: The currently selected mask from the display frame control panel. """
|
||||||
return self._display_frame.tk_selected_mask.get().lower()
|
return self._display_frame.tk_selected_mask.get().lower()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def control_colors(self):
|
def control_colors(self) -> dict[str, str]:
|
||||||
""" :dict: The frame Editor name as key with the current user selected hex code as
|
"""dict[str, str]: The frame Editor name as key with the current user selected hex code as
|
||||||
value. """
|
value. """
|
||||||
return ({key: val.get() for key, val in self._display_frame.tk_control_colors.items()})
|
return ({key: val.get() for key, val in self._display_frame.tk_control_colors.items()})
|
||||||
|
|
||||||
# << CALLBACK FUNCTIONS >> #
|
# << CALLBACK FUNCTIONS >> #
|
||||||
def _set_tk_callbacks(self, detected_faces):
|
def _set_tk_callbacks(self, detected_faces: DetectedFaces):
|
||||||
""" Set the tkinter variable call backs.
|
""" Set the tkinter variable call backs.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
detected_faces: :class:`~tools.manual.detected_faces.DetectedFaces`
|
||||||
|
The Manual Tool's Detected Faces object
|
||||||
|
|
||||||
Redraw the grid on a face size change, a filter change or on add/remove faces.
|
Redraw the grid on a face size change, a filter change or on add/remove faces.
|
||||||
Updates the annotation colors when user amends a color drop down.
|
Updates the annotation colors when user amends a color drop down.
|
||||||
Updates the mask type when the user changes the selected mask types
|
Updates the mask type when the user changes the selected mask types
|
||||||
Toggles the face viewer annotations on an optional annotation button press.
|
Toggles the face viewer annotations on an optional annotation button press.
|
||||||
"""
|
"""
|
||||||
for var in (self._globals.tk_faces_size, self._globals.tk_filter_mode):
|
for var in (self._globals.tk_faces_size, self._globals.tk_filter_mode):
|
||||||
var.trace("w", lambda *e, v=var: self.refresh_grid(v))
|
var.trace_add("write", lambda *e, v=var: self.refresh_grid(v))
|
||||||
var = detected_faces.tk_face_count_changed
|
var = detected_faces.tk_face_count_changed
|
||||||
var.trace("w", lambda *e, v=var: self.refresh_grid(v, retain_position=True))
|
var.trace_add("write", lambda *e, v=var: self.refresh_grid(v, retain_position=True))
|
||||||
|
|
||||||
self._display_frame.tk_control_colors["Mesh"].trace(
|
self._display_frame.tk_control_colors["Mesh"].trace_add(
|
||||||
"w", lambda *e: self._update_mesh_color())
|
"write", lambda *e: self._update_mesh_color())
|
||||||
self._display_frame.tk_control_colors["ExtractBox"].trace(
|
self._display_frame.tk_control_colors["ExtractBox"].trace_add(
|
||||||
"w", lambda *e: self._update_box_color())
|
"write", lambda *e: self._update_box_color())
|
||||||
self._display_frame.tk_selected_mask.trace("w", lambda *e: self._update_mask_type())
|
self._display_frame.tk_selected_mask.trace_add(
|
||||||
|
"write", lambda *e: self._update_mask_type())
|
||||||
|
|
||||||
for opt, var in self._tk_optional_annotations.items():
|
for opt, var in self._tk_optional_annotations.items():
|
||||||
var.trace("w", lambda *e, o=opt: self._toggle_annotations(o))
|
var.trace_add("write", lambda *e, o=opt: self._toggle_annotations(o))
|
||||||
|
|
||||||
self.bind("<Configure>", lambda *e: self._view.update())
|
self.bind("<Configure>", lambda *e: self._view.update())
|
||||||
|
|
||||||
def refresh_grid(self, trigger_var, retain_position=False):
|
def refresh_grid(self, trigger_var: tk.BooleanVar, retain_position: bool = False) -> None:
|
||||||
""" Recalculate the full grid and redraw. Used when the active filter pull down is used, a
|
""" Recalculate the full grid and redraw. Used when the active filter pull down is used, a
|
||||||
face has been added or removed, or the face thumbnail size has changed.
|
face has been added or removed, or the face thumbnail size has changed.
|
||||||
|
|
||||||
|
|
@ -351,15 +372,16 @@ class FacesViewer(tk.Canvas): # pylint:disable=too-many-ancestors
|
||||||
if not size_change:
|
if not size_change:
|
||||||
trigger_var.set(False)
|
trigger_var.set(False)
|
||||||
|
|
||||||
def _update_mask_type(self):
|
def _update_mask_type(self) -> None:
|
||||||
""" Update the displayed mask in the :class:`FacesViewer` canvas when the user changes
|
""" Update the displayed mask in the :class:`FacesViewer` canvas when the user changes
|
||||||
the mask type. """
|
the mask type. """
|
||||||
|
state: T.Literal["normal", "hidden"]
|
||||||
state = "normal" if self.optional_annotations["mask"] else "hidden"
|
state = "normal" if self.optional_annotations["mask"] else "hidden"
|
||||||
logger.debug("Updating mask type: (mask_type: %s. state: %s)", self.selected_mask, state)
|
logger.debug("Updating mask type: (mask_type: %s. state: %s)", self.selected_mask, state)
|
||||||
self._view.toggle_mask(state, self.selected_mask)
|
self._view.toggle_mask(state, self.selected_mask)
|
||||||
|
|
||||||
# << MOUSE HANDLING >>
|
# << MOUSE HANDLING >>
|
||||||
def _bind_mouse_wheel_scrolling(self):
|
def _bind_mouse_wheel_scrolling(self) -> None:
|
||||||
""" Bind mouse wheel to scroll the :class:`FacesViewer` canvas. """
|
""" Bind mouse wheel to scroll the :class:`FacesViewer` canvas. """
|
||||||
if platform.system() == "Linux":
|
if platform.system() == "Linux":
|
||||||
self.bind("<Button-4>", self._scroll)
|
self.bind("<Button-4>", self._scroll)
|
||||||
|
|
@ -367,7 +389,7 @@ class FacesViewer(tk.Canvas): # pylint:disable=too-many-ancestors
|
||||||
else:
|
else:
|
||||||
self.bind("<MouseWheel>", self._scroll)
|
self.bind("<MouseWheel>", self._scroll)
|
||||||
|
|
||||||
def _scroll(self, event):
|
def _scroll(self, event: tk.Event) -> None:
|
||||||
""" Handle mouse wheel scrolling over the :class:`FacesViewer` canvas.
|
""" Handle mouse wheel scrolling over the :class:`FacesViewer` canvas.
|
||||||
|
|
||||||
Update is run in a thread to avoid repeated scroll actions stacking and locking up the GUI.
|
Update is run in a thread to avoid repeated scroll actions stacking and locking up the GUI.
|
||||||
|
|
@ -378,12 +400,13 @@ class FacesViewer(tk.Canvas): # pylint:disable=too-many-ancestors
|
||||||
The event fired by the mouse scrolling
|
The event fired by the mouse scrolling
|
||||||
"""
|
"""
|
||||||
if self._event.is_set():
|
if self._event.is_set():
|
||||||
logger.trace("Update already running. Aborting repeated mousewheel")
|
logger.trace("Update already running. " # type:ignore[attr-defined]
|
||||||
|
"Aborting repeated mousewheel")
|
||||||
return
|
return
|
||||||
if platform.system() == "Darwin":
|
if platform.system() == "Darwin":
|
||||||
adjust = event.delta
|
adjust = event.delta
|
||||||
elif platform.system() == "Windows":
|
elif platform.system() == "Windows":
|
||||||
adjust = event.delta / 120
|
adjust = int(event.delta / 120)
|
||||||
elif event.num == 5:
|
elif event.num == 5:
|
||||||
adjust = -1
|
adjust = -1
|
||||||
else:
|
else:
|
||||||
|
|
@ -392,14 +415,14 @@ class FacesViewer(tk.Canvas): # pylint:disable=too-many-ancestors
|
||||||
thread = Thread(target=self.canvas_scroll, args=(-1 * adjust, "units", self._event))
|
thread = Thread(target=self.canvas_scroll, args=(-1 * adjust, "units", self._event))
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
def canvas_scroll(self, amount, units, event):
|
def canvas_scroll(self, amount: int, units: T.Literal["pages", "units"], event: Event) -> None:
|
||||||
""" Scroll the canvas on an up/down or page-up/page-down key press.
|
""" Scroll the canvas on an up/down or page-up/page-down key press.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
amount: int
|
amount: int
|
||||||
The number of units to scroll the canvas
|
The number of units to scroll the canvas
|
||||||
units: ["page", "units"]
|
units: Literal["pages", "units"]
|
||||||
The unit type to scroll by
|
The unit type to scroll by
|
||||||
event: :class:`threading.Event`
|
event: :class:`threading.Event`
|
||||||
event to indicate to the calling process whether the scroll is still updating
|
event to indicate to the calling process whether the scroll is still updating
|
||||||
|
|
@ -410,7 +433,7 @@ class FacesViewer(tk.Canvas): # pylint:disable=too-many-ancestors
|
||||||
event.clear()
|
event.clear()
|
||||||
|
|
||||||
# << OPTIONAL ANNOTATION METHODS >> #
|
# << OPTIONAL ANNOTATION METHODS >> #
|
||||||
def _update_mesh_color(self):
|
def _update_mesh_color(self) -> None:
|
||||||
""" Update the mesh color when user updates the control panel. """
|
""" Update the mesh color when user updates the control panel. """
|
||||||
color = self.get_muted_color("Mesh")
|
color = self.get_muted_color("Mesh")
|
||||||
if self._annotation_colors["mesh"] == color:
|
if self._annotation_colors["mesh"] == color:
|
||||||
|
|
@ -423,7 +446,7 @@ class FacesViewer(tk.Canvas): # pylint:disable=too-many-ancestors
|
||||||
self.itemconfig("active_mesh_line", fill=highlight_color)
|
self.itemconfig("active_mesh_line", fill=highlight_color)
|
||||||
self._annotation_colors["mesh"] = color
|
self._annotation_colors["mesh"] = color
|
||||||
|
|
||||||
def _update_box_color(self):
|
def _update_box_color(self) -> None:
|
||||||
""" Update the active box color when user updates the control panel. """
|
""" Update the active box color when user updates the control panel. """
|
||||||
color = self.control_colors["ExtractBox"]
|
color = self.control_colors["ExtractBox"]
|
||||||
|
|
||||||
|
|
@ -432,13 +455,18 @@ class FacesViewer(tk.Canvas): # pylint:disable=too-many-ancestors
|
||||||
self.itemconfig("active_highlighter", outline=color)
|
self.itemconfig("active_highlighter", outline=color)
|
||||||
self._annotation_colors["box"] = color
|
self._annotation_colors["box"] = color
|
||||||
|
|
||||||
def get_muted_color(self, color_key):
|
def get_muted_color(self, color_key: str) -> str:
|
||||||
""" Creates a muted version of the given annotation color for non-active faces.
|
""" Creates a muted version of the given annotation color for non-active faces.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
color_key: str
|
color_key: str
|
||||||
The annotation key to obtain the color for from :attr:`control_colors`
|
The annotation key to obtain the color for from :attr:`control_colors`
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
The hex color code of the muted color
|
||||||
"""
|
"""
|
||||||
scale = 0.65
|
scale = 0.65
|
||||||
hls = np.array(colorsys.rgb_to_hls(*hex_to_rgb(self.control_colors[color_key])))
|
hls = np.array(colorsys.rgb_to_hls(*hex_to_rgb(self.control_colors[color_key])))
|
||||||
|
|
@ -448,7 +476,7 @@ class FacesViewer(tk.Canvas): # pylint:disable=too-many-ancestors
|
||||||
retval = rgb_to_hex(rgb)
|
retval = rgb_to_hex(rgb)
|
||||||
return retval
|
return retval
|
||||||
|
|
||||||
def _toggle_annotations(self, annotation):
|
def _toggle_annotations(self, annotation: T.Literal["mesh", "mask"]) -> None:
|
||||||
""" Toggle optional annotations on or off after the user depresses an optional button.
|
""" Toggle optional annotations on or off after the user depresses an optional button.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
|
@ -456,6 +484,7 @@ class FacesViewer(tk.Canvas): # pylint:disable=too-many-ancestors
|
||||||
annotation: ["mesh", "mask"]
|
annotation: ["mesh", "mask"]
|
||||||
The optional annotation to toggle on or off
|
The optional annotation to toggle on or off
|
||||||
"""
|
"""
|
||||||
|
state: T.Literal["hidden", "normal"]
|
||||||
state = "normal" if self.optional_annotations[annotation] else "hidden"
|
state = "normal" if self.optional_annotations[annotation] else "hidden"
|
||||||
logger.debug("Toggle annotation: (annotation: %s, state: %s)", annotation, state)
|
logger.debug("Toggle annotation: (annotation: %s, state: %s)", annotation, state)
|
||||||
if annotation == "mesh":
|
if annotation == "mesh":
|
||||||
|
|
@ -473,23 +502,22 @@ class Grid():
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
canvas: :class:`tkinter.Canvas`
|
canvas: :class:`~FacesViewer`
|
||||||
The :class:`~tools.manual.faceviewer.frame.FacesViewer` canvas
|
The :class:`~tools.manual.faceviewer.frame.FacesViewer` canvas
|
||||||
detected_faces: :class:`~tools.manual.detected_faces.DetectedFaces`
|
detected_faces: :class:`~tools.manual.detected_faces.DetectedFaces`
|
||||||
The :class:`~lib.align.DetectedFace` objects for this video
|
The :class:`~lib.align.DetectedFace` objects for this video
|
||||||
"""
|
"""
|
||||||
def __init__(self, canvas, detected_faces):
|
def __init__(self, canvas: FacesViewer, detected_faces: DetectedFaces):
|
||||||
logger.debug("Initializing %s: (detected_faces: %s)",
|
logger.debug(parse_class_init(locals()))
|
||||||
self.__class__.__name__, detected_faces)
|
|
||||||
self._canvas = canvas
|
self._canvas = canvas
|
||||||
self._detected_faces = detected_faces
|
self._detected_faces = detected_faces
|
||||||
self._raw_indices = detected_faces.filter.raw_indices
|
self._raw_indices = detected_faces.filter.raw_indices
|
||||||
self._frames_list = detected_faces.filter.frames_list
|
self._frames_list = detected_faces.filter.frames_list
|
||||||
|
|
||||||
self._is_valid = False
|
self._is_valid: bool = False
|
||||||
self._face_size = None
|
self._face_size: int = 0
|
||||||
self._grid = None
|
self._grid: np.ndarray | None = None
|
||||||
self._display_faces = None
|
self._display_faces: np.ndarray | None = None
|
||||||
|
|
||||||
self._canvas.update_idletasks()
|
self._canvas.update_idletasks()
|
||||||
self._canvas.create_rectangle(0, 0, 0, 0, tags=["backdrop"])
|
self._canvas.create_rectangle(0, 0, 0, 0, tags=["backdrop"])
|
||||||
|
|
@ -497,64 +525,76 @@ class Grid():
|
||||||
logger.debug("Initialized %s", self.__class__.__name__)
|
logger.debug("Initialized %s", self.__class__.__name__)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def face_size(self):
|
def face_size(self) -> int:
|
||||||
""" int: The pixel size of each thumbnail within the face viewer. """
|
""" int: The pixel size of each thumbnail within the face viewer. """
|
||||||
return self._face_size
|
return self._face_size
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_valid(self):
|
def is_valid(self) -> bool:
|
||||||
""" bool: ``True`` if the current filter means that the grid holds faces. ``False`` if
|
""" bool: ``True`` if the current filter means that the grid holds faces. ``False`` if
|
||||||
there are no faces displayed in the grid. """
|
there are no faces displayed in the grid. """
|
||||||
return self._is_valid
|
return self._is_valid
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def columns_rows(self):
|
def columns_rows(self) -> tuple[int, int]:
|
||||||
""" tuple: the (`columns`, `rows`) required to hold all display images. """
|
""" tuple: the (`columns`, `rows`) required to hold all display images. """
|
||||||
retval = tuple(reversed(self._grid.shape[1:])) if self._is_valid else (0, 0)
|
if not self._is_valid:
|
||||||
return retval
|
return (0, 0)
|
||||||
|
assert self._grid is not None
|
||||||
|
retval = tuple(reversed(self._grid.shape[1:]))
|
||||||
|
return T.cast(tuple[int, int], retval)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dimensions(self):
|
def dimensions(self) -> tuple[int, int]:
|
||||||
""" tuple: The (`width`, `height`) required to hold all display images. """
|
""" tuple: The (`width`, `height`) required to hold all display images. """
|
||||||
if self._is_valid:
|
if self._is_valid:
|
||||||
|
assert self._grid is not None
|
||||||
retval = tuple(dim * self._face_size for dim in reversed(self._grid.shape[1:]))
|
retval = tuple(dim * self._face_size for dim in reversed(self._grid.shape[1:]))
|
||||||
|
assert len(retval) == 2
|
||||||
else:
|
else:
|
||||||
retval = (0, 0)
|
retval = (0, 0)
|
||||||
return retval
|
return T.cast(tuple[int, int], retval)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _visible_row_indices(self):
|
def _visible_row_indices(self) -> tuple[int, int]:
|
||||||
"""tuple: A 1 dimensional array of the (`top_row_index`, `bottom_row_index`) of the grid
|
"""tuple: A 1 dimensional array of the (`top_row_index`, `bottom_row_index`) of the grid
|
||||||
currently in the viewable area.
|
currently in the viewable area.
|
||||||
"""
|
"""
|
||||||
height = self.dimensions[1]
|
height = self.dimensions[1]
|
||||||
visible = (max(0, floor(height * self._canvas.yview()[0]) - self._face_size),
|
visible = (max(0, floor(height * self._canvas.yview()[0]) - self._face_size),
|
||||||
ceil(height * self._canvas.yview()[1]))
|
ceil(height * self._canvas.yview()[1]))
|
||||||
logger.trace("height: %s, yview: %s, face_size: %s, visible: %s",
|
logger.trace("height: %s, yview: %s, face_size: %s, " # type:ignore[attr-defined]
|
||||||
height, self._canvas.yview(), self._face_size, visible)
|
"visible: %s", height, self._canvas.yview(), self._face_size, visible)
|
||||||
|
assert self._grid is not None
|
||||||
y_points = self._grid[3, :, 1]
|
y_points = self._grid[3, :, 1]
|
||||||
top = np.searchsorted(y_points, visible[0], side="left")
|
top = np.searchsorted(y_points, visible[0], side="left")
|
||||||
bottom = np.searchsorted(y_points, visible[1], side="right")
|
bottom = np.searchsorted(y_points, visible[1], side="right")
|
||||||
return top, bottom
|
return int(top), int(bottom)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def visible_area(self):
|
def visible_area(self) -> tuple[np.ndarray, np.ndarray]:
|
||||||
""":class:`numpy.ndarray`: A numpy array of shape (`4`, `rows`, `columns`) corresponding
|
"""tuple[:class:`numpy.ndarray`, :class:`numpy.ndarray`]: Tuple containing 2 arrays.
|
||||||
|
|
||||||
|
1st array contains an array of shape (`4`, `rows`, `columns`) corresponding
|
||||||
to the viewable area of the display grid. 1st dimension contains frame indices, 2nd
|
to the viewable area of the display grid. 1st dimension contains frame indices, 2nd
|
||||||
dimension face indices. The 3rd and 4th dimension contain the x and y position of the top
|
dimension face indices. The 3rd and 4th dimension contain the x and y position of the top
|
||||||
left corner of the face respectively.
|
left corner of the face respectively.
|
||||||
|
|
||||||
|
2nd array contains :class:`~lib.align.DetectedFace` objects laid out in (rows, columns)
|
||||||
|
|
||||||
Any locations that are not populated by a face will have a frame and face index of -1
|
Any locations that are not populated by a face will have a frame and face index of -1
|
||||||
"""
|
"""
|
||||||
if not self._is_valid:
|
if not self._is_valid:
|
||||||
retval = None, None
|
retval = np.zeros((4, 0, 0)), np.zeros((0, 0))
|
||||||
else:
|
else:
|
||||||
|
assert self._grid is not None
|
||||||
|
assert self._display_faces is not None
|
||||||
top, bottom = self._visible_row_indices
|
top, bottom = self._visible_row_indices
|
||||||
retval = self._grid[:, top:bottom, :], self._display_faces[top:bottom, :]
|
retval = self._grid[:, top:bottom, :], self._display_faces[top:bottom, :]
|
||||||
logger.trace([r if r is None else r.shape for r in retval])
|
logger.trace([r if r is None else r.shape for r in retval]) # type:ignore[attr-defined]
|
||||||
return retval
|
return retval
|
||||||
|
|
||||||
def y_coord_from_frame(self, frame_index):
|
def y_coord_from_frame(self, frame_index: int) -> int:
|
||||||
""" Return the y coordinate for the first face that appears in the given frame.
|
""" Return the y coordinate for the first face that appears in the given frame.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
|
@ -567,9 +607,10 @@ class Grid():
|
||||||
int
|
int
|
||||||
The y coordinate of the first face for the given frame
|
The y coordinate of the first face for the given frame
|
||||||
"""
|
"""
|
||||||
|
assert self._grid is not None
|
||||||
return min(self._grid[3][np.where(self._grid[0] == frame_index)])
|
return min(self._grid[3][np.where(self._grid[0] == frame_index)])
|
||||||
|
|
||||||
def frame_has_faces(self, frame_index):
|
def frame_has_faces(self, frame_index: int) -> bool | np.bool_:
|
||||||
""" Check whether the given frame index contains any faces.
|
""" Check whether the given frame index contains any faces.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
|
@ -582,9 +623,12 @@ class Grid():
|
||||||
bool
|
bool
|
||||||
``True`` if there are faces in the given frame otherwise ``False``
|
``True`` if there are faces in the given frame otherwise ``False``
|
||||||
"""
|
"""
|
||||||
return self._is_valid and np.any(self._grid[0] == frame_index)
|
if not self._is_valid:
|
||||||
|
return False
|
||||||
|
assert self._grid is not None
|
||||||
|
return np.any(self._grid[0] == frame_index)
|
||||||
|
|
||||||
def update(self):
|
def update(self) -> None:
|
||||||
""" Update the underlying grid.
|
""" Update the underlying grid.
|
||||||
|
|
||||||
Called on initialization, on a filter change or on add/remove faces. Recalculates the
|
Called on initialization, on a filter change or on add/remove faces. Recalculates the
|
||||||
|
|
@ -597,25 +641,23 @@ class Grid():
|
||||||
self._get_grid()
|
self._get_grid()
|
||||||
self._get_display_faces()
|
self._get_display_faces()
|
||||||
self._canvas.coords("backdrop", 0, 0, *self.dimensions)
|
self._canvas.coords("backdrop", 0, 0, *self.dimensions)
|
||||||
self._canvas.configure(scrollregion=(self._canvas.bbox("backdrop")))
|
self._canvas.configure(scrollregion=self._canvas.bbox("backdrop"))
|
||||||
self._canvas.yview_moveto(0.0)
|
self._canvas.yview_moveto(0.0)
|
||||||
|
|
||||||
def _get_grid(self):
|
def _get_grid(self) -> None:
|
||||||
""" Get the grid information for faces currently displayed in the :class:`FacesViewer`.
|
""" Get the grid information for faces currently displayed in the :class:`FacesViewer`.
|
||||||
|
and set to :attr:`_grid`. Creates a numpy array of shape (`4`, `rows`, `columns`)
|
||||||
|
corresponding to the display grid. 1st dimension contains frame indices, 2nd dimension face
|
||||||
|
indices. The 3rd and 4th dimension contain the x and y position of the top left corner of
|
||||||
|
the face respectively.
|
||||||
|
|
||||||
Returns
|
Any locations that are not populated by a face will have a frame and face index of -1"""
|
||||||
:class:`numpy.ndarray`
|
|
||||||
A numpy array of shape (`4`, `rows`, `columns`) corresponding to the display grid.
|
|
||||||
1st dimension contains frame indices, 2nd dimension face indices. The 3rd and 4th
|
|
||||||
dimension contain the x and y position of the top left corner of the face respectively.
|
|
||||||
|
|
||||||
Any locations that are not populated by a face will have a frame and face index of -1
|
|
||||||
"""
|
|
||||||
labels = self._get_labels()
|
labels = self._get_labels()
|
||||||
if not self._is_valid:
|
if not self._is_valid:
|
||||||
logger.debug("Setting grid to None for no faces.")
|
logger.debug("Setting grid to None for no faces.")
|
||||||
self._grid = None
|
self._grid = None
|
||||||
return
|
return
|
||||||
|
assert labels is not None
|
||||||
x_coords = np.linspace(0,
|
x_coords = np.linspace(0,
|
||||||
labels.shape[2] * self._face_size,
|
labels.shape[2] * self._face_size,
|
||||||
num=labels.shape[2],
|
num=labels.shape[2],
|
||||||
|
|
@ -629,12 +671,12 @@ class Grid():
|
||||||
self._grid = np.array((*labels, *np.meshgrid(x_coords, y_coords)), dtype="int")
|
self._grid = np.array((*labels, *np.meshgrid(x_coords, y_coords)), dtype="int")
|
||||||
logger.debug(self._grid.shape)
|
logger.debug(self._grid.shape)
|
||||||
|
|
||||||
def _get_labels(self):
|
def _get_labels(self) -> np.ndarray | None:
|
||||||
""" Get the frame and face index for each grid position for the current filter.
|
""" Get the frame and face index for each grid position for the current filter.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
:class:`numpy.ndarray`
|
:class:`numpy.ndarray` | None
|
||||||
Array of dimensions (2, rows, columns) corresponding to the display grid, with frame
|
Array of dimensions (2, rows, columns) corresponding to the display grid, with frame
|
||||||
index as the first dimension and face index within the frame as the 2nd dimension.
|
index as the first dimension and face index within the frame as the 2nd dimension.
|
||||||
|
|
||||||
|
|
@ -657,17 +699,12 @@ class Grid():
|
||||||
return labels
|
return labels
|
||||||
|
|
||||||
def _get_display_faces(self):
|
def _get_display_faces(self):
|
||||||
""" Get the detected faces for the current filter and arrange to grid.
|
""" Get the detected faces for the current filter, arrange to grid and set to
|
||||||
|
:attr:`_display_faces`. This is an array of dimensions (rows, columns) corresponding to the
|
||||||
|
display grid, containing the corresponding :class:`lib.align.DetectFace` object
|
||||||
|
|
||||||
Returns
|
Any remaining placeholders at the end of the grid which are not populated with a face are
|
||||||
-------
|
replaced with ``None``"""
|
||||||
:class:`numpy.ndarray`
|
|
||||||
Array of dimensions (rows, columns) corresponding to the display grid, containing the
|
|
||||||
corresponding :class:`lib.align.DetectFace` object
|
|
||||||
|
|
||||||
Any remaining placeholders at the end of the grid which are not populated with a face
|
|
||||||
are replaced with ``None``
|
|
||||||
"""
|
|
||||||
if not self._is_valid:
|
if not self._is_valid:
|
||||||
logger.debug("Setting display_faces to None for no faces.")
|
logger.debug("Setting display_faces to None for no faces.")
|
||||||
self._display_faces = None
|
self._display_faces = None
|
||||||
|
|
@ -684,7 +721,7 @@ class Grid():
|
||||||
logger.debug("faces: (shape: %s, dtype: %s)",
|
logger.debug("faces: (shape: %s, dtype: %s)",
|
||||||
self._display_faces.shape, self._display_faces.dtype)
|
self._display_faces.shape, self._display_faces.dtype)
|
||||||
|
|
||||||
def transport_index_from_frame(self, frame_index):
|
def transport_index_from_frame(self, frame_index: int) -> int | None:
|
||||||
""" Return the main frame's transport index for the given frame index based on the current
|
""" Return the main frame's transport index for the given frame index based on the current
|
||||||
filter criteria.
|
filter criteria.
|
||||||
|
|
||||||
|
|
@ -695,11 +732,13 @@ class Grid():
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
int
|
int | None
|
||||||
The index of the requested frame within the filtered frames view.
|
The index of the requested frame within the filtered frames view. None if no valid
|
||||||
|
frames
|
||||||
"""
|
"""
|
||||||
retval = self._frames_list.index(frame_index) if frame_index in self._frames_list else None
|
retval = self._frames_list.index(frame_index) if frame_index in self._frames_list else None
|
||||||
logger.trace("frame_index: %s, transport_index: %s", frame_index, retval)
|
logger.trace("frame_index: %s, transport_index: %s", # type:ignore[attr-defined]
|
||||||
|
frame_index, retval)
|
||||||
return retval
|
return retval
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -737,17 +776,17 @@ class ContextMenu(): # pylint:disable=too-few-public-methods
|
||||||
frame_idx, face_idx = self._canvas.viewport.face_from_point(
|
frame_idx, face_idx = self._canvas.viewport.face_from_point(
|
||||||
self._canvas.canvasx(event.x), self._canvas.canvasy(event.y))[:2]
|
self._canvas.canvasx(event.x), self._canvas.canvasy(event.y))[:2]
|
||||||
if frame_idx == -1:
|
if frame_idx == -1:
|
||||||
logger.trace("No valid item under mouse")
|
logger.trace("No valid item under mouse") # type:ignore[attr-defined]
|
||||||
self._frame_index = self._face_index = None
|
self._frame_index = self._face_index = None
|
||||||
return
|
return
|
||||||
self._frame_index = frame_idx
|
self._frame_index = frame_idx
|
||||||
self._face_index = face_idx
|
self._face_index = face_idx
|
||||||
logger.trace("Popping right click menu")
|
logger.trace("Popping right click menu") # type:ignore[attr-defined]
|
||||||
self._menu.popup(event)
|
self._menu.popup(event)
|
||||||
|
|
||||||
def _delete_face(self):
|
def _delete_face(self):
|
||||||
""" Delete the selected face on a right click mouse delete action. """
|
""" Delete the selected face on a right click mouse delete action. """
|
||||||
logger.trace("Right click delete received. frame_id: %s, face_id: %s",
|
logger.trace("Right click delete received. frame_id: %s, " # type:ignore[attr-defined]
|
||||||
self._frame_index, self._face_index)
|
"face_id: %s", self._frame_index, self._face_index)
|
||||||
self._detected_faces.update.delete(self._frame_index, self._face_index)
|
self._detected_faces.update.delete(self._frame_index, self._face_index)
|
||||||
self._frame_index = self._face_index = None
|
self._frame_index = self._face_index = None
|
||||||
|
|
|
||||||
423
tools/manual/faceviewer/interact.py
Normal file
423
tools/manual/faceviewer/interact.py
Normal file
|
|
@ -0,0 +1,423 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
""" Handles the viewport area for mouse hover actions and the active frame """
|
||||||
|
from __future__ import annotations
|
||||||
|
import logging
|
||||||
|
import tkinter as tk
|
||||||
|
import typing as T
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from lib.logger import parse_class_init
|
||||||
|
|
||||||
|
if T.TYPE_CHECKING:
|
||||||
|
from lib.align import DetectedFace
|
||||||
|
from .viewport import Viewport
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class HoverBox():
|
||||||
|
""" Handle the current mouse location when over the :class:`Viewport`.
|
||||||
|
|
||||||
|
Highlights the face currently underneath the cursor and handles actions when clicking
|
||||||
|
on a face.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
viewport: :class:`Viewport`
|
||||||
|
The viewport object for the :class:`~tools.manual.faceviewer.frame.FacesViewer` canvas
|
||||||
|
"""
|
||||||
|
def __init__(self, viewport: Viewport) -> None:
|
||||||
|
logger.debug(parse_class_init(locals()))
|
||||||
|
self._viewport = viewport
|
||||||
|
self._canvas = viewport._canvas
|
||||||
|
self._grid = viewport._canvas.layout
|
||||||
|
self._globals = viewport._canvas._globals
|
||||||
|
self._navigation = viewport._canvas._display_frame.navigation
|
||||||
|
self._box = self._canvas.create_rectangle(0., # type:ignore[call-overload]
|
||||||
|
0.,
|
||||||
|
float(self._size),
|
||||||
|
float(self._size),
|
||||||
|
outline="#0000ff",
|
||||||
|
width=2,
|
||||||
|
state="hidden",
|
||||||
|
fill="#0000ff",
|
||||||
|
stipple="gray12",
|
||||||
|
tags="hover_box")
|
||||||
|
self._current_frame_index = None
|
||||||
|
self._current_face_index = None
|
||||||
|
self._canvas.bind("<Leave>", lambda e: self._clear())
|
||||||
|
self._canvas.bind("<Motion>", self.on_hover)
|
||||||
|
self._canvas.bind("<ButtonPress-1>", lambda e: self._select_frame())
|
||||||
|
logger.debug("Initialized: %s", self.__class__.__name__)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _size(self) -> int:
|
||||||
|
""" int: the currently set viewport face size in pixels. """
|
||||||
|
return self._viewport.face_size
|
||||||
|
|
||||||
|
def on_hover(self, event: tk.Event | None) -> None:
|
||||||
|
""" Highlight the face and set the mouse cursor for the mouse's current location.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
event: :class:`tkinter.Event` or ``None``
|
||||||
|
The tkinter mouse event. Provides the current location of the mouse cursor. If ``None``
|
||||||
|
is passed as the event (for example when this function is being called outside of a
|
||||||
|
mouse event) then the location of the cursor will be calculated
|
||||||
|
"""
|
||||||
|
if event is None:
|
||||||
|
pnts = np.array((self._canvas.winfo_pointerx(), self._canvas.winfo_pointery()))
|
||||||
|
pnts -= np.array((self._canvas.winfo_rootx(), self._canvas.winfo_rooty()))
|
||||||
|
else:
|
||||||
|
pnts = np.array((event.x, event.y))
|
||||||
|
|
||||||
|
coords = (int(self._canvas.canvasx(pnts[0])), int(self._canvas.canvasy(pnts[1])))
|
||||||
|
face = self._viewport.face_from_point(*coords)
|
||||||
|
frame_idx, face_idx = face[:2]
|
||||||
|
|
||||||
|
if frame_idx == self._current_frame_index and face_idx == self._current_face_index:
|
||||||
|
return
|
||||||
|
|
||||||
|
is_zoomed = self._globals.is_zoomed
|
||||||
|
if (-1 in face or (frame_idx == self._globals.frame_index
|
||||||
|
and (not is_zoomed or
|
||||||
|
(is_zoomed and face_idx == self._globals.tk_face_index.get())))):
|
||||||
|
self._clear()
|
||||||
|
self._canvas.config(cursor="")
|
||||||
|
self._current_frame_index = None
|
||||||
|
self._current_face_index = None
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.debug("Viewport hover: frame_idx: %s, face_idx: %s", frame_idx, face_idx)
|
||||||
|
|
||||||
|
self._canvas.config(cursor="hand2")
|
||||||
|
self._highlight(face[2:])
|
||||||
|
self._current_frame_index = frame_idx
|
||||||
|
self._current_face_index = face_idx
|
||||||
|
|
||||||
|
def _clear(self) -> None:
|
||||||
|
""" Hide the hover box when the mouse is not over a face. """
|
||||||
|
if self._canvas.itemcget(self._box, "state") != "hidden":
|
||||||
|
self._canvas.itemconfig(self._box, state="hidden")
|
||||||
|
|
||||||
|
def _highlight(self, top_left: np.ndarray) -> None:
|
||||||
|
""" Display the hover box around the face that the mouse is currently over.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
top_left: :class:`np.ndarray`
|
||||||
|
The top left point of the highlight box location
|
||||||
|
"""
|
||||||
|
coords = (*top_left, *[x + self._size for x in top_left])
|
||||||
|
self._canvas.coords(self._box, *coords)
|
||||||
|
self._canvas.itemconfig(self._box, state="normal")
|
||||||
|
self._canvas.tag_raise(self._box)
|
||||||
|
|
||||||
|
def _select_frame(self) -> None:
|
||||||
|
""" Select the face and the subsequent frame (in the editor view) when a face is clicked
|
||||||
|
on in the :class:`Viewport`. """
|
||||||
|
frame_id = self._current_frame_index
|
||||||
|
is_zoomed = self._globals.is_zoomed
|
||||||
|
logger.debug("Face clicked. Global frame index: %s, Current frame_id: %s, is_zoomed: %s",
|
||||||
|
self._globals.frame_index, frame_id, is_zoomed)
|
||||||
|
if frame_id is None or (frame_id == self._globals.frame_index and not is_zoomed):
|
||||||
|
return
|
||||||
|
face_idx = self._current_face_index if is_zoomed else 0
|
||||||
|
self._globals.tk_face_index.set(face_idx)
|
||||||
|
transport_id = self._grid.transport_index_from_frame(frame_id)
|
||||||
|
logger.trace("frame_index: %s, transport_id: %s, face_idx: %s",
|
||||||
|
frame_id, transport_id, face_idx)
|
||||||
|
if transport_id is None:
|
||||||
|
return
|
||||||
|
self._navigation.stop_playback()
|
||||||
|
self._globals.tk_transport_index.set(transport_id)
|
||||||
|
self._viewport.move_active_to_top()
|
||||||
|
self.on_hover(None)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Asset:
|
||||||
|
""" Holds all of the display assets identifiers for the active frame's face viewer objects
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
images: list[int]
|
||||||
|
Indices for a frame's tk image ids displayed in the active frame
|
||||||
|
meshes: list[dict[Literal["polygon", "line"], list[int]]]
|
||||||
|
Indices for a frame's tk line/polygon object ids displayed in the active frame
|
||||||
|
faces: list[:class:`~lib.align.detected_faces.DetectedFace`]
|
||||||
|
DetectedFace objects that exist in the current frame
|
||||||
|
boxes: list[int]
|
||||||
|
Indices for a frame's bounding box object ids displayed in the active frame
|
||||||
|
"""
|
||||||
|
images: list[int]
|
||||||
|
"""list[int]: Indices for a frame's tk image ids displayed in the active frame"""
|
||||||
|
meshes: list[dict[T.Literal["polygon", "line"], list[int]]]
|
||||||
|
"""list[dict[Literal["polygon", "line"], list[int]]]: Indices for a frame's tk line/polygon
|
||||||
|
object ids displayed in the active frame"""
|
||||||
|
faces: list[DetectedFace]
|
||||||
|
"""list[:class:`~lib.align.detected_faces.DetectedFace`]: DetectedFace objects that exist
|
||||||
|
in the current frame"""
|
||||||
|
boxes: list[int]
|
||||||
|
"""list[int]: Indices for a frame's bounding box object ids displayed in the active
|
||||||
|
frame"""
|
||||||
|
|
||||||
|
|
||||||
|
class ActiveFrame():
|
||||||
|
""" Handles the display of faces and annotations for the currently active frame.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
canvas: :class:`tkinter.Canvas`
|
||||||
|
The :class:`~tools.manual.faceviewer.frame.FacesViewer` canvas
|
||||||
|
tk_edited_variable: :class:`tkinter.BooleanVar`
|
||||||
|
The tkinter callback variable indicating that a face has been edited
|
||||||
|
"""
|
||||||
|
def __init__(self, viewport: Viewport, tk_edited_variable: tk.BooleanVar) -> None:
|
||||||
|
logger.debug(parse_class_init(locals()))
|
||||||
|
self._objects = viewport._objects
|
||||||
|
self._viewport = viewport
|
||||||
|
self._grid = viewport._grid
|
||||||
|
self._tk_faces = viewport._tk_faces
|
||||||
|
self._canvas = viewport._canvas
|
||||||
|
self._globals = viewport._canvas._globals
|
||||||
|
self._navigation = viewport._canvas._display_frame.navigation
|
||||||
|
self._last_execution: dict[T.Literal["frame_index", "size"],
|
||||||
|
int] = {"frame_index": -1, "size": viewport.face_size}
|
||||||
|
self._tk_vars: dict[T.Literal["selected_editor", "edited"],
|
||||||
|
tk.StringVar | tk.BooleanVar] = {
|
||||||
|
"selected_editor": self._canvas._display_frame.tk_selected_action,
|
||||||
|
"edited": tk_edited_variable}
|
||||||
|
self._assets: Asset = Asset([], [], [], [])
|
||||||
|
|
||||||
|
self._globals.tk_update_active_viewport.trace_add("write",
|
||||||
|
lambda *e: self._reload_callback())
|
||||||
|
tk_edited_variable.trace_add("write", lambda *e: self._update_on_edit())
|
||||||
|
logger.debug("Initialized: %s", self.__class__.__name__)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def frame_index(self) -> int:
|
||||||
|
""" int: The frame index of the currently displayed frame. """
|
||||||
|
return self._globals.frame_index
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_frame(self) -> np.ndarray:
|
||||||
|
""" :class:`numpy.ndarray`: A BGR version of the frame currently being displayed. """
|
||||||
|
return self._globals.current_frame["image"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _size(self) -> int:
|
||||||
|
""" int: The size of the thumbnails displayed in the viewport, in pixels. """
|
||||||
|
return self._viewport.face_size
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _optional_annotations(self) -> dict[T.Literal["mesh", "mask"], bool]:
|
||||||
|
""" dict[Literal["mesh", "mask"], bool]: The currently selected optional
|
||||||
|
annotations """
|
||||||
|
return self._canvas.optional_annotations
|
||||||
|
|
||||||
|
def _reload_callback(self) -> None:
|
||||||
|
""" If a frame has changed, triggering the variable, then update the active frame. Return
|
||||||
|
having done nothing if the variable is resetting. """
|
||||||
|
if self._globals.tk_update_active_viewport.get():
|
||||||
|
self.reload_annotations()
|
||||||
|
|
||||||
|
def reload_annotations(self) -> None:
|
||||||
|
""" Handles the reloading of annotations for the currently active faces.
|
||||||
|
|
||||||
|
Highlights the faces within the viewport of those faces that exist in the currently
|
||||||
|
displaying frame. Applies annotations based on the optional annotations and current
|
||||||
|
editor selections.
|
||||||
|
"""
|
||||||
|
logger.trace("Reloading annotations") # type:ignore[attr-defined]
|
||||||
|
if self._assets.images:
|
||||||
|
self._clear_previous()
|
||||||
|
|
||||||
|
self._set_active_objects()
|
||||||
|
self._check_active_in_view()
|
||||||
|
|
||||||
|
if not self._assets.images:
|
||||||
|
logger.trace("No active faces. Returning") # type:ignore[attr-defined]
|
||||||
|
self._last_execution["frame_index"] = self.frame_index
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._last_execution["frame_index"] != self.frame_index:
|
||||||
|
self.move_to_top()
|
||||||
|
self._create_new_boxes()
|
||||||
|
|
||||||
|
self._update_face()
|
||||||
|
self._canvas.tag_raise("active_highlighter")
|
||||||
|
self._globals.tk_update_active_viewport.set(False)
|
||||||
|
self._last_execution["frame_index"] = self.frame_index
|
||||||
|
|
||||||
|
def _clear_previous(self) -> None:
|
||||||
|
""" Reverts the previously selected annotations to their default state. """
|
||||||
|
logger.trace("Clearing previous active frame") # type:ignore[attr-defined]
|
||||||
|
self._canvas.itemconfig("active_highlighter", state="hidden")
|
||||||
|
|
||||||
|
for key in T.get_args(T.Literal["polygon", "line"]):
|
||||||
|
tag = f"active_mesh_{key}"
|
||||||
|
self._canvas.itemconfig(tag, **self._viewport.mesh_kwargs[key], width=1)
|
||||||
|
self._canvas.dtag(tag)
|
||||||
|
|
||||||
|
if self._viewport.selected_editor == "mask" and not self._optional_annotations["mask"]:
|
||||||
|
for name, tk_face in self._tk_faces.items():
|
||||||
|
if name.startswith(f"{self._last_execution['frame_index']}_"):
|
||||||
|
tk_face.update_mask(None)
|
||||||
|
|
||||||
|
def _set_active_objects(self) -> None:
|
||||||
|
""" Collect the objects that exist in the currently active frame from the main grid. """
|
||||||
|
if self._grid.is_valid:
|
||||||
|
rows, cols = np.where(self._objects.visible_grid[0] == self.frame_index)
|
||||||
|
logger.trace("Setting active objects: (rows: %s, " # type:ignore[attr-defined]
|
||||||
|
"columns: %s)", rows, cols)
|
||||||
|
self._assets.images = self._objects.images[rows, cols].tolist()
|
||||||
|
self._assets.meshes = self._objects.meshes[rows, cols].tolist()
|
||||||
|
self._assets.faces = self._objects.visible_faces[rows, cols].tolist()
|
||||||
|
else:
|
||||||
|
logger.trace("No valid grid. Clearing active objects") # type:ignore[attr-defined]
|
||||||
|
self._assets.images = []
|
||||||
|
self._assets.meshes = []
|
||||||
|
self._assets.faces = []
|
||||||
|
|
||||||
|
def _check_active_in_view(self) -> None:
|
||||||
|
""" If the frame has changed, there are faces in the frame, but they don't appear in the
|
||||||
|
viewport, then bring the active faces to the top of the viewport. """
|
||||||
|
if (not self._assets.images and
|
||||||
|
self._last_execution["frame_index"] != self.frame_index and
|
||||||
|
self._grid.frame_has_faces(self.frame_index)):
|
||||||
|
y_coord = self._grid.y_coord_from_frame(self.frame_index)
|
||||||
|
logger.trace("Active not in view. Moving to: %s", y_coord) # type:ignore[attr-defined]
|
||||||
|
self._canvas.yview_moveto(y_coord / self._canvas.bbox("backdrop")[3])
|
||||||
|
self._viewport.update()
|
||||||
|
|
||||||
|
def move_to_top(self) -> None:
|
||||||
|
""" Move the currently selected frame's faces to the top of the viewport if they are moving
|
||||||
|
off the bottom of the viewer. """
|
||||||
|
height = self._canvas.bbox("backdrop")[3]
|
||||||
|
bot = int(self._canvas.coords(self._assets.images[-1])[1] + self._size)
|
||||||
|
|
||||||
|
y_top, y_bot = (int(round(pnt * height)) for pnt in self._canvas.yview())
|
||||||
|
|
||||||
|
if y_top < bot < y_bot: # bottom face is still in fully visible area
|
||||||
|
logger.trace("Active faces in frame. Returning") # type:ignore[attr-defined]
|
||||||
|
return
|
||||||
|
|
||||||
|
top = int(self._canvas.coords(self._assets.images[0])[1])
|
||||||
|
if y_top == top:
|
||||||
|
logger.trace("Top face already on top row. Returning") # type:ignore[attr-defined]
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._canvas.winfo_height() > self._size:
|
||||||
|
logger.trace("Viewport taller than single face height. " # type:ignore[attr-defined]
|
||||||
|
"Moving Active faces to top: %s", top)
|
||||||
|
self._canvas.yview_moveto(top / height)
|
||||||
|
self._viewport.update()
|
||||||
|
elif self._canvas.winfo_height() <= self._size and y_top != top:
|
||||||
|
logger.trace("Viewport shorter than single face height. " # type:ignore[attr-defined]
|
||||||
|
"Moving Active faces to top: %s", top)
|
||||||
|
self._canvas.yview_moveto(top / height)
|
||||||
|
self._viewport.update()
|
||||||
|
|
||||||
|
def _create_new_boxes(self) -> None:
|
||||||
|
""" The highlight boxes (border around selected faces) are the only additional annotations
|
||||||
|
that are required for the highlighter. If more faces are displayed in the current frame
|
||||||
|
than highlight boxes are available, then new boxes are created to accommodate the
|
||||||
|
additional faces. """
|
||||||
|
new_boxes_count = max(0, len(self._assets.images) - len(self._assets.boxes))
|
||||||
|
if new_boxes_count == 0:
|
||||||
|
return
|
||||||
|
logger.debug("new_boxes_count: %s", new_boxes_count)
|
||||||
|
for _ in range(new_boxes_count):
|
||||||
|
box = self._canvas.create_rectangle(0., # type:ignore[call-overload]
|
||||||
|
0.,
|
||||||
|
float(self._viewport.face_size),
|
||||||
|
float(self._viewport.face_size),
|
||||||
|
outline="#00FF00",
|
||||||
|
width=2,
|
||||||
|
state="hidden",
|
||||||
|
tags=["active_highlighter"])
|
||||||
|
logger.trace("Created new highlight_box: %s", box) # type:ignore[attr-defined]
|
||||||
|
self._assets.boxes.append(box)
|
||||||
|
|
||||||
|
def _update_on_edit(self) -> None:
|
||||||
|
""" Update the active faces on a frame edit. """
|
||||||
|
if not self._tk_vars["edited"].get():
|
||||||
|
return
|
||||||
|
self._set_active_objects()
|
||||||
|
self._update_face()
|
||||||
|
assert isinstance(self._tk_vars["edited"], tk.BooleanVar)
|
||||||
|
self._tk_vars["edited"].set(False)
|
||||||
|
|
||||||
|
def _update_face(self) -> None:
|
||||||
|
""" Update the highlighted annotations for faces in the currently selected frame. """
|
||||||
|
for face_idx, (image_id, mesh_ids, box_id, det_face), in enumerate(
|
||||||
|
zip(self._assets.images,
|
||||||
|
self._assets.meshes,
|
||||||
|
self._assets.boxes,
|
||||||
|
self._assets.faces)):
|
||||||
|
if det_face is None:
|
||||||
|
continue
|
||||||
|
top_left = self._canvas.coords(image_id)
|
||||||
|
coords = [*top_left, *[x + self._size for x in top_left]]
|
||||||
|
tk_face = self._viewport.get_tk_face(self.frame_index, face_idx, det_face)
|
||||||
|
self._canvas.itemconfig(image_id, image=tk_face.photo)
|
||||||
|
self._show_box(box_id, coords)
|
||||||
|
self._show_mesh(mesh_ids, face_idx, det_face, top_left)
|
||||||
|
self._last_execution["size"] = self._viewport.face_size
|
||||||
|
|
||||||
|
def _show_box(self, item_id: int, coordinates: list[float]) -> None:
|
||||||
|
""" Display the highlight box around the given coordinates.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
item_id: int
|
||||||
|
The tkinter canvas object identifier for the highlight box
|
||||||
|
coordinates: list[float]
|
||||||
|
The (x, y, x1, y1) coordinates of the top left corner of the box
|
||||||
|
"""
|
||||||
|
self._canvas.coords(item_id, *coordinates)
|
||||||
|
self._canvas.itemconfig(item_id, state="normal")
|
||||||
|
|
||||||
|
def _show_mesh(self,
|
||||||
|
mesh_ids: dict[T.Literal["polygon", "line"], list[int]],
|
||||||
|
face_index: int,
|
||||||
|
detected_face: DetectedFace,
|
||||||
|
top_left: list[float]) -> None:
|
||||||
|
""" Display the mesh annotation for the given face, at the given location.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
mesh_ids: dict[Literal["polygon", "line"], list[int]]
|
||||||
|
Dictionary containing the `polygon` and `line` tkinter canvas identifiers that make up
|
||||||
|
the mesh for the given face
|
||||||
|
face_index: int
|
||||||
|
The face index within the frame for the given face
|
||||||
|
detected_face: :class:`~lib.align.DetectedFace`
|
||||||
|
The detected face object that contains the landmarks for generating the mesh
|
||||||
|
top_left: list[float]
|
||||||
|
The (x, y) top left co-ordinates of the mesh's bounding box
|
||||||
|
"""
|
||||||
|
state = "normal" if (self._tk_vars["selected_editor"].get() != "Mask" or
|
||||||
|
self._optional_annotations["mesh"]) else "hidden"
|
||||||
|
kwargs: dict[T.Literal["polygon", "line"], dict[str, T.Any]] = {
|
||||||
|
"polygon": {"fill": "", "width": 2, "outline": self._canvas.control_colors["Mesh"]},
|
||||||
|
"line": {"fill": self._canvas.control_colors["Mesh"], "width": 2}}
|
||||||
|
|
||||||
|
assert isinstance(self._tk_vars["edited"], tk.BooleanVar)
|
||||||
|
edited = (self._tk_vars["edited"].get() and
|
||||||
|
self._tk_vars["selected_editor"].get() not in ("Mask", "View"))
|
||||||
|
landmarks = self._viewport.get_landmarks(self.frame_index,
|
||||||
|
face_index,
|
||||||
|
detected_face,
|
||||||
|
top_left,
|
||||||
|
edited)
|
||||||
|
for key, kwarg in kwargs.items():
|
||||||
|
if key not in mesh_ids:
|
||||||
|
continue
|
||||||
|
for idx, mesh_id in enumerate(mesh_ids[key]):
|
||||||
|
self._canvas.coords(mesh_id, *landmarks[key][idx].flatten())
|
||||||
|
self._canvas.itemconfig(mesh_id, state=state, **kwarg)
|
||||||
|
self._canvas.addtag_withtag(f"active_mesh_{key}", mesh_id)
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -3,7 +3,7 @@
|
||||||
import gettext
|
import gettext
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from lib.align import AlignedFace
|
from lib.align import AlignedFace, LANDMARK_PARTS, LandmarkType
|
||||||
from ._base import Editor, logger
|
from ._base import Editor, logger
|
||||||
|
|
||||||
# LOCALES
|
# LOCALES
|
||||||
|
|
@ -67,7 +67,7 @@ class Landmarks(Editor):
|
||||||
outline="gray",
|
outline="gray",
|
||||||
state="hidden")
|
state="hidden")
|
||||||
self._canvas.coords(self._selection_box, 0, 0, 0, 0)
|
self._canvas.coords(self._selection_box, 0, 0, 0, 0)
|
||||||
self._drag_data = dict()
|
self._drag_data = {}
|
||||||
if event is not None:
|
if event is not None:
|
||||||
self._drag_start(event)
|
self._drag_start(event)
|
||||||
|
|
||||||
|
|
@ -83,7 +83,7 @@ class Landmarks(Editor):
|
||||||
landmarks = aligned.landmarks + zoomed_offset
|
landmarks = aligned.landmarks + zoomed_offset
|
||||||
# Hide all landmarks and only display selected
|
# Hide all landmarks and only display selected
|
||||||
self._canvas.itemconfig("lm_dsp", state="hidden")
|
self._canvas.itemconfig("lm_dsp", state="hidden")
|
||||||
self._canvas.itemconfig("lm_dsp_face_{}".format(face_index), state="normal")
|
self._canvas.itemconfig(f"lm_dsp_face_{face_index}", state="normal")
|
||||||
else:
|
else:
|
||||||
landmarks = self._scale_to_display(face.landmarks_xy)
|
landmarks = self._scale_to_display(face.landmarks_xy)
|
||||||
for lm_idx, landmark in enumerate(landmarks):
|
for lm_idx, landmark in enumerate(landmarks):
|
||||||
|
|
@ -109,8 +109,8 @@ class Landmarks(Editor):
|
||||||
color = self._control_color
|
color = self._control_color
|
||||||
bbox = (bounding_box[0] - radius, bounding_box[1] - radius,
|
bbox = (bounding_box[0] - radius, bounding_box[1] - radius,
|
||||||
bounding_box[0] + radius, bounding_box[1] + radius)
|
bounding_box[0] + radius, bounding_box[1] + radius)
|
||||||
key = "lm_dsp_{}".format(landmark_index)
|
key = f"lm_dsp_{landmark_index}"
|
||||||
kwargs = dict(outline=color, fill=color, width=radius)
|
kwargs = {"outline": color, "fill": color, "width": radius}
|
||||||
self._object_tracker(key, "oval", face_index, bbox, kwargs)
|
self._object_tracker(key, "oval", face_index, bbox, kwargs)
|
||||||
|
|
||||||
def _label_landmark(self, bounding_box, face_index, landmark_index):
|
def _label_landmark(self, bounding_box, face_index, landmark_index):
|
||||||
|
|
@ -132,9 +132,9 @@ class Landmarks(Editor):
|
||||||
# NB The text must be visible to be able to get the bounding box, so set to hidden
|
# NB The text must be visible to be able to get the bounding box, so set to hidden
|
||||||
# after the bounding box has been retrieved
|
# after the bounding box has been retrieved
|
||||||
|
|
||||||
keys = ["lm_lbl_{}".format(landmark_index), "lm_lbl_bg_{}".format(landmark_index)]
|
keys = [f"lm_lbl_{landmark_index}", f"lm_lbl_bg_{landmark_index}"]
|
||||||
text_kwargs = dict(fill="black", font=("Default", 10), text=str(landmark_index + 1))
|
text_kwargs = {"fill": "black", "font": ("Default", 10), "text": str(landmark_index + 1)}
|
||||||
bg_kwargs = dict(fill="#ffffea", outline="black")
|
bg_kwargs = {"fill": "#ffffea", "outline": "black"}
|
||||||
|
|
||||||
text_id = self._object_tracker(keys[0], "text", face_index, top_left, text_kwargs)
|
text_id = self._object_tracker(keys[0], "text", face_index, top_left, text_kwargs)
|
||||||
bbox = self._canvas.bbox(text_id)
|
bbox = self._canvas.bbox(text_id)
|
||||||
|
|
@ -162,11 +162,11 @@ class Landmarks(Editor):
|
||||||
radius = 7
|
radius = 7
|
||||||
bbox = (bounding_box[0] - radius, bounding_box[1] - radius,
|
bbox = (bounding_box[0] - radius, bounding_box[1] - radius,
|
||||||
bounding_box[0] + radius, bounding_box[1] + radius)
|
bounding_box[0] + radius, bounding_box[1] + radius)
|
||||||
key = "lm_grb_{}".format(landmark_index)
|
key = f"lm_grb_{landmark_index}"
|
||||||
kwargs = dict(outline="",
|
kwargs = {"outline": "",
|
||||||
fill="",
|
"fill": "",
|
||||||
width=1,
|
"width": 1,
|
||||||
dash=(2, 4))
|
"dash": (2, 4)}
|
||||||
self._object_tracker(key, "oval", face_index, bbox, kwargs)
|
self._object_tracker(key, "oval", face_index, bbox, kwargs)
|
||||||
|
|
||||||
# << MOUSE HANDLING >>
|
# << MOUSE HANDLING >>
|
||||||
|
|
@ -185,7 +185,7 @@ class Landmarks(Editor):
|
||||||
if self._drag_data:
|
if self._drag_data:
|
||||||
self._update_cursor_select_mode(event)
|
self._update_cursor_select_mode(event)
|
||||||
else:
|
else:
|
||||||
objs = self._canvas.find_withtag("lm_grb_face_{}".format(self._globals.face_index)
|
objs = self._canvas.find_withtag(f"lm_grb_face_{self._globals.face_index}"
|
||||||
if self._globals.is_zoomed else "lm_grb")
|
if self._globals.is_zoomed else "lm_grb")
|
||||||
item_ids = set(self._canvas.find_overlapping(event.x - 6,
|
item_ids = set(self._canvas.find_overlapping(event.x - 6,
|
||||||
event.y - 6,
|
event.y - 6,
|
||||||
|
|
@ -226,7 +226,7 @@ class Landmarks(Editor):
|
||||||
|
|
||||||
self._canvas.config(cursor="none")
|
self._canvas.config(cursor="none")
|
||||||
for prefix in ("lm_lbl_", "lm_lbl_bg_"):
|
for prefix in ("lm_lbl_", "lm_lbl_bg_"):
|
||||||
tag = "{}{}_face_{}".format(prefix, lm_idx, face_idx)
|
tag = f"{prefix}{lm_idx}_face_{face_idx}"
|
||||||
logger.trace("Displaying: %s tag: %s", self._canvas.type(tag), tag)
|
logger.trace("Displaying: %s tag: %s", self._canvas.type(tag), tag)
|
||||||
self._canvas.itemconfig(tag, state="normal")
|
self._canvas.itemconfig(tag, state="normal")
|
||||||
self._mouse_location = obj_idx
|
self._mouse_location = obj_idx
|
||||||
|
|
@ -271,7 +271,7 @@ class Landmarks(Editor):
|
||||||
self._drag_data["start_location"] = (event.x, event.y)
|
self._drag_data["start_location"] = (event.x, event.y)
|
||||||
self._drag_callback = self._move_selection
|
self._drag_callback = self._move_selection
|
||||||
else: # Reset
|
else: # Reset
|
||||||
self._drag_data = dict()
|
self._drag_data = {}
|
||||||
self._drag_callback = None
|
self._drag_callback = None
|
||||||
self._reset_selection(event)
|
self._reset_selection(event)
|
||||||
|
|
||||||
|
|
@ -294,7 +294,7 @@ class Landmarks(Editor):
|
||||||
self._det_faces.update.post_edit_trigger(self._globals.frame_index,
|
self._det_faces.update.post_edit_trigger(self._globals.frame_index,
|
||||||
self._mouse_location[0])
|
self._mouse_location[0])
|
||||||
self._mouse_location = None
|
self._mouse_location = None
|
||||||
self._drag_data = dict()
|
self._drag_data = {}
|
||||||
elif self._drag_data and self._drag_data.get("selected", False):
|
elif self._drag_data and self._drag_data.get("selected", False):
|
||||||
self._drag_stop_selected()
|
self._drag_stop_selected()
|
||||||
else:
|
else:
|
||||||
|
|
@ -429,15 +429,6 @@ class Mesh(Editor):
|
||||||
The _detected_faces data for this manual session
|
The _detected_faces data for this manual session
|
||||||
"""
|
"""
|
||||||
def __init__(self, canvas, detected_faces):
|
def __init__(self, canvas, detected_faces):
|
||||||
self._landmark_mapping = dict(mouth_inner=(60, 68),
|
|
||||||
mouth_outer=(48, 60),
|
|
||||||
right_eyebrow=(17, 22),
|
|
||||||
left_eyebrow=(22, 27),
|
|
||||||
right_eye=(36, 42),
|
|
||||||
left_eye=(42, 48),
|
|
||||||
nose=(27, 36),
|
|
||||||
jaw=(0, 17),
|
|
||||||
chin=(8, 11))
|
|
||||||
super().__init__(canvas, detected_faces, None)
|
super().__init__(canvas, detected_faces, None)
|
||||||
|
|
||||||
def update_annotation(self):
|
def update_annotation(self):
|
||||||
|
|
@ -452,19 +443,23 @@ class Mesh(Editor):
|
||||||
centering="face",
|
centering="face",
|
||||||
size=min(self._globals.frame_display_dims))
|
size=min(self._globals.frame_display_dims))
|
||||||
landmarks = aligned.landmarks + zoomed_offset
|
landmarks = aligned.landmarks + zoomed_offset
|
||||||
|
landmark_mapping = LANDMARK_PARTS[aligned.landmark_type]
|
||||||
# Hide all meshes and only display selected
|
# Hide all meshes and only display selected
|
||||||
self._canvas.itemconfig("Mesh", state="hidden")
|
self._canvas.itemconfig("Mesh", state="hidden")
|
||||||
self._canvas.itemconfig("Mesh_face_{}".format(face_index), state="normal")
|
self._canvas.itemconfig(f"Mesh_face_{face_index}", state="normal")
|
||||||
else:
|
else:
|
||||||
landmarks = self._scale_to_display(face.landmarks_xy)
|
landmarks = self._scale_to_display(face.landmarks_xy)
|
||||||
|
landmark_mapping = LANDMARK_PARTS[LandmarkType.from_shape(landmarks.shape)]
|
||||||
logger.trace("Drawing Landmarks Mesh: (landmarks: %s, color: %s)", landmarks, color)
|
logger.trace("Drawing Landmarks Mesh: (landmarks: %s, color: %s)", landmarks, color)
|
||||||
for idx, (segment, val) in enumerate(self._landmark_mapping.items()):
|
for idx, (start, end, fill) in enumerate(landmark_mapping.values()):
|
||||||
key = "mesh_{}".format(idx)
|
key = f"mesh_{idx}"
|
||||||
pts = landmarks[val[0]:val[1]].flatten()
|
pts = landmarks[start:end].flatten()
|
||||||
if segment in ("right_eye", "left_eye", "mouth_inner", "mouth_outer"):
|
if fill:
|
||||||
kwargs = dict(fill="", outline=color, width=1)
|
kwargs = {"fill": "", "outline": color, "width": 1}
|
||||||
self._object_tracker(key, "polygon", face_index, pts, kwargs)
|
asset = "polygon"
|
||||||
else:
|
else:
|
||||||
self._object_tracker(key, "line", face_index, pts, dict(fill=color, width=1))
|
kwargs = {"fill": color, "width": 1}
|
||||||
|
asset = "line"
|
||||||
|
self._object_tracker(key, asset, face_index, pts, kwargs)
|
||||||
# Place mesh as bottom annotation
|
# Place mesh as bottom annotation
|
||||||
self._canvas.tag_raise(self.__class__.__name__, "main_image")
|
self._canvas.tag_raise(self.__class__.__name__, "main_image")
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ class DisplayFrame(ttk.Frame): # pylint:disable=too-many-ancestors
|
||||||
|
|
||||||
self._globals = tk_globals
|
self._globals = tk_globals
|
||||||
self._det_faces = detected_faces
|
self._det_faces = detected_faces
|
||||||
self._optional_widgets = dict()
|
self._optional_widgets = {}
|
||||||
|
|
||||||
self._actions_frame = ActionsFrame(self)
|
self._actions_frame = ActionsFrame(self)
|
||||||
main_frame = ttk.Frame(self)
|
main_frame = ttk.Frame(self)
|
||||||
|
|
@ -74,28 +74,28 @@ class DisplayFrame(ttk.Frame): # pylint:disable=too-many-ancestors
|
||||||
@property
|
@property
|
||||||
def _helptext(self):
|
def _helptext(self):
|
||||||
""" dict: {`name`: `help text`} Helptext lookup for navigation buttons """
|
""" dict: {`name`: `help text`} Helptext lookup for navigation buttons """
|
||||||
return dict(
|
return {
|
||||||
play=_("Play/Pause (SPACE)"),
|
"play": _("Play/Pause (SPACE)"),
|
||||||
beginning=_("Go to First Frame (HOME)"),
|
"beginning": _("Go to First Frame (HOME)"),
|
||||||
prev=_("Go to Previous Frame (Z)"),
|
"prev": _("Go to Previous Frame (Z)"),
|
||||||
next=_("Go to Next Frame (X)"),
|
"next": _("Go to Next Frame (X)"),
|
||||||
end=_("Go to Last Frame (END)"),
|
"end": _("Go to Last Frame (END)"),
|
||||||
extract=_("Extract the faces to a folder... (Ctrl+E)"),
|
"extract": _("Extract the faces to a folder... (Ctrl+E)"),
|
||||||
save=_("Save the Alignments file (Ctrl+S)"),
|
"save": _("Save the Alignments file (Ctrl+S)"),
|
||||||
mode=_("Filter Frames to only those Containing the Selected Item (F)"),
|
"mode": _("Filter Frames to only those Containing the Selected Item (F)"),
|
||||||
distance=_("Set the distance from an 'average face' to be considered misaligned. "
|
"distance": _("Set the distance from an 'average face' to be considered misaligned. "
|
||||||
"Higher distances are more restrictive"))
|
"Higher distances are more restrictive")}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _btn_action(self):
|
def _btn_action(self):
|
||||||
""" dict: {`name`: `action`} Command lookup for navigation buttons """
|
""" dict: {`name`: `action`} Command lookup for navigation buttons """
|
||||||
actions = dict(play=self._navigation.handle_play_button,
|
actions = {"play": self._navigation.handle_play_button,
|
||||||
beginning=self._navigation.goto_first_frame,
|
"beginning": self._navigation.goto_first_frame,
|
||||||
prev=self._navigation.decrement_frame,
|
"prev": self._navigation.decrement_frame,
|
||||||
next=self._navigation.increment_frame,
|
"next": self._navigation.increment_frame,
|
||||||
end=self._navigation.goto_last_frame,
|
"end": self._navigation.goto_last_frame,
|
||||||
extract=self._det_faces.extract,
|
"extract": self._det_faces.extract,
|
||||||
save=self._det_faces.save)
|
"save": self._det_faces.save}
|
||||||
return actions
|
return actions
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -149,7 +149,7 @@ class DisplayFrame(ttk.Frame): # pylint:disable=too-many-ancestors
|
||||||
textvariable=self._globals.tk_transport_index,
|
textvariable=self._globals.tk_transport_index,
|
||||||
justify=tk.RIGHT)
|
justify=tk.RIGHT)
|
||||||
tbox.pack(padx=0, side=tk.LEFT)
|
tbox.pack(padx=0, side=tk.LEFT)
|
||||||
lbl = ttk.Label(lbl_frame, text="/{}".format(max_frame))
|
lbl = ttk.Label(lbl_frame, text=f"/{max_frame}")
|
||||||
lbl.pack(side=tk.RIGHT)
|
lbl.pack(side=tk.RIGHT)
|
||||||
|
|
||||||
cmd = partial(set_slider_rounding,
|
cmd = partial(set_slider_rounding,
|
||||||
|
|
@ -165,7 +165,7 @@ class DisplayFrame(ttk.Frame): # pylint:disable=too-many-ancestors
|
||||||
command=cmd)
|
command=cmd)
|
||||||
nav.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
nav.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
self._globals.tk_transport_index.trace("w", self._set_frame_index)
|
self._globals.tk_transport_index.trace("w", self._set_frame_index)
|
||||||
return dict(entry=tbox, scale=nav, label=lbl)
|
return {"entry": tbox, "scale": nav, "label": lbl}
|
||||||
|
|
||||||
def _set_frame_index(self, *args): # pylint:disable=unused-argument
|
def _set_frame_index(self, *args): # pylint:disable=unused-argument
|
||||||
""" Set the actual frame index based on current slider position and filter mode. """
|
""" Set the actual frame index based on current slider position and filter mode. """
|
||||||
|
|
@ -187,7 +187,7 @@ class DisplayFrame(ttk.Frame): # pylint:disable=too-many-ancestors
|
||||||
frame = ttk.Frame(self._transport_frame)
|
frame = ttk.Frame(self._transport_frame)
|
||||||
frame.pack(side=tk.BOTTOM, fill=tk.X)
|
frame.pack(side=tk.BOTTOM, fill=tk.X)
|
||||||
icons = get_images().icons
|
icons = get_images().icons
|
||||||
buttons = dict()
|
buttons = {}
|
||||||
for action in ("play", "beginning", "prev", "next", "end", "save", "extract", "mode"):
|
for action in ("play", "beginning", "prev", "next", "end", "save", "extract", "mode"):
|
||||||
padx = (0, 6) if action in ("play", "prev", "mode") else (0, 0)
|
padx = (0, 6) if action in ("play", "prev", "mode") else (0, 0)
|
||||||
side = tk.RIGHT if action in ("extract", "save", "mode") else tk.LEFT
|
side = tk.RIGHT if action in ("extract", "save", "mode") else tk.LEFT
|
||||||
|
|
@ -366,7 +366,7 @@ class ActionsFrame(ttk.Frame): # pylint:disable=too-many-ancestors
|
||||||
self._buttons = self._add_buttons()
|
self._buttons = self._add_buttons()
|
||||||
self._static_buttons = self._add_static_buttons()
|
self._static_buttons = self._add_static_buttons()
|
||||||
self._selected_action = self._set_selected_action_tkvar()
|
self._selected_action = self._set_selected_action_tkvar()
|
||||||
self._optional_buttons = dict() # Has to be set from parent after canvas is initialized
|
self._optional_buttons = {} # Has to be set from parent after canvas is initialized
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def actions(self):
|
def actions(self):
|
||||||
|
|
@ -382,19 +382,19 @@ class ActionsFrame(ttk.Frame): # pylint:disable=too-many-ancestors
|
||||||
def key_bindings(self):
|
def key_bindings(self):
|
||||||
""" dict: {`key`: `action`}. The mapping of key presses to actions. Keyboard shortcut is
|
""" dict: {`key`: `action`}. The mapping of key presses to actions. Keyboard shortcut is
|
||||||
the first letter of each action. """
|
the first letter of each action. """
|
||||||
return {"F{}".format(idx + 1): action for idx, action in enumerate(self._actions)}
|
return {f"F{idx + 1}": action for idx, action in enumerate(self._actions)}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _helptext(self):
|
def _helptext(self):
|
||||||
""" dict: `button key`: `button helptext`. The help text to display for each button. """
|
""" dict: `button key`: `button helptext`. The help text to display for each button. """
|
||||||
inverse_keybindings = {val: key for key, val in self.key_bindings.items()}
|
inverse_keybindings = {val: key for key, val in self.key_bindings.items()}
|
||||||
retval = dict(View=_("View alignments"),
|
retval = {"View": _('View alignments'),
|
||||||
BoundingBox=_("Bounding box editor"),
|
"BoundingBox": _('Bounding box editor'),
|
||||||
ExtractBox=_("Location editor"),
|
"ExtractBox": _("Location editor"),
|
||||||
Mask=_("Mask editor"),
|
"Mask": _("Mask editor"),
|
||||||
Landmarks=_("Landmark point editor"))
|
"Landmarks": _("Landmark point editor")}
|
||||||
for item in retval:
|
for item in retval:
|
||||||
retval[item] += " ({})".format(inverse_keybindings[item])
|
retval[item] += f" ({inverse_keybindings[item]})"
|
||||||
return retval
|
return retval
|
||||||
|
|
||||||
def _configure_styles(self):
|
def _configure_styles(self):
|
||||||
|
|
@ -415,7 +415,7 @@ class ActionsFrame(ttk.Frame): # pylint:disable=too-many-ancestors
|
||||||
"""
|
"""
|
||||||
frame = ttk.Frame(self)
|
frame = ttk.Frame(self)
|
||||||
frame.pack(side=tk.TOP, fill=tk.Y)
|
frame.pack(side=tk.TOP, fill=tk.Y)
|
||||||
buttons = dict()
|
buttons = {}
|
||||||
for action in self.key_bindings.values():
|
for action in self.key_bindings.values():
|
||||||
if action == self._initial_action:
|
if action == self._initial_action:
|
||||||
btn_style = "actions_selected.TButton"
|
btn_style = "actions_selected.TButton"
|
||||||
|
|
@ -467,22 +467,24 @@ class ActionsFrame(ttk.Frame): # pylint:disable=too-many-ancestors
|
||||||
|
|
||||||
def _add_static_buttons(self):
|
def _add_static_buttons(self):
|
||||||
""" Add the buttons to copy alignments from previous and next frames """
|
""" Add the buttons to copy alignments from previous and next frames """
|
||||||
lookup = dict(copy_prev=(_("Previous"), "C"), copy_next=(_("Next"), "V"), reload=("", "R"))
|
lookup = {"copy_prev": (_("Previous"), "C"),
|
||||||
|
"copy_next": (_("Next"), "V"),
|
||||||
|
"reload": ("", "R")}
|
||||||
frame = ttk.Frame(self)
|
frame = ttk.Frame(self)
|
||||||
frame.pack(side=tk.TOP, fill=tk.Y)
|
frame.pack(side=tk.TOP, fill=tk.Y)
|
||||||
sep = ttk.Frame(frame, height=2, relief=tk.RIDGE)
|
sep = ttk.Frame(frame, height=2, relief=tk.RIDGE)
|
||||||
sep.pack(fill=tk.X, pady=5, side=tk.TOP)
|
sep.pack(fill=tk.X, pady=5, side=tk.TOP)
|
||||||
buttons = dict()
|
buttons = {}
|
||||||
tk_frame_index = self._globals.tk_frame_index
|
tk_frame_index = self._globals.tk_frame_index
|
||||||
for action in ("copy_prev", "copy_next", "reload"):
|
for action in ("copy_prev", "copy_next", "reload"):
|
||||||
if action == "reload":
|
if action == "reload":
|
||||||
icon = "reload3"
|
icon = "reload3"
|
||||||
cmd = lambda f=tk_frame_index: self._det_faces.revert_to_saved(f.get()) # noqa
|
cmd = lambda f=tk_frame_index: self._det_faces.revert_to_saved(f.get()) # noqa=E731 # pylint:disable=line-too-long,unnecessary-lambda-assignment
|
||||||
helptext = _("Revert to saved Alignments ({})").format(lookup[action][1])
|
helptext = _("Revert to saved Alignments ({})").format(lookup[action][1])
|
||||||
else:
|
else:
|
||||||
icon = action
|
icon = action
|
||||||
direction = action.replace("copy_", "")
|
direction = action.replace("copy_", "")
|
||||||
cmd = lambda f=tk_frame_index, d=direction: self._det_faces.update.copy( # noqa
|
cmd = lambda f=tk_frame_index, d=direction: self._det_faces.update.copy( # noqa=E731 # pylint:disable=line-too-long,unnecessary-lambda-assignment
|
||||||
f.get(), d)
|
f.get(), d)
|
||||||
helptext = _("Copy {} Alignments ({})").format(*lookup[action])
|
helptext = _("Copy {} Alignments ({})").format(*lookup[action])
|
||||||
state = ["!disabled"] if action == "copy_next" else ["disabled"]
|
state = ["!disabled"] if action == "copy_next" else ["disabled"]
|
||||||
|
|
@ -506,10 +508,10 @@ class ActionsFrame(ttk.Frame): # pylint:disable=too-many-ancestors
|
||||||
for count in face_count_per_index[:position])
|
for count in face_count_per_index[:position])
|
||||||
next_exists = position != -1 and any(count != 0
|
next_exists = position != -1 and any(count != 0
|
||||||
for count in face_count_per_index[position + 1:])
|
for count in face_count_per_index[position + 1:])
|
||||||
states = dict(prev=["!disabled"] if prev_exists else ["disabled"],
|
states = {"prev": ["!disabled"] if prev_exists else ["disabled"],
|
||||||
next=["!disabled"] if next_exists else ["disabled"])
|
"next": ["!disabled"] if next_exists else ["disabled"]}
|
||||||
for direction in ("prev", "next"):
|
for direction in ("prev", "next"):
|
||||||
self._static_buttons["copy_{}".format(direction)].state(states[direction])
|
self._static_buttons[f"copy_{direction}"].state(states[direction])
|
||||||
|
|
||||||
def _disable_enable_reload_button(self, *args): # pylint:disable=unused-argument
|
def _disable_enable_reload_button(self, *args): # pylint:disable=unused-argument
|
||||||
""" Disable or enable the static buttons """
|
""" Disable or enable the static buttons """
|
||||||
|
|
@ -549,12 +551,12 @@ class ActionsFrame(ttk.Frame): # pylint:disable=too-many-ancestors
|
||||||
|
|
||||||
helptext = action["helptext"]
|
helptext = action["helptext"]
|
||||||
hotkey = action["hotkey"]
|
hotkey = action["hotkey"]
|
||||||
helptext += "" if hotkey is None else " ({})".format(hotkey.upper())
|
helptext += "" if hotkey is None else f" ({hotkey.upper()})"
|
||||||
Tooltip(button, text=helptext)
|
Tooltip(button, text=helptext)
|
||||||
self._optional_buttons.setdefault(
|
self._optional_buttons.setdefault(
|
||||||
name, dict())[button] = dict(hotkey=hotkey,
|
name, {})[button] = {"hotkey": hotkey,
|
||||||
group=group,
|
"group": group,
|
||||||
tk_var=action["tk_var"])
|
"tk_var": action["tk_var"]}
|
||||||
self._optional_buttons[name]["frame"] = frame
|
self._optional_buttons[name]["frame"] = frame
|
||||||
self._display_optional_buttons()
|
self._display_optional_buttons()
|
||||||
|
|
||||||
|
|
@ -652,9 +654,9 @@ class FrameViewer(tk.Canvas): # pylint:disable=too-many-ancestors
|
||||||
self._actions = actions
|
self._actions = actions
|
||||||
self._tk_action_var = tk_action_var
|
self._tk_action_var = tk_action_var
|
||||||
self._image = BackgroundImage(self)
|
self._image = BackgroundImage(self)
|
||||||
self._editor_globals = dict(control_tk_vars=dict(),
|
self._editor_globals = {"control_tk_vars": {},
|
||||||
annotation_formats=dict(),
|
"annotation_formats": {},
|
||||||
key_bindings=dict())
|
"key_bindings": {}}
|
||||||
self._max_face_count = 0
|
self._max_face_count = 0
|
||||||
self._editors = self._get_editors()
|
self._editors = self._get_editors()
|
||||||
self._add_callbacks()
|
self._add_callbacks()
|
||||||
|
|
@ -695,11 +697,11 @@ class FrameViewer(tk.Canvas): # pylint:disable=too-many-ancestors
|
||||||
@property
|
@property
|
||||||
def editor_display(self):
|
def editor_display(self):
|
||||||
""" dict: List of editors and any additional annotations they should display. """
|
""" dict: List of editors and any additional annotations they should display. """
|
||||||
return dict(View=["BoundingBox", "ExtractBox", "Landmarks", "Mesh"],
|
return {"View": ["BoundingBox", "ExtractBox", "Landmarks", "Mesh"],
|
||||||
BoundingBox=["Mesh"],
|
"BoundingBox": ["Mesh"],
|
||||||
ExtractBox=["Mesh"],
|
"ExtractBox": ["Mesh"],
|
||||||
Landmarks=["ExtractBox", "Mesh"],
|
"Landmarks": ["ExtractBox", "Mesh"],
|
||||||
Mask=[])
|
"Mask": []}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def offset(self):
|
def offset(self):
|
||||||
|
|
@ -719,7 +721,7 @@ class FrameViewer(tk.Canvas): # pylint:disable=too-many-ancestors
|
||||||
dict
|
dict
|
||||||
The {`action`: :class:`Editor`} dictionary of editors for :attr:`_actions` name.
|
The {`action`: :class:`Editor`} dictionary of editors for :attr:`_actions` name.
|
||||||
"""
|
"""
|
||||||
editors = dict()
|
editors = {}
|
||||||
for editor_name in self._actions + ("Mesh", ):
|
for editor_name in self._actions + ("Mesh", ):
|
||||||
editor = eval(editor_name)(self, # pylint:disable=eval-used
|
editor = eval(editor_name)(self, # pylint:disable=eval-used
|
||||||
self._det_faces)
|
self._det_faces)
|
||||||
|
|
@ -797,7 +799,7 @@ class FrameViewer(tk.Canvas): # pylint:disable=too-many-ancestors
|
||||||
self._max_face_count = current_face_count
|
self._max_face_count = current_face_count
|
||||||
return
|
return
|
||||||
for idx in range(current_face_count, self._max_face_count):
|
for idx in range(current_face_count, self._max_face_count):
|
||||||
tag = "face_{}".format(idx)
|
tag = f"face_{idx}"
|
||||||
if any(self.itemcget(item_id, "state") != "hidden"
|
if any(self.itemcget(item_id, "state") != "hidden"
|
||||||
for item_id in self.find_withtag(tag)):
|
for item_id in self.find_withtag(tag)):
|
||||||
logger.debug("Hiding face tag '%s'", tag)
|
logger.debug("Hiding face tag '%s'", tag)
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ from lib.gui.utils import get_images, get_config, initialize_config, initialize_
|
||||||
from lib.image import SingleFrameLoader, read_image_meta
|
from lib.image import SingleFrameLoader, read_image_meta
|
||||||
from lib.multithreading import MultiThread
|
from lib.multithreading import MultiThread
|
||||||
from lib.utils import handle_deprecated_cliopts, VIDEO_EXTENSIONS
|
from lib.utils import handle_deprecated_cliopts, VIDEO_EXTENSIONS
|
||||||
from plugins.extract.pipeline import Extractor, ExtractMedia
|
from plugins.extract import ExtractMedia, Extractor
|
||||||
|
|
||||||
from .detected_faces import DetectedFaces
|
from .detected_faces import DetectedFaces
|
||||||
from .faceviewer.frame import FacesFrame
|
from .faceviewer.frame import FacesFrame
|
||||||
|
|
@ -678,8 +678,8 @@ class Aligner():
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _feed_face(self) -> ExtractMedia:
|
def _feed_face(self) -> ExtractMedia:
|
||||||
""" :class:`plugins.extract.pipeline.ExtractMedia`: The current face for feeding into the
|
""" :class:`~plugins.extract.extract_media.ExtractMedia`: The current face for feeding into
|
||||||
aligner, formatted for the pipeline """
|
the aligner, formatted for the pipeline """
|
||||||
assert self._frame_index is not None
|
assert self._frame_index is not None
|
||||||
assert self._face_index is not None
|
assert self._face_index is not None
|
||||||
assert self._detected_faces is not None
|
assert self._detected_faces is not None
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ from tqdm import tqdm
|
||||||
from lib.align import DetectedFace, update_legacy_png_header
|
from lib.align import DetectedFace, update_legacy_png_header
|
||||||
from lib.align.alignments import AlignmentFileDict
|
from lib.align.alignments import AlignmentFileDict
|
||||||
from lib.image import FacesLoader, ImagesLoader
|
from lib.image import FacesLoader, ImagesLoader
|
||||||
from plugins.extract.pipeline import ExtractMedia
|
from plugins.extract import ExtractMedia
|
||||||
|
|
||||||
if T.TYPE_CHECKING:
|
if T.TYPE_CHECKING:
|
||||||
from lib.align import Alignments
|
from lib.align import Alignments
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ from multiprocessing import Process
|
||||||
from lib.align import Alignments
|
from lib.align import Alignments
|
||||||
|
|
||||||
from lib.utils import handle_deprecated_cliopts, VIDEO_EXTENSIONS
|
from lib.utils import handle_deprecated_cliopts, VIDEO_EXTENSIONS
|
||||||
from plugins.extract.pipeline import ExtractMedia
|
from plugins.extract import ExtractMedia
|
||||||
|
|
||||||
from .loader import Loader
|
from .loader import Loader
|
||||||
from .mask_import import Import
|
from .mask_import import Import
|
||||||
|
|
@ -239,7 +239,7 @@ class _Mask:
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
media: :class:`~plugins.extract.pipeline.ExtractMedia`
|
media: :class:`~plugins.extract.extract_media.ExtractMedia`
|
||||||
The extract media holding the faces to output
|
The extract media holding the faces to output
|
||||||
"""
|
"""
|
||||||
filename = os.path.basename(media.frame_metadata["source_filename"]
|
filename = os.path.basename(media.frame_metadata["source_filename"]
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,13 @@ import typing as T
|
||||||
|
|
||||||
from lib.image import encode_image, ImagesSaver
|
from lib.image import encode_image, ImagesSaver
|
||||||
from lib.multithreading import MultiThread
|
from lib.multithreading import MultiThread
|
||||||
from plugins.extract.pipeline import Extractor
|
from plugins.extract import Extractor
|
||||||
|
|
||||||
if T.TYPE_CHECKING:
|
if T.TYPE_CHECKING:
|
||||||
from lib.align import Alignments, DetectedFace
|
from lib.align import Alignments, DetectedFace
|
||||||
from lib.align.alignments import PNGHeaderDict
|
from lib.align.alignments import PNGHeaderDict
|
||||||
from lib.queue_manager import EventQueue
|
from lib.queue_manager import EventQueue
|
||||||
from plugins.extract.pipeline import ExtractMedia
|
from plugins.extract import ExtractMedia
|
||||||
from .loader import Loader
|
from .loader import Loader
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ from lib.utils import get_image_paths
|
||||||
if T.TYPE_CHECKING:
|
if T.TYPE_CHECKING:
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from .loader import Loader
|
from .loader import Loader
|
||||||
from plugins.extract.pipeline import ExtractMedia
|
from plugins.extract import ExtractMedia
|
||||||
from lib.align import Alignments, DetectedFace
|
from lib.align import Alignments, DetectedFace
|
||||||
from lib.align.alignments import PNGHeaderDict
|
from lib.align.alignments import PNGHeaderDict
|
||||||
from lib.align.aligned_face import CenteringType
|
from lib.align.aligned_face import CenteringType
|
||||||
|
|
@ -306,7 +306,7 @@ class Import:
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
media: :class:`~plugins.extract.pipeline.ExtractMedia`
|
media: :class:`~plugins.extract.extract_media.ExtractMedia`
|
||||||
The extract media object containing the face(s) to import the mask for
|
The extract media object containing the face(s) to import the mask for
|
||||||
|
|
||||||
mask: :class:`numpy.ndarray`
|
mask: :class:`numpy.ndarray`
|
||||||
|
|
@ -361,7 +361,7 @@ class Import:
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
media: :class:`~plugins.extract.pipeline.ExtractMedia`
|
media: :class:`~plugins.extract.extract_media.ExtractMedia`
|
||||||
The extract media object containing the face(s) to import the mask for
|
The extract media object containing the face(s) to import the mask for
|
||||||
|
|
||||||
mask: :class:`numpy.ndarray`
|
mask: :class:`numpy.ndarray`
|
||||||
|
|
@ -384,7 +384,7 @@ class Import:
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
media: :class:`~plugins.extract.pipeline.ExtractMedia`
|
media: :class:`~plugins.extract.extract_media.ExtractMedia`
|
||||||
The extract media object containing the face(s) to import the mask for
|
The extract media object containing the face(s) to import the mask for
|
||||||
"""
|
"""
|
||||||
mask_file = self._mapping.get(os.path.basename(media.filename))
|
mask_file = self._mapping.get(os.path.basename(media.filename))
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ from lib.queue_manager import queue_manager
|
||||||
from scripts.fsmedia import Alignments, Images
|
from scripts.fsmedia import Alignments, Images
|
||||||
from scripts.convert import Predict, ConvertItem
|
from scripts.convert import Predict, ConvertItem
|
||||||
|
|
||||||
from plugins.extract.pipeline import ExtractMedia
|
from plugins.extract import ExtractMedia
|
||||||
|
|
||||||
from .control_panels import ActionFrame, ConfigTools, OptionsBook
|
from .control_panels import ActionFrame, ConfigTools, OptionsBook
|
||||||
from .viewer import FacesDisplay, ImagesCanvas
|
from .viewer import FacesDisplay, ImagesCanvas
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
from lib.align import AlignedFace, DetectedFace
|
from lib.align import AlignedFace, DetectedFace, LandmarkType
|
||||||
from lib.image import FacesLoader, ImagesLoader, read_image_meta_batch, update_existing_metadata
|
from lib.image import FacesLoader, ImagesLoader, read_image_meta_batch, update_existing_metadata
|
||||||
from lib.utils import FaceswapError
|
from lib.utils import FaceswapError
|
||||||
from plugins.extract.recognition.vgg_face2 import Cluster, Recognition as VGGFace
|
from plugins.extract.recognition.vgg_face2 import Cluster, Recognition as VGGFace
|
||||||
|
|
@ -217,6 +217,8 @@ class SortMethod():
|
||||||
Set to ``True`` if this class is going to be called exclusively for binning.
|
Set to ``True`` if this class is going to be called exclusively for binning.
|
||||||
Default: ``False``
|
Default: ``False``
|
||||||
"""
|
"""
|
||||||
|
_log_mask_once = False
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
arguments: Namespace,
|
arguments: Namespace,
|
||||||
loader_type: T.Literal["face", "meta", "all"] = "meta",
|
loader_type: T.Literal["face", "meta", "all"] = "meta",
|
||||||
|
|
@ -454,12 +456,22 @@ class SortMethod():
|
||||||
centering="legacy",
|
centering="legacy",
|
||||||
size=256,
|
size=256,
|
||||||
is_aligned=True)
|
is_aligned=True)
|
||||||
mask = det_face.mask["components"]
|
assert aln_face.face is not None
|
||||||
|
|
||||||
|
mask = det_face.mask.get("components", det_face.mask.get("extended", None))
|
||||||
|
|
||||||
|
if mask is None and not cls._log_mask_once:
|
||||||
|
logger.warning("No masks are available for masking the data. Results are likely to be "
|
||||||
|
"sub-standard")
|
||||||
|
cls._log_mask_once = True
|
||||||
|
|
||||||
|
if mask is None:
|
||||||
|
return aln_face.face
|
||||||
|
|
||||||
mask.set_sub_crop(aln_face.pose.offset[mask.stored_centering],
|
mask.set_sub_crop(aln_face.pose.offset[mask.stored_centering],
|
||||||
aln_face.pose.offset["legacy"],
|
aln_face.pose.offset["legacy"],
|
||||||
centering="legacy")
|
centering="legacy")
|
||||||
nmask = cv2.resize(mask.mask, (256, 256), interpolation=cv2.INTER_CUBIC)[..., None]
|
nmask = cv2.resize(mask.mask, (256, 256), interpolation=cv2.INTER_CUBIC)[..., None]
|
||||||
assert aln_face.face is not None
|
|
||||||
return np.minimum(aln_face.face, nmask)
|
return np.minimum(aln_face.face, nmask)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -832,6 +844,11 @@ class SortFace(SortMethod):
|
||||||
Set to ``True`` if this class is going to be called exclusively for binning.
|
Set to ``True`` if this class is going to be called exclusively for binning.
|
||||||
Default: ``False``
|
Default: ``False``
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_logged_lm_count_once = False
|
||||||
|
_warning = ("Extracted faces do not contain facial landmark data. Results sorted by this "
|
||||||
|
"method are likely to be sub-standard.")
|
||||||
|
|
||||||
def __init__(self, arguments: Namespace, is_group: bool = False) -> None:
|
def __init__(self, arguments: Namespace, is_group: bool = False) -> None:
|
||||||
super().__init__(arguments, loader_type="all", is_group=is_group)
|
super().__init__(arguments, loader_type="all", is_group=is_group)
|
||||||
self._vgg_face = VGGFace(exclude_gpus=arguments.exclude_gpus)
|
self._vgg_face = VGGFace(exclude_gpus=arguments.exclude_gpus)
|
||||||
|
|
@ -872,6 +889,11 @@ class SortFace(SortMethod):
|
||||||
|
|
||||||
if alignments.get("identity", {}).get("vggface2"):
|
if alignments.get("identity", {}).get("vggface2"):
|
||||||
embedding = np.array(alignments["identity"]["vggface2"], dtype="float32")
|
embedding = np.array(alignments["identity"]["vggface2"], dtype="float32")
|
||||||
|
|
||||||
|
if not self._logged_lm_count_once and len(alignments["landmarks_xy"]) == 4:
|
||||||
|
logger.warning(self._warning)
|
||||||
|
self._logged_lm_count_once = True
|
||||||
|
|
||||||
self._result.append((filename, embedding))
|
self._result.append((filename, embedding))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -880,11 +902,17 @@ class SortFace(SortMethod):
|
||||||
"Sorting by this method will be quicker next time")
|
"Sorting by this method will be quicker next time")
|
||||||
self._output_update_info = False
|
self._output_update_info = False
|
||||||
|
|
||||||
face = AlignedFace(np.array(alignments["landmarks_xy"], dtype="float32"),
|
a_face = AlignedFace(np.array(alignments["landmarks_xy"], dtype="float32"),
|
||||||
image=image,
|
image=image,
|
||||||
centering="legacy",
|
centering="legacy",
|
||||||
size=self._vgg_face.input_size,
|
size=self._vgg_face.input_size,
|
||||||
is_aligned=True).face
|
is_aligned=True)
|
||||||
|
|
||||||
|
if a_face.landmark_type == LandmarkType.LM_2D_4 and not self._logged_lm_count_once:
|
||||||
|
logger.warning(self._warning)
|
||||||
|
self._logged_lm_count_once = True
|
||||||
|
|
||||||
|
face = a_face.face
|
||||||
assert face is not None
|
assert face is not None
|
||||||
embedding = self._vgg_face.predict(face[None, ...])[0]
|
embedding = self._vgg_face.predict(face[None, ...])[0]
|
||||||
alignments.setdefault("identity", {})["vggface2"] = embedding.tolist()
|
alignments.setdefault("identity", {})["vggface2"] = embedding.tolist()
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import typing as T
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
from lib.align import AlignedFace
|
from lib.align import AlignedFace, LandmarkType
|
||||||
from lib.utils import FaceswapError
|
from lib.utils import FaceswapError
|
||||||
from .sort_methods import SortMethod
|
from .sort_methods import SortMethod
|
||||||
|
|
||||||
|
|
@ -36,6 +36,9 @@ class SortAlignedMetric(SortMethod):
|
||||||
Set to ``True`` if this class is going to be called exclusively for binning.
|
Set to ``True`` if this class is going to be called exclusively for binning.
|
||||||
Default: ``False``
|
Default: ``False``
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_logged_lm_count_once: bool = False
|
||||||
|
|
||||||
def _get_metric(self, aligned_face: AlignedFace) -> np.ndarray | float:
|
def _get_metric(self, aligned_face: AlignedFace) -> np.ndarray | float:
|
||||||
""" Obtain the correct metric for the given sort method"
|
""" Obtain the correct metric for the given sort method"
|
||||||
|
|
||||||
|
|
@ -85,6 +88,12 @@ class SortAlignedMetric(SortMethod):
|
||||||
raise FaceswapError(msg)
|
raise FaceswapError(msg)
|
||||||
|
|
||||||
face = AlignedFace(np.array(alignments["landmarks_xy"], dtype="float32"))
|
face = AlignedFace(np.array(alignments["landmarks_xy"], dtype="float32"))
|
||||||
|
if (not self._logged_lm_count_once
|
||||||
|
and face.landmark_type == LandmarkType.LM_2D_4
|
||||||
|
and self.__class__.__name__ != "SortSize"):
|
||||||
|
logger.warning("You have selected to sort by an aligned metric, but at least one face "
|
||||||
|
"does not contain facial landmark data. This probably won't work")
|
||||||
|
self._logged_lm_count_once = True
|
||||||
self._result.append((filename, self._get_metric(face)))
|
self._result.append((filename, self._get_metric(face)))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user