Skip to content

Commit 83fdd5f

Browse files
authored
Train: Migrate data loading to Torch (#1540)
1 parent 453ef58 commit 83fdd5f

48 files changed

Lines changed: 3304 additions & 4525 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/pytest.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,12 @@ jobs:
8585
# These backends will fail as GPU drivers not available
8686
if: matrix.backend == 'cpu'
8787
run: |
88-
KERAS_BACKEND=torch FACESWAP_BACKEND="${{ matrix.backend }}" py.test -v tests/;
88+
KERAS_BACKEND=torch KERAS_TORCH_DEVICE=CPU FACESWAP_BACKEND="${{ matrix.backend }}" py.test -v tests/;
8989
- name: End to End Tests
9090
# These backends will fail as GPU drivers not available
9191
if: matrix.backend == 'cpu'
9292
run: |
93-
KERAS_BACKEND=torch FACESWAP_BACKEND="${{ matrix.backend }}" python tests/simple_tests.py;
93+
KERAS_BACKEND=torch KERAS_TORCH_DEVICE=CPU FACESWAP_BACKEND="${{ matrix.backend }}" python tests/simple_tests.py;
9494
9595
build_linux:
9696
name: "pip (ubuntu-latest, ${{ matrix.backend }} ${{ matrix.python-version }})"
@@ -132,10 +132,10 @@ jobs:
132132
run: FACESWAP_BACKEND="${{ matrix.backend }}" python -m lib.system.sysinfo
133133
- name: Unit Tests
134134
run: |
135-
KERAS_BACKEND=torch FACESWAP_BACKEND="${{ matrix.backend }}" py.test -v tests/;
135+
KERAS_BACKEND=torch KERAS_TORCH_DEVICE=CPU FACESWAP_BACKEND="${{ matrix.backend }}" py.test -v tests/;
136136
- name: End to End Tests
137137
run: |
138-
KERAS_BACKEND=torch FACESWAP_BACKEND="${{ matrix.backend }}" python tests/simple_tests.py;
138+
KERAS_BACKEND=torch KERAS_TORCH_DEVICE=CPU FACESWAP_BACKEND="${{ matrix.backend }}" python tests/simple_tests.py;
139139
140140
build_windows:
141141
name: "pip (windows-latest, ${{ matrix.backend }} ${{ matrix.python-version }})"

docs/full/lib/training.rst

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,16 @@ The training Package handles libraries to assist with training a model
88
:local:
99
:depth: 2
1010

11-
.. automodapi:: lib.training.augmentation
11+
.. automodapi:: lib.training.data_augmentation
1212
:include-all-objects:
1313
:no-inheritance-diagram:
1414

1515
|
16-
.. automodapi:: lib.training.cache
16+
.. automodapi:: lib.training.data_loader
1717
:include-all-objects:
18-
:no-inheritance-diagram:
1918

2019
|
21-
.. automodapi:: lib.training.generator
20+
.. automodapi:: lib.training.data_set
2221
:include-all-objects:
2322

2423
|
@@ -30,6 +29,11 @@ The training Package handles libraries to assist with training a model
3029
:include-all-objects:
3130
:no-inheritance-diagram:
3231

32+
|
33+
.. automodapi:: lib.training.preview
34+
:include-all-objects:
35+
:no-inheritance-diagram:
36+
3337
|
3438
.. automodapi:: lib.training.preview_cv
3539
:include-all-objects:
@@ -41,3 +45,7 @@ The training Package handles libraries to assist with training a model
4145
|
4246
.. automodapi:: lib.training.tensorboard
4347
:include-all-objects:
48+
49+
|
50+
.. automodapi:: lib.training.train
51+
:include-all-objects:

docs/full/plugins/train.rst

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,7 @@ trainer package
5050

5151
This package contains the training loop for Faceswap
5252

53-
.. automodapi:: plugins.train.trainer._base
54-
:include-all-objects:
55-
:no-inheritance-diagram:
56-
57-
|
58-
.. automodapi:: plugins.train.trainer._display
53+
.. automodapi:: plugins.train.trainer.base
5954
:include-all-objects:
6055
:no-inheritance-diagram:
6156

lib/align/aligned_face.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
from .aligned_mask import LandmarksMask
2121
from .pose import PoseEstimate
2222

23+
if T.TYPE_CHECKING:
24+
import numpy.typing as npt
2325

2426
logger = logging.getLogger(__name__)
2527

@@ -476,7 +478,10 @@ def split_mask(self) -> np.ndarray:
476478

477479
def get_landmark_mask(self,
478480
area: T.Literal["eye", "mouth", "face", "face_extended"],
479-
dilation: float) -> LandmarksMask:
481+
dilation: float = 0,
482+
blur_kernel: int = 0,
483+
blur_type: T.Literal["gaussian", "normalized"] | None = "gaussian",
484+
blur_passes: int = 1) -> npt.NDArray[np.uint8]:
480485
"""Obtain a :class:`~lib.align.aligned_mask.LandmarksMask` based mask for this face
481486
482487
Landmark based masks are generated from Aligned Face landmark points.
@@ -487,21 +492,31 @@ def get_landmark_mask(self,
487492
The type of mask to obtain. `face` is a full face mask, `face_extended` is a face mask
488493
that extends above the eyebrows. The others are masks for those specific areas
489494
dilation
490-
The amount of dilation to apply to the mask. as a percentage of the mask size
495+
The amount of dilation to apply to the mask. as a percentage of the mask size.
496+
Default: 0
497+
blur_kernel
498+
The kernel size, in pixels to apply gaussian blurring to the mask. Set to 0 for no
499+
blurring. Should be odd, if an even number is passed in (outside of 0) then it is
500+
rounded up to the next odd number. Default: 0
501+
blur_type
502+
The blur type to use. ``gaussian`` or ``normalized`` box filter. Default: ``gaussian``
503+
blur_passes
504+
The number of passed to perform when blurring. Default: 1
491505
492506
Returns
493507
-------
494-
The requested Landmarks Mask object
508+
The requested Landmarks Mask
495509
"""
496510
logger.trace("area: %s, dilation: %s", area, dilation) # type:ignore[attr-defined]
497511
mask = LandmarksMask(area,
498512
self.landmark_type,
499513
self.landmarks,
500-
self.adjusted_matrix,
501-
storage_size=self.size,
502-
storage_centering=self.centering,
503-
dilation=dilation)
504-
return mask
514+
self.size,
515+
dilation=dilation,
516+
blur_kernel=blur_kernel,
517+
blur_type=blur_type,
518+
blur_passes=blur_passes)
519+
return mask.mask
505520

506521

507522
def _umeyama(source: np.ndarray, destination: np.ndarray, estimate_scale: bool) -> np.ndarray:

lib/align/aligned_mask.py

Lines changed: 84 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import cv2
1111
import numpy as np
1212

13-
from lib.logger import parse_class_init
13+
from lib.logger import format_array, parse_class_init
1414
from lib.utils import FaceswapError, get_module_objects
1515

1616
from .aligned_utils import get_adjusted_center, get_sub_crop_size
@@ -423,7 +423,7 @@ def from_dict(self, mask: MaskAlignmentsFile) -> T.Self:
423423
return self
424424

425425

426-
class LandmarksMask(Mask):
426+
class LandmarksMask():
427427
"""Create a single channel mask from aligned landmark points.
428428
429429
Landmarks masks are created on the fly, so the stored centering and size should be the same as
@@ -444,36 +444,59 @@ class LandmarksMask(Mask):
444444
The type of landmarks that this mask is being created from
445445
landmarks
446446
The landmarks to generate the mask from
447-
affine_matrix
448-
The transformation matrix required to transform the mask to the original frame.
449-
storage_size
450-
The size (in pixels) that the compressed mask should be stored at. Default: 128.
451-
storage_centering
452-
The centering to store the mask at. One of `"legacy"`, `"face"`, `"head"`.
453-
Default: `"face"`
447+
size
448+
The size (in pixels) that the compressed mask should be
454449
dilation
455450
The amount of dilation to apply to the mask. as a percentage of the mask size. Default: 0.0
451+
blur_kernel
452+
The kernel size, in pixels to apply gaussian blurring to the mask. Set to 0 for no
453+
blurring. Should be odd, if an even number is passed in (outside of 0) then it is rounded
454+
up to the next odd number. Default: 0
455+
blur_type
456+
The blur type to use. ``gaussian`` or ``normalized`` box filter. Default: ``gaussian``
457+
blur_passes
458+
The number of passed to perform when blurring. Default: 1
456459
"""
457460
def __init__(self,
458461
area: T.Literal["eye", "mouth", "face", "face_extended"],
459462
landmark_type: LandmarkType,
460463
landmarks: npt.NDArray[np.float32],
461-
affine_matrix: npt.NDArray[np.float32],
462-
storage_size: int = 128,
463-
storage_centering: CenteringType = "face",
464-
dilation: float = 0.0) -> None:
465-
super().__init__(storage_size=storage_size, storage_centering=storage_centering)
464+
size: int,
465+
dilation: float = 0.0,
466+
blur_kernel: int = 0,
467+
blur_type: T.Literal["gaussian", "normalized"] | None = "gaussian",
468+
blur_passes: int = 1) -> None:
469+
logger.debug(parse_class_init(locals()))
466470
self._area = area
467471
self._landmark_type = landmark_type
468-
self._lm_matrix = affine_matrix
469-
self._points = self._get_points(landmarks)
470-
self.set_dilation(dilation)
472+
self._landmarks = landmarks
473+
self._size = size
474+
self._original_mask: npt.NDArray[np.uint8] | None = None
475+
476+
self.dilation = dilation
477+
"""The amount of dilation to apply to the mask. as a percentage of the mask size.
478+
Default: 0.0"""
479+
self.blur_kernel = blur_kernel
480+
"""The kernel size, in pixels to apply gaussian blurring to the mask. Set to 0 for no
481+
blurring. Should be odd, if an even number is passed in (outside of 0) then it is rounded
482+
up to the next odd number. Default: 0"""
483+
self.blur_type: T.Literal["gaussian", "normalized"] | None = blur_type
484+
"""The blur type to use. ``gaussian``, ``normalized`` box filter or ``None`` for no blur.
485+
Default: ``gaussian``"""
486+
self.blur_passes = blur_passes
487+
"""The number of passed to perform when blurring. Default: 1"""
488+
self.mask = self.generate_mask()
489+
"""The mask at the size of :attr:`size` with any requested blurring, threshold amount and
490+
centering applied."""
471491

472-
@property
473-
def mask(self) -> npt.NDArray[np.uint8]:
474-
"""Overrides the default mask property, creating the processed mask at first call and
475-
compressing it. The decompressed mask is returned from this property."""
476-
return self.stored_mask
492+
def __repr__(self) -> str:
493+
"""Pretty print for logging"""
494+
params = {f"{k[1:]}": format_array(v) if isinstance(v, np.ndarray) else v
495+
for k, v in self.__dict__.items()
496+
if k in ("_area", "_landmark_type", "_landmarks", "_size",
497+
"_dilation", "_blur_kernel", "_blur_type", "blur_passes")}
498+
s_params = ", ".join(f"{k}={repr(v)}" for k, v in params.items())
499+
return f"{self.__class__.__name__}({s_params})"
477500

478501
def _get_slices(self) -> list[slice] | list[list[slice]]:
479502
"""Obtain the slices that will extract the points for the given area and landmark type
@@ -543,19 +566,15 @@ def _extend_face_landmarks(self,
543566
retval[22:27] = top_r + ((top_r - bot_r) // 2)
544567
return retval
545568

546-
def _get_points(self, landmarks: npt.NDArray[np.float32]) -> list[npt.NDArray[np.int32]]:
569+
def _get_points(self) -> list[npt.NDArray[np.int32]]:
547570
"""Obtain the points required to create the mask
548571
549-
Parameters
550-
----------
551-
landmarks
552-
The landmarks to obtain the points from
553-
554572
Returns
555573
-------
556574
The list of points for creating each section of the mask
557575
"""
558576
slices = self._get_slices()
577+
landmarks = self._landmarks
559578
if self._area == "face_extended":
560579
landmarks = self._extend_face_landmarks(landmarks)
561580

@@ -567,26 +586,49 @@ def _get_points(self, landmarks: npt.NDArray[np.float32]) -> list[npt.NDArray[np
567586
for zone in T.cast(list[list[slice]], slices)]
568587
return retval
569588

570-
def generate_mask(self) -> None:
589+
def _dilate(self, mask: npt.NDArray[np.uint8]):
590+
"""Perform dilation on the mask
591+
592+
Parameters
593+
----------
594+
mask
595+
The mask to dilate
596+
"""
597+
if self.dilation == 0.0:
598+
return
599+
kernel_size = int(round(self._size * abs(self.dilation / 100.), 0))
600+
element = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size))
601+
func = cv2.erode if self.dilation < 0 else cv2.dilate
602+
func(mask, element, dst=mask, iterations=1)
603+
604+
def generate_mask(self) -> npt.NDArray[np.uint8]:
571605
"""Generate the mask.
572606
573-
Creates the mask applying any requested dilation and blurring and assigns compressed mask
574-
to :attr:`_mask`
607+
Creates the mask applying any requested dilation and blurring
608+
609+
Returns
610+
-------
611+
The landmarks based mask
575612
"""
576-
mask = np.zeros((self.stored_size, self.stored_size, 1), dtype=np.uint8)
577-
for pts in self._points:
578-
lms = np.rint(pts).astype("int")
579-
cv2.fillConvexPoly(mask, cv2.convexHull(lms), [255], lineType=cv2.LINE_AA)
580-
if self._dilation[-1] is not None:
581-
self._dilate_mask(mask)
582-
if self._blur_kernel != 0 and self._blur_type is not None:
583-
mask = BlurMask(self._blur_type,
613+
if self._original_mask is None:
614+
points = self._get_points()
615+
mask = np.zeros((self._size, self._size, 1), dtype=np.uint8)
616+
for pts in points:
617+
lms = np.rint(pts).astype("int")
618+
cv2.fillConvexPoly(mask, cv2.convexHull(lms), [255], lineType=cv2.LINE_AA)
619+
self._original_mask = mask
620+
621+
mask = self._original_mask.copy()
622+
self._dilate(mask)
623+
624+
if self.blur_kernel != 0 and self.blur_type is not None:
625+
mask = BlurMask(self.blur_type,
584626
mask,
585-
self._blur_kernel,
586-
passes=self._blur_passes).blurred
627+
self.blur_kernel,
628+
passes=self.blur_passes).blurred
587629
logger.trace("[LM_MASK] mask: (shape: %s, dtype: %s)", # type:ignore[attr-defined]
588630
mask.shape, mask.dtype)
589-
self.add(mask, self._lm_matrix)
631+
return mask
590632

591633

592634
class BlurMask():

lib/align/aligned_utils.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,51 @@ def sub_crop(image: npt.NDArray[np.uint8 | np.float32], # pylint:disable=too-ma
294294

295295

296296
# Batch functions
297+
def batch_create_matrices(size: int,
298+
rotation: npt.NDArray[np.float32],
299+
scale: npt.NDArray[np.float32] | None = None,
300+
translation: npt.NDArray[np.float32] | None = None
301+
) -> npt.NDArray[np.float32]:
302+
"""Generate affine transformation matrices for the given rotations, scales and translations
303+
304+
Parameters
305+
----------
306+
size
307+
The size of the image that the matrix is transforming to
308+
rotation
309+
A 1D batch of rotation amounts or ``None`` for no rotation. Default: ``None``
310+
scale
311+
A 1D batch of scale amounts or ``None`` for no scaling. Default: ``None``
312+
translation
313+
A 2D batch of (x, y) translation amounts or ``None`` for no translation. Default: ``None``
314+
315+
Returns
316+
-------
317+
The (3, 3) transformation matrices for the requested transform
318+
"""
319+
theta = np.deg2rad(rotation)
320+
cos_t = np.cos(theta)
321+
sin_t = np.sin(theta)
322+
if scale is not None:
323+
cos_t *= scale
324+
sin_t *= scale
325+
326+
cx = cy = (size - 1) / 2.0
327+
328+
matrices = np.zeros((len(rotation), 3, 3), dtype=np.float32)
329+
matrices[:, 0, 0] = cos_t
330+
matrices[:, 0, 1] = sin_t
331+
matrices[:, 1, 0] = -sin_t
332+
matrices[:, 1, 1] = cos_t
333+
matrices[:, 0, 2] = cx * (1 - cos_t) - cy * sin_t
334+
matrices[:, 1, 2] = cx * sin_t + cy * (1 - cos_t)
335+
if translation is not None:
336+
matrices[:, :2, 2] += translation
337+
matrices[:, 2, :] = [0., 0., 1.]
338+
logger.trace("Created affine matrices: %s", matrices.tolist()) # type:ignore[attr-defined]
339+
return matrices
340+
341+
297342
def batch_transform(matrices: npt.NDArray[np.float32],
298343
points: npt.NDArray[np.float32],
299344
in_place: bool = False) -> npt.NDArray[np.float32]:

0 commit comments

Comments
 (0)