mirror of
https://github.com/deepfakes/faceswap.git
synced 2026-10-18 14:34:30 +08:00
494 lines
20 KiB
Python
494 lines
20 KiB
Python
#!/usr/bin python3
|
|
""" Pytest unit tests for :mod:`tools.preview.viewer` """
|
|
from __future__ import annotations
|
|
import tkinter as tk
|
|
import typing as T
|
|
|
|
from tkinter import ttk
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
import pytest_mock
|
|
import numpy as np
|
|
from PIL import ImageTk
|
|
|
|
from lib.logger import log_setup
|
|
# Need to setup logging to avoid trace/verbose errors
|
|
log_setup("DEBUG", "pytest_viewer.log", "PyTest, False")
|
|
|
|
from lib.utils import get_backend # pylint:disable=wrong-import-position # noqa
|
|
from tools.preview.viewer import _Faces, FacesDisplay, ImagesCanvas # pylint:disable=wrong-import-position # noqa
|
|
|
|
if T.TYPE_CHECKING:
|
|
from lib.align.aligned_face import CenteringType
|
|
|
|
|
|
# pylint:disable=protected-access
|
|
|
|
|
|
def test__faces():
|
|
""" Test the :class:`~tools.preview.viewer._Faces dataclass initializes correctly """
|
|
faces = _Faces(5, 64)
|
|
assert isinstance(faces.filenames, list) and not faces.filenames
|
|
assert isinstance(faces.matrix, np.ndarray)
|
|
assert isinstance(faces.src, np.ndarray)
|
|
assert isinstance(faces.dst, np.ndarray)
|
|
|
|
|
|
_PARAMS = ((3, 448), (4, 333), (5, 254), (6, 128)) # columns/face_size
|
|
_IDS = [f"cols:{c},size:{s}[{get_backend().upper()}]" for c, s in _PARAMS]
|
|
|
|
|
|
class TestFacesDisplay():
|
|
""" Test :class:`~tools.preview.viewer.FacesDisplay """
|
|
_padding = 64
|
|
|
|
def get_faces_display_instance(self, columns: int = 5, face_size: int = 256) -> FacesDisplay:
|
|
""" Obtain an instance of :class:`~tools.preview.viewer.FacesDisplay` with the given column
|
|
and face size layout.
|
|
|
|
Parameters
|
|
----------
|
|
columns: int, optional
|
|
The number of columns to display in the viewer, default: 5
|
|
face_size: int, optional
|
|
The size of each face image to be displayed in the viewer, default: 256
|
|
|
|
Returns
|
|
-------
|
|
:class:`~tools.preview.viewer.FacesDisplay`
|
|
An instance of the FacesDisplay class at the given settings
|
|
"""
|
|
app = MagicMock()
|
|
retval = FacesDisplay(app, face_size, self._padding, columns)
|
|
retval._faces = _Faces(columns, face_size)
|
|
return retval
|
|
|
|
def test_init(self) -> None:
|
|
""" Test :class:`~tools.preview.viewer.FacesDisplay` __init__ method """
|
|
f_display = self.get_faces_display_instance(face_size=256)
|
|
assert f_display._size == 256
|
|
assert f_display._padding == self._padding
|
|
assert isinstance(f_display._app, MagicMock)
|
|
|
|
assert f_display._display_dims == (1, 1)
|
|
assert isinstance(f_display._faces, _Faces)
|
|
|
|
assert f_display._centering is None
|
|
assert f_display._faces_source.size == 0
|
|
assert f_display._faces_dest.size == 0
|
|
assert f_display._tk_image is None
|
|
assert f_display.update_source is False
|
|
assert not f_display.source and isinstance(f_display.source, list)
|
|
assert not f_display.destination and isinstance(f_display.destination, list)
|
|
|
|
@pytest.mark.parametrize("columns, face_size", _PARAMS, ids=_IDS)
|
|
def test__total_columns(self, columns: int, face_size: int) -> None:
|
|
""" Test :class:`~tools.preview.viewer.FacesDisplay` _total_columns property is correctly
|
|
calculated
|
|
|
|
Parameters
|
|
----------
|
|
columns: int
|
|
The number of columns to display in the viewer
|
|
face_size: int
|
|
The size of each face image to be displayed in the viewer
|
|
"""
|
|
f_display = self.get_faces_display_instance(columns, face_size)
|
|
f_display.source = [None for _ in range(columns)] # type:ignore
|
|
assert f_display._total_columns == columns
|
|
|
|
def test_set_centering(self) -> None:
|
|
""" Test :class:`~tools.preview.viewer.FacesDisplay` set_centering_offset method """
|
|
f_display = self.get_faces_display_instance()
|
|
assert f_display._centering is None
|
|
centering: CenteringType = "legacy"
|
|
f_display.set_centering_offset(centering, 0.80)
|
|
assert f_display._centering == centering
|
|
assert f_display._y_offset == 0.80
|
|
|
|
def test_set_display_dimensions(self) -> None:
|
|
""" Test :class:`~tools.preview.viewer.FacesDisplay` set_display_dimensions method """
|
|
f_display = self.get_faces_display_instance()
|
|
assert f_display._display_dims == (1, 1)
|
|
dimensions = (800, 600)
|
|
f_display.set_display_dimensions(dimensions)
|
|
assert f_display._display_dims == dimensions
|
|
|
|
# TODO remove the next line that suppresses a weird pytest bug when it tears down the tempdir
|
|
@pytest.mark.filterwarnings("ignore::pytest.PytestUnraisableExceptionWarning")
|
|
@pytest.mark.parametrize("columns, face_size", _PARAMS, ids=_IDS)
|
|
def test_update_tk_image(self,
|
|
columns: int,
|
|
face_size: int,
|
|
mocker: pytest_mock.MockerFixture) -> None:
|
|
""" Test :class:`~tools.preview.viewer.FacesDisplay` update_tk_image method
|
|
|
|
Parameters
|
|
----------
|
|
columns: int
|
|
The number of columns to display in the viewer
|
|
face_size: int
|
|
The size of each face image to be displayed in the viewer
|
|
mocker: :class:`pytest_mock.MockerFixture`
|
|
Mocker for checking _build_faces_image method called
|
|
"""
|
|
f_display = self.get_faces_display_instance(columns, face_size)
|
|
f_display._build_faces_image = T.cast(MagicMock, mocker.MagicMock()) # type:ignore
|
|
f_display._get_scale_size = T.cast(MagicMock, # type:ignore
|
|
mocker.MagicMock(return_value=(128, 128)))
|
|
f_display._faces_source = np.zeros((face_size, face_size, 3), dtype=np.uint8)
|
|
f_display._faces_dest = np.zeros((face_size, face_size, 3), dtype=np.uint8)
|
|
|
|
try:
|
|
tk.Tk() # tkinter instance needed for image creation
|
|
except tk.TclError:
|
|
# Some Windows runners arbitrarily don't install Tk correctly
|
|
pytest.skip("Tk not available on this system")
|
|
f_display.update_tk_image()
|
|
|
|
f_display._build_faces_image.assert_called_once()
|
|
f_display._get_scale_size.assert_called_once()
|
|
assert isinstance(f_display._tk_image, ImageTk.PhotoImage)
|
|
assert f_display._tk_image.width() == 128
|
|
assert f_display._tk_image.height() == 128
|
|
assert f_display.tk_image == f_display._tk_image # public property test
|
|
|
|
@pytest.mark.parametrize("columns, face_size", _PARAMS, ids=_IDS)
|
|
def test_get_scale_size(self, columns: int, face_size: int) -> None:
|
|
""" Test :class:`~tools.preview.viewer.FacesDisplay` get_scale_size method
|
|
|
|
Parameters
|
|
----------
|
|
columns: int
|
|
The number of columns to display in the viewer
|
|
face_size: int
|
|
The size of each face image to be displayed in the viewer
|
|
"""
|
|
f_display = self.get_faces_display_instance(columns, face_size)
|
|
f_display.set_display_dimensions((800, 600))
|
|
|
|
img = np.zeros((face_size, face_size, 3), dtype=np.uint8)
|
|
size = f_display._get_scale_size(img)
|
|
assert size == (600, 600)
|
|
|
|
@pytest.mark.parametrize("columns, face_size", _PARAMS, ids=_IDS)
|
|
def test__build_faces_image(self,
|
|
columns: int,
|
|
face_size: int,
|
|
mocker: pytest_mock.MockerFixture) -> None:
|
|
""" Test :class:`~tools.preview.viewer.FacesDisplay` _build_faces_image method
|
|
|
|
Parameters
|
|
----------
|
|
columns: int
|
|
The number of columns to display in the viewer
|
|
face_size: int
|
|
The size of each face image to be displayed in the viewer
|
|
mocker: :class:`pytest_mock.MockerFixture`
|
|
Mocker for checking internal methods called
|
|
"""
|
|
header_size = 32
|
|
|
|
f_display = self.get_faces_display_instance(columns, face_size)
|
|
f_display._faces_from_frames = T.cast(MagicMock, mocker.MagicMock()) # type:ignore
|
|
f_display._header_text = T.cast( # type:ignore
|
|
MagicMock,
|
|
mocker.MagicMock(return_value=np.random.rand(header_size, face_size * columns, 3)))
|
|
f_display._draw_rect = T.cast(MagicMock, # type:ignore
|
|
mocker.MagicMock(side_effect=lambda x: x))
|
|
|
|
# Test full update
|
|
f_display.update_source = True
|
|
f_display._build_faces_image()
|
|
|
|
f_display._faces_from_frames.assert_called_once()
|
|
f_display._header_text.assert_called_once()
|
|
assert f_display._draw_rect.call_count == columns * 2 # src + dst
|
|
assert f_display._faces_source.shape == (face_size + header_size, face_size * columns, 3)
|
|
assert f_display._faces_dest.shape == (face_size, face_size * columns, 3)
|
|
|
|
f_display._faces_from_frames.reset_mock()
|
|
f_display._header_text.reset_mock()
|
|
f_display._draw_rect.reset_mock()
|
|
|
|
# Test dst update only
|
|
f_display.update_source = False
|
|
f_display._build_faces_image()
|
|
|
|
f_display._faces_from_frames.assert_called_once()
|
|
assert not f_display._header_text.called
|
|
assert f_display._draw_rect.call_count == columns # dst only
|
|
assert f_display._faces_dest.shape == (face_size, face_size * columns, 3)
|
|
|
|
@pytest.mark.parametrize("columns, face_size", _PARAMS, ids=_IDS)
|
|
def test_faces__from_frames(self,
|
|
columns,
|
|
face_size,
|
|
mocker: pytest_mock.MockerFixture) -> None:
|
|
""" Test :class:`~tools.preview.viewer.FacesDisplay` _from_frames method
|
|
|
|
Parameters
|
|
----------
|
|
columns: int
|
|
The number of columns to display in the viewer
|
|
face_size: int
|
|
The size of each face image to be displayed in the viewer
|
|
mocker: :class:`pytest_mock.MockerFixture`
|
|
Mocker for checking _build_faces_image method called
|
|
"""
|
|
f_display = self.get_faces_display_instance(columns, face_size)
|
|
f_display.source = [mocker.MagicMock() for _ in range(3)]
|
|
f_display.destination = [np.random.rand(face_size, face_size, 3) for _ in range(3)]
|
|
f_display._crop_source_faces = T.cast(MagicMock, mocker.MagicMock()) # type:ignore
|
|
f_display._crop_destination_faces = T.cast(MagicMock, mocker.MagicMock()) # type:ignore
|
|
|
|
# Both src + dst
|
|
f_display.update_source = True
|
|
f_display._faces_from_frames()
|
|
f_display._crop_source_faces.assert_called_once()
|
|
f_display._crop_destination_faces.assert_called_once()
|
|
|
|
f_display._crop_source_faces.reset_mock()
|
|
f_display._crop_destination_faces.reset_mock()
|
|
|
|
# Just dst
|
|
f_display.update_source = False
|
|
f_display._faces_from_frames()
|
|
assert not f_display._crop_source_faces.called
|
|
f_display._crop_destination_faces.assert_called_once()
|
|
|
|
@pytest.mark.parametrize("columns, face_size", _PARAMS, ids=_IDS)
|
|
def test__crop_source_faces(self,
|
|
columns: int,
|
|
face_size: int,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
mocker: pytest_mock.MockerFixture) -> None:
|
|
""" Test :class:`~tools.preview.viewer.FacesDisplay` _crop_source_faces method
|
|
|
|
Parameters
|
|
----------
|
|
columns: int
|
|
The number of columns to display in the viewer
|
|
face_size: int
|
|
The size of each face image to be displayed in the viewer
|
|
monkeypatch: :class:`pytest.MonkeyPatch`
|
|
For patching the transform_image function
|
|
mocker: :class:`pytest_mock.MockerFixture`
|
|
Mocker for mocking various internal methods
|
|
"""
|
|
f_display = self.get_faces_display_instance(columns, face_size)
|
|
f_display._centering = "face"
|
|
f_display.update_source = True
|
|
|
|
transform_image_mock = mocker.MagicMock(return_value=np.zeros((face_size, face_size, 3),
|
|
dtype=np.uint8))
|
|
monkeypatch.setattr("tools.preview.viewer.transform_image", transform_image_mock)
|
|
|
|
mats = np.random.random((columns, 2, 3)).astype(np.float32)
|
|
f_display.source = [mocker.MagicMock() for _ in range(columns)]
|
|
for idx, mock in enumerate(f_display.source):
|
|
assert isinstance(mock, MagicMock)
|
|
mock.inbound.detected_faces.__getitem__ = lambda self, x, y=mock: y
|
|
mock.aligned.matrix = mats[idx]
|
|
mock.inbound.filename = f"test_filename_{idx}.txt"
|
|
|
|
f_display._crop_source_faces()
|
|
|
|
assert len(f_display._faces.filenames) == columns
|
|
assert len(f_display._faces.matrix) == columns
|
|
assert len(f_display._faces.src) == columns
|
|
assert not f_display.update_source
|
|
assert transform_image_mock.call_count == columns
|
|
|
|
for idx in range(columns):
|
|
assert f_display._faces.filenames[idx] == f"test_filename_{idx}"
|
|
assert np.all(f_display._faces.matrix[idx] == mats[idx])
|
|
|
|
@pytest.mark.parametrize("columns, face_size", _PARAMS, ids=_IDS)
|
|
def test__crop_destination_faces(self,
|
|
columns: int,
|
|
face_size: int,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
mocker: pytest_mock.MockerFixture) -> None:
|
|
""" Test :class:`~tools.preview.viewer.FacesDisplay` _crop_destination_faces method
|
|
|
|
Parameters
|
|
----------
|
|
columns: int
|
|
The number of columns to display in the viewer
|
|
face_size: int
|
|
The size of each face image to be displayed in the viewer
|
|
mocker: :class:`pytest_mock.MockerFixture`
|
|
Mocker for dummying in full frames
|
|
"""
|
|
f_display = self.get_faces_display_instance(columns, face_size)
|
|
f_display._centering = "face"
|
|
|
|
transform_image_mock = mocker.MagicMock(return_value=np.zeros((face_size, face_size, 3),
|
|
dtype=np.uint8))
|
|
monkeypatch.setattr("tools.preview.viewer.transform_image", transform_image_mock)
|
|
|
|
f_display.source = [mocker.MagicMock() for _ in range(columns)]
|
|
for item in f_display.source: # type ignore
|
|
item.inbound.image = np.random.rand(1280, 720, 3) # type:ignore
|
|
|
|
f_display._crop_destination_faces()
|
|
assert transform_image_mock.call_count == columns
|
|
assert len(f_display._faces.dst) == columns
|
|
assert all(f.shape == (face_size, face_size, 3) for f in f_display._faces.dst)
|
|
|
|
@pytest.mark.parametrize("columns, face_size", _PARAMS, ids=_IDS)
|
|
def test__header_text(self,
|
|
columns: int,
|
|
face_size: int,
|
|
mocker: pytest_mock.MockerFixture) -> None:
|
|
""" Test :class:`~tools.preview.viewer.FacesDisplay` _header_text method
|
|
|
|
Parameters
|
|
----------
|
|
columns: int
|
|
The number of columns to display in the viewer
|
|
face_size: int
|
|
The size of each face image to be displayed in the viewer
|
|
mocker: :class:`pytest_mock.MockerFixture`
|
|
Mocker for dummying in cv2 calls
|
|
"""
|
|
f_display = self.get_faces_display_instance(columns, face_size)
|
|
f_display.source = [None for _ in range(columns)] # type:ignore
|
|
f_display._faces.filenames = [f"filename_{idx}.png" for idx in range(columns)]
|
|
|
|
cv2_mock = mocker.patch("tools.preview.viewer.cv2")
|
|
text_width, text_height = (100, 32)
|
|
cv2_mock.getTextSize.return_value = [(text_width, text_height), ]
|
|
|
|
header_box = f_display._header_text()
|
|
assert cv2_mock.getTextSize.call_count == columns
|
|
assert cv2_mock.putText.call_count == columns
|
|
assert header_box.shape == (face_size // 8, face_size * columns, 3)
|
|
|
|
@pytest.mark.parametrize("columns, face_size", _PARAMS, ids=_IDS)
|
|
def test__draw_rect_text(self,
|
|
columns: int,
|
|
face_size: int,
|
|
mocker: pytest_mock.MockerFixture) -> None:
|
|
""" Test :class:`~tools.preview.viewer.FacesDisplay` _draw_rect method
|
|
|
|
Parameters
|
|
----------
|
|
columns: int
|
|
The number of columns to display in the viewer
|
|
face_size: int
|
|
The size of each face image to be displayed in the viewer
|
|
mocker: :class:`pytest_mock.MockerFixture`
|
|
Mocker for dummying in cv2 calls
|
|
"""
|
|
f_display = self.get_faces_display_instance(columns, face_size)
|
|
cv2_mock = mocker.patch("tools.preview.viewer.cv2")
|
|
|
|
image = (np.random.rand(face_size, face_size, 3) * 255.0) + 50
|
|
assert image.max() > 255.0
|
|
output = f_display._draw_rect(image)
|
|
cv2_mock.rectangle.assert_called_once()
|
|
assert output.max() == 255.0 # np.clip
|
|
|
|
|
|
class TestImagesCanvas:
|
|
""" Test :class:`~tools.preview.viewer.ImagesCanvas` """
|
|
|
|
@pytest.fixture
|
|
def parent(self) -> MagicMock:
|
|
""" Mock object to act as the parent widget to the ImagesCanvas
|
|
|
|
Returns
|
|
--------
|
|
:class:`unittest.mock.MagicMock`
|
|
The mocked ttk.PanedWindow widget
|
|
"""
|
|
retval = MagicMock(spec=ttk.PanedWindow)
|
|
retval.tk = retval
|
|
retval._w = "mock_ttkPanedWindow"
|
|
retval.children = {}
|
|
retval.call = retval
|
|
retval.createcommand = retval
|
|
retval.preview_display = MagicMock(spec=FacesDisplay)
|
|
return retval
|
|
|
|
@pytest.fixture(name="images_canvas_instance")
|
|
def images_canvas_fixture(self, parent) -> ImagesCanvas:
|
|
""" Fixture for creating a testing :class:`~tools.preview.viewer.ImagesCanvas` instance
|
|
|
|
Parameters
|
|
----------
|
|
parent: :class:`unittest.mock.MagicMock`
|
|
The mocked ttk.PanedWindow parent
|
|
|
|
Returns
|
|
-------
|
|
:class:`~tools.preview.viewer.ImagesCanvas`
|
|
The class instance for testing
|
|
"""
|
|
app = MagicMock()
|
|
return ImagesCanvas(app, parent)
|
|
|
|
def test_init(self, images_canvas_instance: ImagesCanvas, parent: MagicMock) -> None:
|
|
""" Test :class:`~tools.preview.viewer.ImagesCanvas` __init__ method
|
|
|
|
Parameters
|
|
----------
|
|
images_canvas_instance: :class:`~tools.preview.viewer.ImagesCanvas`
|
|
The class instance to test
|
|
parent: :class:`unittest.mock.MagicMock`
|
|
The mocked parent ttk.PanedWindow
|
|
"""
|
|
assert images_canvas_instance._display == parent.preview_display
|
|
assert isinstance(images_canvas_instance._canvas, tk.Canvas)
|
|
assert images_canvas_instance._canvas.master == images_canvas_instance
|
|
assert images_canvas_instance._canvas.winfo_ismapped()
|
|
|
|
def test_resize(self,
|
|
images_canvas_instance: ImagesCanvas,
|
|
parent: MagicMock,
|
|
mocker: pytest_mock.MockerFixture) -> None:
|
|
""" Test :class:`~tools.preview.viewer.ImagesCanvas` resize method
|
|
|
|
Parameters
|
|
----------
|
|
images_canvas_instance: :class:`~tools.preview.viewer.ImagesCanvas`
|
|
The class instance to test
|
|
parent: :class:`unittest.mock.MagicMock`
|
|
The mocked parent ttk.PanedWindow
|
|
mocker: :class:`pytest_mock.MockerFixture`
|
|
Mocker for dummying in tk calls
|
|
"""
|
|
event_mock = mocker.MagicMock(spec=tk.Event, width=100, height=200)
|
|
images_canvas_instance.reload = T.cast(MagicMock, mocker.MagicMock()) # type:ignore
|
|
|
|
images_canvas_instance._resize(event_mock)
|
|
|
|
parent.preview_display.set_display_dimensions.assert_called_once_with((100, 200))
|
|
images_canvas_instance.reload.assert_called_once()
|
|
|
|
def test_reload(self,
|
|
images_canvas_instance: ImagesCanvas,
|
|
parent: MagicMock,
|
|
mocker: pytest_mock.MockerFixture) -> None:
|
|
""" Test :class:`~tools.preview.viewer.ImagesCanvas` reload method
|
|
|
|
Parameters
|
|
----------
|
|
images_canvas_instance: :class:`~tools.preview.viewer.ImagesCanvas`
|
|
The class instance to test
|
|
parent: :class:`unittest.mock.MagicMock`
|
|
The mocked parent ttk.PanedWindow
|
|
mocker: :class:`pytest_mock.MockerFixture`
|
|
Mocker for dummying in tk calls
|
|
"""
|
|
itemconfig_mock = mocker.patch.object(tk.Canvas, "itemconfig")
|
|
|
|
images_canvas_instance.reload()
|
|
|
|
parent.preview_display.update_tk_image.assert_called_once()
|
|
itemconfig_mock.assert_called_once()
|