mirror of
https://github.com/zebrajr/faceswap.git
synced 2025-12-06 00:20:09 +01:00
Add Mask importing to the mask tool (#1376)
* lib.align.alignments: expose count_faces_in_frame * tools.mask: refactor and fix frame output to display all masks in a single frame * tools.mask: add import mask process * manual tool: Remove NN masks on landmark edit
This commit is contained in:
parent
d1dfce8a13
commit
7a16f753cc
|
|
@ -426,7 +426,7 @@ class Alignments():
|
|||
frame_data = self._data.get(frame_name, T.cast(AlignmentDict, {}))
|
||||
return frame_data.get("faces", T.cast(list[AlignmentFileDict], []))
|
||||
|
||||
def _count_faces_in_frame(self, frame_name: str) -> int:
|
||||
def count_faces_in_frame(self, frame_name: str) -> int:
|
||||
""" Return number of faces that appear within :attr:`data` for the given frame_name.
|
||||
|
||||
Parameters
|
||||
|
|
@ -464,7 +464,7 @@ class Alignments():
|
|||
"""
|
||||
logger.debug("Deleting face %s for frame_name '%s'", face_index, frame_name)
|
||||
face_index = int(face_index)
|
||||
if face_index + 1 > self._count_faces_in_frame(frame_name):
|
||||
if face_index + 1 > self.count_faces_in_frame(frame_name):
|
||||
logger.debug("No face to delete: (frame_name: '%s', face_index %s)",
|
||||
frame_name, face_index)
|
||||
return False
|
||||
|
|
@ -493,7 +493,7 @@ class Alignments():
|
|||
if frame_name not in self._data:
|
||||
self._data[frame_name] = {"faces": [], "video_meta": {}}
|
||||
self._data[frame_name]["faces"].append(face)
|
||||
retval = self._count_faces_in_frame(frame_name) - 1
|
||||
retval = self.count_faces_in_frame(frame_name) - 1
|
||||
logger.debug("Returning new face index: %s", retval)
|
||||
return retval
|
||||
|
||||
|
|
@ -542,6 +542,18 @@ class Alignments():
|
|||
source_frame, face_idx)
|
||||
del frame_data["faces"][face_idx]
|
||||
|
||||
def update_from_dict(self, data: dict[str, AlignmentDict]) -> None:
|
||||
""" Replace all alignments with the contents of the given dictionary
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data: dict[str, AlignmentDict]
|
||||
The alignments, in correctly formatted dictionary form, to be populated into this
|
||||
:class:`Alignments`
|
||||
"""
|
||||
logger.debug("Populating alignments with %s entries", len(data))
|
||||
self._data = data
|
||||
|
||||
# << GENERATORS >> #
|
||||
def yield_faces(self) -> Generator[tuple[str, list[AlignmentFileDict], int, str], None, None]:
|
||||
""" Generator to obtain all faces with meta information from :attr:`data`. The results
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -6,8 +6,8 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: faceswap.spanish\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-02-20 23:42+0000\n"
|
||||
"PO-Revision-Date: 2023-02-20 23:45+0000\n"
|
||||
"POT-Creation-Date: 2024-03-11 23:45+0000\n"
|
||||
"PO-Revision-Date: 2024-03-11 23:50+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: tokafondo\n"
|
||||
"Language: es_ES\n"
|
||||
|
|
@ -16,30 +16,36 @@ msgstr ""
|
|||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"Generated-By: pygettext.py 1.5\n"
|
||||
"X-Generator: Poedit 3.0.1\n"
|
||||
"X-Generator: Poedit 3.4.2\n"
|
||||
|
||||
#: tools/mask/cli.py:15
|
||||
msgid "This command lets you generate masks for existing alignments."
|
||||
msgid ""
|
||||
"This tool allows you to generate, import, export or preview masks for "
|
||||
"existing alignments."
|
||||
msgstr ""
|
||||
"Este comando permite generar máscaras para las alineaciones existentes."
|
||||
"Esta herramienta le permite generar, importar, exportar o obtener una vista "
|
||||
"previa de máscaras para alineaciones existentes.\n"
|
||||
"Genere, importe, exporte o obtenga una vista previa de máscaras para "
|
||||
"archivos de alineaciones existentes."
|
||||
|
||||
#: tools/mask/cli.py:24
|
||||
#: tools/mask/cli.py:25
|
||||
msgid ""
|
||||
"Mask tool\n"
|
||||
"Generate masks for existing alignments files."
|
||||
"Generate, import, export or preview masks for existing alignments files."
|
||||
msgstr ""
|
||||
"Herramienta de máscara\n"
|
||||
"Genera máscaras para los archivos de alineación existentes."
|
||||
"Genere, importe, exporte o obtenga una vista previa de máscaras para "
|
||||
"archivos de alineaciones existentes."
|
||||
|
||||
#: tools/mask/cli.py:33 tools/mask/cli.py:44 tools/mask/cli.py:54
|
||||
#: tools/mask/cli.py:64
|
||||
#: tools/mask/cli.py:35 tools/mask/cli.py:47 tools/mask/cli.py:58
|
||||
#: tools/mask/cli.py:69
|
||||
msgid "data"
|
||||
msgstr "datos"
|
||||
|
||||
#: tools/mask/cli.py:36
|
||||
#: tools/mask/cli.py:39
|
||||
msgid ""
|
||||
"Full path to the alignments file to add the mask to if not at the default "
|
||||
"location. NB: If the input-type is faces and you wish to update the "
|
||||
"Full path to the alignments file that contains the masks if not at the "
|
||||
"default location. NB: If the input-type is faces and you wish to update the "
|
||||
"corresponding alignments file, then you must provide a value here as the "
|
||||
"location cannot be automatically detected."
|
||||
msgstr ""
|
||||
|
|
@ -48,13 +54,13 @@ msgstr ""
|
|||
"actualizar el archivo de alineaciones correspondiente, debe proporcionar un "
|
||||
"valor aquí ya que la ubicación no se puede detectar automáticamente."
|
||||
|
||||
#: tools/mask/cli.py:47
|
||||
#: tools/mask/cli.py:51
|
||||
msgid "Directory containing extracted faces, source frames, or a video file."
|
||||
msgstr ""
|
||||
"Directorio que contiene las caras extraídas, los fotogramas de origen o un "
|
||||
"archivo de vídeo."
|
||||
|
||||
#: tools/mask/cli.py:56
|
||||
#: tools/mask/cli.py:61
|
||||
msgid ""
|
||||
"R|Whether the `input` is a folder of faces or a folder frames/video\n"
|
||||
"L|faces: The input is a folder containing extracted faces.\n"
|
||||
|
|
@ -64,7 +70,7 @@ msgstr ""
|
|||
"L|faces: La entrada es una carpeta que contiene caras extraídas.\n"
|
||||
"L|frames: La entrada es una carpeta que contiene fotogramas o es un vídeo"
|
||||
|
||||
#: tools/mask/cli.py:65
|
||||
#: tools/mask/cli.py:71
|
||||
msgid ""
|
||||
"R|Run the mask tool on multiple sources. If selected then the other options "
|
||||
"should be set as follows:\n"
|
||||
|
|
@ -89,11 +95,11 @@ msgstr ""
|
|||
"con 'caras' como tipo de entrada, solo se actualizará el encabezado PNG "
|
||||
"dentro de las caras extraídas."
|
||||
|
||||
#: tools/mask/cli.py:81 tools/mask/cli.py:113
|
||||
#: tools/mask/cli.py:87 tools/mask/cli.py:119
|
||||
msgid "process"
|
||||
msgstr "proceso"
|
||||
|
||||
#: tools/mask/cli.py:82
|
||||
#: tools/mask/cli.py:89
|
||||
msgid ""
|
||||
"R|Masker to use.\n"
|
||||
"L|bisenet-fp: Relatively lightweight NN based mask that provides more "
|
||||
|
|
@ -118,9 +124,8 @@ msgid ""
|
|||
"some facial obstructions (hands and eyeglasses). Profile faces may result in "
|
||||
"sub-par performance.\n"
|
||||
"L|unet-dfl: Mask designed to provide smart segmentation of mostly frontal "
|
||||
"faces. The mask model has been trained by community members and will need "
|
||||
"testing for further description. Profile faces may result in sub-par "
|
||||
"performance."
|
||||
"faces. The mask model has been trained by community members. Profile faces "
|
||||
"may result in sub-par performance."
|
||||
msgstr ""
|
||||
"R|Máscara a utilizar.\n"
|
||||
"L|bisenet-fp: Máscara relativamente ligera basada en NN que proporciona un "
|
||||
|
|
@ -152,32 +157,111 @@ msgstr ""
|
|||
"descripción. Los rostros de perfil pueden dar lugar a un rendimiento "
|
||||
"inferior."
|
||||
|
||||
#: tools/mask/cli.py:114
|
||||
#: tools/mask/cli.py:121
|
||||
msgid ""
|
||||
"R|Whether to update all masks in the alignments files, only those faces that "
|
||||
"do not already have a mask of the given `mask type` or just to output the "
|
||||
"masks to the `output` location.\n"
|
||||
"L|all: Update the mask for all faces in the alignments file.\n"
|
||||
"R|The Mask tool process to perform.\n"
|
||||
"L|all: Update the mask for all faces in the alignments file for the selected "
|
||||
"'masker'.\n"
|
||||
"L|missing: Create a mask for all faces in the alignments file where a mask "
|
||||
"does not previously exist.\n"
|
||||
"L|output: Don't update the masks, just output them for review in the given "
|
||||
"output folder."
|
||||
"does not previously exist for the selected 'masker'.\n"
|
||||
"L|output: Don't update the masks, just output the selected 'masker' for "
|
||||
"review/editing in external tools to the given output folder.\n"
|
||||
"L|import: Import masks that have been edited outside of faceswap into the "
|
||||
"alignments file. Note: 'custom' must be the selected 'masker' and the masks "
|
||||
"must be in the same format as the 'input-type' (frames or faces)"
|
||||
msgstr ""
|
||||
"R|Si se actualizan todas las máscaras en los archivos de alineación, sólo "
|
||||
"aquellas caras que no tienen ya una máscara del \"tipo de máscara\" dado o "
|
||||
"sólo se envían las máscaras a la ubicación \"de salida\".\n"
|
||||
"L|all: Actualiza la máscara de todas las caras del archivo de alineación.\n"
|
||||
"L|missing: Crea una máscara para todas las caras del fichero de alineaciones "
|
||||
"en las que no existe una máscara previamente.\n"
|
||||
"L|output: No actualiza las máscaras, sólo las emite para su revisión en la "
|
||||
"carpeta de salida dada."
|
||||
"R|Процесс инструмента «Маска», который необходимо выполнить.\n"
|
||||
"L|all: обновить маску для всех лиц в файле выравниваний для выбранного "
|
||||
"«masker».\n"
|
||||
"L|missing: создать маску для всех граней в файле выравниваний, где маска "
|
||||
"ранее не существовала для выбранного «masker».\n"
|
||||
"L|output: не обновляйте маски, просто выведите выбранный «masker» для "
|
||||
"просмотра/редактирования во внешних инструментах в данную выходную папку.\n"
|
||||
"L|import: импортируйте маски, которые были отредактированы вне Facewap, в "
|
||||
"файл выравниваний. Примечание. «custom» должен быть выбранным «masker», а "
|
||||
"маски должны быть в том же формате, что и «input-type» (frames или faces)."
|
||||
|
||||
#: tools/mask/cli.py:127 tools/mask/cli.py:134 tools/mask/cli.py:147
|
||||
#: tools/mask/cli.py:160 tools/mask/cli.py:169
|
||||
#: tools/mask/cli.py:135 tools/mask/cli.py:154 tools/mask/cli.py:174
|
||||
msgid "import"
|
||||
msgstr "importar"
|
||||
|
||||
#: tools/mask/cli.py:137
|
||||
msgid ""
|
||||
"R|Import only. The path to the folder that contains masks to be imported.\n"
|
||||
"L|How the masks are provided is not important, but they will be stored, "
|
||||
"internally, as 8-bit grayscale images.\n"
|
||||
"L|If the input are images, then the masks must be named exactly the same as "
|
||||
"input frames/faces (excluding the file extension).\n"
|
||||
"L|If the input is a video file, then the filename of the masks is not "
|
||||
"important but should contain the frame number at the end of the filename "
|
||||
"(but before the file extension). The frame number can be separated from the "
|
||||
"rest of the filename by any non-numeric character and can be padded by any "
|
||||
"number of zeros. The frame number must correspond correctly to the frame "
|
||||
"number in the original video (starting from frame 1)."
|
||||
msgstr ""
|
||||
"R|Sólo importar. La ruta a la carpeta que contiene las máscaras que se "
|
||||
"importarán.\n"
|
||||
"L|Cómo se proporcionan las máscaras no es importante, pero se almacenarán "
|
||||
"internamente como imágenes en escala de grises de 8 bits.\n"
|
||||
"L|Si la entrada son imágenes, entonces las máscaras deben tener el mismo "
|
||||
"nombre que los cuadros/caras de entrada (excluyendo la extensión del "
|
||||
"archivo).\n"
|
||||
"L|Si la entrada es un archivo de vídeo, entonces el nombre del archivo de "
|
||||
"las máscaras no es importante pero debe contener el número de fotograma al "
|
||||
"final del nombre del archivo (pero antes de la extensión del archivo). El "
|
||||
"número de fotograma se puede separar del resto del nombre del archivo "
|
||||
"mediante cualquier carácter no numérico y se puede rellenar con cualquier "
|
||||
"número de ceros. El número de fotograma debe corresponder correctamente al "
|
||||
"número de fotograma del vídeo original (a partir del fotograma 1)."
|
||||
|
||||
#: tools/mask/cli.py:156
|
||||
msgid ""
|
||||
"R|Import only. The centering to use when importing masks. Note: For any job "
|
||||
"other than 'import' this option is ignored as mask centering is handled "
|
||||
"internally.\n"
|
||||
"L|face: Centers the mask on the center of the face, adjusting for pitch and "
|
||||
"yaw. Outside of requirements for full head masking/training, this is likely "
|
||||
"to be the best choice.\n"
|
||||
"L|head: Centers the mask on the center of the head, adjusting for pitch and "
|
||||
"yaw. Note: You should only select head centering if you intend to include "
|
||||
"the full head (including hair) within the mask and are looking to train a "
|
||||
"full head model.\n"
|
||||
"L|legacy: The 'original' extraction technique. Centers the mask near the of "
|
||||
"the nose with and crops closely to the face. Can result in the edges of the "
|
||||
"mask appearing outside of the training area."
|
||||
msgstr ""
|
||||
"R|Sólo importar. El centrado a utilizar al importar máscaras. Nota: Para "
|
||||
"cualquier trabajo que no sea \"importar\", esta opción se ignora ya que el "
|
||||
"centrado de la máscara se maneja internamente.\n"
|
||||
"L|cara: centra la máscara en el centro de la cara, ajustando el tono y la "
|
||||
"orientación. Aparte de los requisitos para el entrenamiento/enmascaramiento "
|
||||
"de cabeza completa, esta probablemente sea la mejor opción.\n"
|
||||
"L|head: centra la máscara en el centro de la cabeza, ajustando el cabeceo y "
|
||||
"la guiñada. Nota: Sólo debe seleccionar el centrado de la cabeza si desea "
|
||||
"incluir la cabeza completa (incluido el cabello) dentro de la máscara y "
|
||||
"desea entrenar un modelo de cabeza completa.\n"
|
||||
"L|legacy: La técnica de extracción 'original'. Centra la máscara cerca de la "
|
||||
"nariz y la recorta cerca de la cara. Puede provocar que los bordes de la "
|
||||
"máscara aparezcan fuera del área de entrenamiento."
|
||||
|
||||
#: tools/mask/cli.py:179
|
||||
msgid ""
|
||||
"Import only. The size, in pixels to internally store the mask at.\n"
|
||||
"The default is 128 which is fine for nearly all usecases. Larger sizes will "
|
||||
"result in larger alignments files and longer processing."
|
||||
msgstr ""
|
||||
"Sólo importar. El tamaño, en píxeles, para almacenar internamente la "
|
||||
"máscara.\n"
|
||||
"El valor predeterminado es 128, que está bien para casi todos los casos de "
|
||||
"uso. Los tamaños más grandes darán como resultado archivos de alineaciones "
|
||||
"más grandes y un procesamiento más largo."
|
||||
|
||||
#: tools/mask/cli.py:187 tools/mask/cli.py:195 tools/mask/cli.py:209
|
||||
#: tools/mask/cli.py:223 tools/mask/cli.py:233
|
||||
msgid "output"
|
||||
msgstr "salida"
|
||||
|
||||
#: tools/mask/cli.py:128
|
||||
#: tools/mask/cli.py:189
|
||||
msgid ""
|
||||
"Optional output location. If provided, a preview of the masks created will "
|
||||
"be output in the given folder."
|
||||
|
|
@ -185,7 +269,7 @@ msgstr ""
|
|||
"Ubicación de salida opcional. Si se proporciona, se obtendrá una vista "
|
||||
"previa de las máscaras creadas en la carpeta indicada."
|
||||
|
||||
#: tools/mask/cli.py:138
|
||||
#: tools/mask/cli.py:200
|
||||
msgid ""
|
||||
"Apply gaussian blur to the mask output. Has the effect of smoothing the "
|
||||
"edges of the mask giving less of a hard edge. the size is in pixels. This "
|
||||
|
|
@ -198,7 +282,7 @@ msgstr ""
|
|||
"redondeará al siguiente número impar. NB: Sólo afecta a la vista previa de "
|
||||
"salida. Si se ajusta a 0, se desactiva"
|
||||
|
||||
#: tools/mask/cli.py:151
|
||||
#: tools/mask/cli.py:214
|
||||
msgid ""
|
||||
"Helps reduce 'blotchiness' on some masks by making light shades white and "
|
||||
"dark shades black. Higher values will impact more of the mask. NB: Only "
|
||||
|
|
@ -209,7 +293,7 @@ msgstr ""
|
|||
"más a la máscara. NB: Sólo afecta a la vista previa de salida. Si se ajusta "
|
||||
"a 0, se desactiva"
|
||||
|
||||
#: tools/mask/cli.py:161
|
||||
#: tools/mask/cli.py:225
|
||||
msgid ""
|
||||
"R|How to format the output when processing is set to 'output'.\n"
|
||||
"L|combined: The image contains the face/frame, face mask and masked face.\n"
|
||||
|
|
@ -224,7 +308,7 @@ msgstr ""
|
|||
"enmascarada.\n"
|
||||
"L|mask: Sólo emite la máscara como una imagen de un solo canal."
|
||||
|
||||
#: tools/mask/cli.py:170
|
||||
#: tools/mask/cli.py:235
|
||||
msgid ""
|
||||
"R|Whether to output the whole frame or only the face box when using output "
|
||||
"processing. Only has an effect when using frames as input."
|
||||
|
|
@ -233,6 +317,26 @@ msgstr ""
|
|||
"el cuadro de la cara cuando se utiliza el procesamiento de salida. Sólo "
|
||||
"tiene efecto cuando se utilizan cuadros como entrada."
|
||||
|
||||
#~ msgid ""
|
||||
#~ "R|Whether to update all masks in the alignments files, only those faces "
|
||||
#~ "that do not already have a mask of the given `mask type` or just to "
|
||||
#~ "output the masks to the `output` location.\n"
|
||||
#~ "L|all: Update the mask for all faces in the alignments file.\n"
|
||||
#~ "L|missing: Create a mask for all faces in the alignments file where a "
|
||||
#~ "mask does not previously exist.\n"
|
||||
#~ "L|output: Don't update the masks, just output them for review in the "
|
||||
#~ "given output folder."
|
||||
#~ msgstr ""
|
||||
#~ "R|Si se actualizan todas las máscaras en los archivos de alineación, sólo "
|
||||
#~ "aquellas caras que no tienen ya una máscara del \"tipo de máscara\" dado "
|
||||
#~ "o sólo se envían las máscaras a la ubicación \"de salida\".\n"
|
||||
#~ "L|all: Actualiza la máscara de todas las caras del archivo de "
|
||||
#~ "alineación.\n"
|
||||
#~ "L|missing: Crea una máscara para todas las caras del fichero de "
|
||||
#~ "alineaciones en las que no existe una máscara previamente.\n"
|
||||
#~ "L|output: No actualiza las máscaras, sólo las emite para su revisión en "
|
||||
#~ "la carpeta de salida dada."
|
||||
|
||||
#~ msgid ""
|
||||
#~ "Full path to the alignments file to add the mask to. NB: if the mask "
|
||||
#~ "already exists in the alignments file it will be overwritten."
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -7,8 +7,8 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-02-20 23:42+0000\n"
|
||||
"PO-Revision-Date: 2023-02-20 23:44+0000\n"
|
||||
"POT-Creation-Date: 2024-03-11 23:45+0000\n"
|
||||
"PO-Revision-Date: 2024-03-11 23:50+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Language: ko_KR\n"
|
||||
|
|
@ -16,29 +16,34 @@ msgstr ""
|
|||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Poedit 3.0.1\n"
|
||||
"X-Generator: Poedit 3.4.2\n"
|
||||
|
||||
#: tools/mask/cli.py:15
|
||||
msgid "This command lets you generate masks for existing alignments."
|
||||
msgstr "이 명령어는 이미 존재하는 alignments로부터 마스크를 생성하게 해줍니다."
|
||||
msgid ""
|
||||
"This tool allows you to generate, import, export or preview masks for "
|
||||
"existing alignments."
|
||||
msgstr ""
|
||||
"이 도구를 사용하면 기존 정렬에 대한 마스크를 생성, 가져오기, 내보내기 또는 미"
|
||||
"리 볼 수 있습니다."
|
||||
|
||||
#: tools/mask/cli.py:24
|
||||
#: tools/mask/cli.py:25
|
||||
msgid ""
|
||||
"Mask tool\n"
|
||||
"Generate masks for existing alignments files."
|
||||
"Generate, import, export or preview masks for existing alignments files."
|
||||
msgstr ""
|
||||
"마스크 도구\n"
|
||||
"존재하는 alignments 파일들로부터 마스크를 생성합니다."
|
||||
"기존 alignments 파일에 대한 마스크를 생성, 가져오기, 내보내기 또는 미리 봅니"
|
||||
"다."
|
||||
|
||||
#: tools/mask/cli.py:33 tools/mask/cli.py:44 tools/mask/cli.py:54
|
||||
#: tools/mask/cli.py:64
|
||||
#: tools/mask/cli.py:35 tools/mask/cli.py:47 tools/mask/cli.py:58
|
||||
#: tools/mask/cli.py:69
|
||||
msgid "data"
|
||||
msgstr "데이터"
|
||||
|
||||
#: tools/mask/cli.py:36
|
||||
#: tools/mask/cli.py:39
|
||||
msgid ""
|
||||
"Full path to the alignments file to add the mask to if not at the default "
|
||||
"location. NB: If the input-type is faces and you wish to update the "
|
||||
"Full path to the alignments file that contains the masks if not at the "
|
||||
"default location. NB: If the input-type is faces and you wish to update the "
|
||||
"corresponding alignments file, then you must provide a value here as the "
|
||||
"location cannot be automatically detected."
|
||||
msgstr ""
|
||||
|
|
@ -46,11 +51,11 @@ msgstr ""
|
|||
"유형이 얼굴이고 해당 정렬 파일을 업데이트하려는 경우 위치를 자동으로 감지할 "
|
||||
"수 없으므로 여기에 값을 제공해야 합니다."
|
||||
|
||||
#: tools/mask/cli.py:47
|
||||
#: tools/mask/cli.py:51
|
||||
msgid "Directory containing extracted faces, source frames, or a video file."
|
||||
msgstr "추출된 얼굴들, 원본 프레임들, 또는 비디오 파일이 존재하는 디렉토리."
|
||||
|
||||
#: tools/mask/cli.py:56
|
||||
#: tools/mask/cli.py:61
|
||||
msgid ""
|
||||
"R|Whether the `input` is a folder of faces or a folder frames/video\n"
|
||||
"L|faces: The input is a folder containing extracted faces.\n"
|
||||
|
|
@ -60,7 +65,7 @@ msgstr ""
|
|||
"L|faces: 입력은 추출된 얼굴을 포함된 폴더입니다.\n"
|
||||
"L|frames: 입력이 프레임을 포함된 폴더이거나 비디오입니다"
|
||||
|
||||
#: tools/mask/cli.py:65
|
||||
#: tools/mask/cli.py:71
|
||||
msgid ""
|
||||
"R|Run the mask tool on multiple sources. If selected then the other options "
|
||||
"should be set as follows:\n"
|
||||
|
|
@ -83,11 +88,11 @@ msgstr ""
|
|||
"(프레임용)에 있어야 합니다. 입력 유형이 '얼굴'인 마스크를 일괄 처리하는 경우 "
|
||||
"추출된 얼굴 내의 PNG 헤더만 업데이트됩니다."
|
||||
|
||||
#: tools/mask/cli.py:81 tools/mask/cli.py:113
|
||||
#: tools/mask/cli.py:87 tools/mask/cli.py:119
|
||||
msgid "process"
|
||||
msgstr "진행"
|
||||
|
||||
#: tools/mask/cli.py:82
|
||||
#: tools/mask/cli.py:89
|
||||
msgid ""
|
||||
"R|Masker to use.\n"
|
||||
"L|bisenet-fp: Relatively lightweight NN based mask that provides more "
|
||||
|
|
@ -112,9 +117,8 @@ msgid ""
|
|||
"some facial obstructions (hands and eyeglasses). Profile faces may result in "
|
||||
"sub-par performance.\n"
|
||||
"L|unet-dfl: Mask designed to provide smart segmentation of mostly frontal "
|
||||
"faces. The mask model has been trained by community members and will need "
|
||||
"testing for further description. Profile faces may result in sub-par "
|
||||
"performance."
|
||||
"faces. The mask model has been trained by community members. Profile faces "
|
||||
"may result in sub-par performance."
|
||||
msgstr ""
|
||||
"R|사용할 마스크.\n"
|
||||
"L|bisnet-fp: 전체 얼굴 마스킹(마스크 설정에서 구성 가능)을 포함하여 마스킹할 "
|
||||
|
|
@ -137,32 +141,102 @@ msgstr ""
|
|||
"델은 커뮤니티 구성원들에 의해 훈련되었으며 추가 설명을 위해 테스트가 필요합니"
|
||||
"다. 옆 얼굴은 평균 이하의 성능을 초래할 수 있습니다."
|
||||
|
||||
#: tools/mask/cli.py:114
|
||||
#: tools/mask/cli.py:121
|
||||
msgid ""
|
||||
"R|Whether to update all masks in the alignments files, only those faces that "
|
||||
"do not already have a mask of the given `mask type` or just to output the "
|
||||
"masks to the `output` location.\n"
|
||||
"L|all: Update the mask for all faces in the alignments file.\n"
|
||||
"R|The Mask tool process to perform.\n"
|
||||
"L|all: Update the mask for all faces in the alignments file for the selected "
|
||||
"'masker'.\n"
|
||||
"L|missing: Create a mask for all faces in the alignments file where a mask "
|
||||
"does not previously exist.\n"
|
||||
"L|output: Don't update the masks, just output them for review in the given "
|
||||
"output folder."
|
||||
"does not previously exist for the selected 'masker'.\n"
|
||||
"L|output: Don't update the masks, just output the selected 'masker' for "
|
||||
"review/editing in external tools to the given output folder.\n"
|
||||
"L|import: Import masks that have been edited outside of faceswap into the "
|
||||
"alignments file. Note: 'custom' must be the selected 'masker' and the masks "
|
||||
"must be in the same format as the 'input-type' (frames or faces)"
|
||||
msgstr ""
|
||||
"R|alignments 파일의 모든 마스크를 업데이트할지, 지정된 '마스크 유형'의 마스크"
|
||||
"가 아직 없는 페이스만 업데이트할지, 아니면 단순히 '출력' 위치로 마스크를 출력"
|
||||
"할지 여부.\n"
|
||||
"L|all: alignments 파일의 모든 얼굴에 대한 마스크를 업데이트합니다.\n"
|
||||
"L|missing: 마스크가 없었던 alignments 파일의 모든 얼굴에 대한 마스크를 만듭니"
|
||||
"다.\n"
|
||||
"L|output: 마스크를 업데이트하지 말고 지정된 출력 폴더에서 검토할 수 있도록 출"
|
||||
"력하십시오."
|
||||
"R|수행할 마스크 도구 프로세스입니다.\n"
|
||||
"L|all: 선택한 'masker'에 대한 정렬 파일의 모든 면에 대한 마스크를 업데이트합"
|
||||
"니다.\n"
|
||||
"L|missing: 선택한 'masker'에 대해 이전에 마스크가 존재하지 않았던 정렬 파일"
|
||||
"의 모든 면에 대한 마스크를 생성합니다.\n"
|
||||
"L|output: 마스크를 업데이트하지 않고 외부 도구에서 검토/편집하기 위해 선택한 "
|
||||
"'masker'를 지정된 출력 폴더로 출력합니다.\n"
|
||||
"L|import: Faceswap 외부에서 편집된 마스크를 정렬 파일로 가져옵니다. 참고: "
|
||||
"'custom'은 선택된 'masker'여야 하며 마스크는 'input-type'(frames 또는 faces)"
|
||||
"과 동일한 형식이어야 합니다."
|
||||
|
||||
#: tools/mask/cli.py:127 tools/mask/cli.py:134 tools/mask/cli.py:147
|
||||
#: tools/mask/cli.py:160 tools/mask/cli.py:169
|
||||
#: tools/mask/cli.py:135 tools/mask/cli.py:154 tools/mask/cli.py:174
|
||||
msgid "import"
|
||||
msgstr "수입"
|
||||
|
||||
#: tools/mask/cli.py:137
|
||||
msgid ""
|
||||
"R|Import only. The path to the folder that contains masks to be imported.\n"
|
||||
"L|How the masks are provided is not important, but they will be stored, "
|
||||
"internally, as 8-bit grayscale images.\n"
|
||||
"L|If the input are images, then the masks must be named exactly the same as "
|
||||
"input frames/faces (excluding the file extension).\n"
|
||||
"L|If the input is a video file, then the filename of the masks is not "
|
||||
"important but should contain the frame number at the end of the filename "
|
||||
"(but before the file extension). The frame number can be separated from the "
|
||||
"rest of the filename by any non-numeric character and can be padded by any "
|
||||
"number of zeros. The frame number must correspond correctly to the frame "
|
||||
"number in the original video (starting from frame 1)."
|
||||
msgstr ""
|
||||
"R|가져오기만 가능합니다. 가져올 마스크가 포함된 폴더의 경로입니다.\n"
|
||||
"L|마스크 제공 방법은 중요하지 않지만 내부적으로 8비트 회색조 이미지로 저장됩"
|
||||
"니다.\n"
|
||||
"L|입력이 이미지인 경우 마스크 이름은 입력 프레임/얼굴과 정확히 동일하게 지정"
|
||||
"되어야 합니다(파일 확장자 제외).\n"
|
||||
"L|입력이 비디오 파일인 경우 마스크의 파일 이름은 중요하지 않지만 파일 이름 끝"
|
||||
"에(파일 확장자 앞에) 프레임 번호가 포함되어야 합니다. 프레임 번호는 숫자가 아"
|
||||
"닌 문자로 파일 이름의 나머지 부분과 구분될 수 있으며 임의 개수의 0으로 채워"
|
||||
"질 수 있습니다. 프레임 번호는 원본 비디오의 프레임 번호(프레임 1부터 시작)와 "
|
||||
"정확하게 일치해야 합니다."
|
||||
|
||||
#: tools/mask/cli.py:156
|
||||
msgid ""
|
||||
"R|Import only. The centering to use when importing masks. Note: For any job "
|
||||
"other than 'import' this option is ignored as mask centering is handled "
|
||||
"internally.\n"
|
||||
"L|face: Centers the mask on the center of the face, adjusting for pitch and "
|
||||
"yaw. Outside of requirements for full head masking/training, this is likely "
|
||||
"to be the best choice.\n"
|
||||
"L|head: Centers the mask on the center of the head, adjusting for pitch and "
|
||||
"yaw. Note: You should only select head centering if you intend to include "
|
||||
"the full head (including hair) within the mask and are looking to train a "
|
||||
"full head model.\n"
|
||||
"L|legacy: The 'original' extraction technique. Centers the mask near the of "
|
||||
"the nose with and crops closely to the face. Can result in the edges of the "
|
||||
"mask appearing outside of the training area."
|
||||
msgstr ""
|
||||
"R|가져오기만. 마스크를 가져올 때 사용할 센터링입니다. 참고: '가져오기' 이외"
|
||||
"의 모든 작업의 경우 마스크 센터링이 내부적으로 처리되므로 이 옵션은 무시됩니"
|
||||
"다.\n"
|
||||
"L|면: 피치와 요를 조정하여 마스크를 얼굴 중앙에 배치합니다. 머리 전체 마스킹/"
|
||||
"훈련에 대한 요구 사항을 제외하면 이것이 최선의 선택일 가능성이 높습니다.\n"
|
||||
"L|head: 마스크를 머리 중앙에 배치하여 피치와 요를 조정합니다. 참고: 마스크 내"
|
||||
"에 머리 전체(머리카락 포함)를 포함하고 머리 전체 모델을 훈련시키려는 경우 머"
|
||||
"리 중심 맞추기만 선택해야 합니다.\n"
|
||||
"L|레거시: '원래' 추출 기술입니다. 마스크를 코 근처 중앙에 배치하고 얼굴에 가"
|
||||
"깝게 자릅니다. 마스크 가장자리가 훈련 영역 외부에 나타날 수 있습니다."
|
||||
|
||||
#: tools/mask/cli.py:179
|
||||
msgid ""
|
||||
"Import only. The size, in pixels to internally store the mask at.\n"
|
||||
"The default is 128 which is fine for nearly all usecases. Larger sizes will "
|
||||
"result in larger alignments files and longer processing."
|
||||
msgstr ""
|
||||
"가져오기만. 마스크를 내부적으로 저장할 크기(픽셀)입니다.\n"
|
||||
"기본값은 128이며 거의 모든 사용 사례에 적합합니다. 크기가 클수록 정렬 파일도 "
|
||||
"커지고 처리 시간도 길어집니다."
|
||||
|
||||
#: tools/mask/cli.py:187 tools/mask/cli.py:195 tools/mask/cli.py:209
|
||||
#: tools/mask/cli.py:223 tools/mask/cli.py:233
|
||||
msgid "output"
|
||||
msgstr "출력"
|
||||
|
||||
#: tools/mask/cli.py:128
|
||||
#: tools/mask/cli.py:189
|
||||
msgid ""
|
||||
"Optional output location. If provided, a preview of the masks created will "
|
||||
"be output in the given folder."
|
||||
|
|
@ -170,7 +244,7 @@ msgstr ""
|
|||
"선택적 출력 위치. 만약 값이 제공된다면 생성된 마스크 미리 보기가 주어진 폴더"
|
||||
"에 출력됩니다."
|
||||
|
||||
#: tools/mask/cli.py:138
|
||||
#: tools/mask/cli.py:200
|
||||
msgid ""
|
||||
"Apply gaussian blur to the mask output. Has the effect of smoothing the "
|
||||
"edges of the mask giving less of a hard edge. the size is in pixels. This "
|
||||
|
|
@ -182,7 +256,7 @@ msgstr ""
|
|||
"은 홀수여야 하며 짝수가 전달되면 다음 홀수로 반올림됩니다. NB: 출력 미리 보기"
|
||||
"에만 영향을 줍니다. 0으로 설정하면 꺼집니다"
|
||||
|
||||
#: tools/mask/cli.py:151
|
||||
#: tools/mask/cli.py:214
|
||||
msgid ""
|
||||
"Helps reduce 'blotchiness' on some masks by making light shades white and "
|
||||
"dark shades black. Higher values will impact more of the mask. NB: Only "
|
||||
|
|
@ -192,7 +266,7 @@ msgstr ""
|
|||
"줄이는 데 도움이 됩니다. 값이 클수록 마스크에 더 많은 영향을 미칩니다. NB: 출"
|
||||
"력 미리 보기에만 영향을 줍니다. 0으로 설정하면 꺼집니다"
|
||||
|
||||
#: tools/mask/cli.py:161
|
||||
#: tools/mask/cli.py:225
|
||||
msgid ""
|
||||
"R|How to format the output when processing is set to 'output'.\n"
|
||||
"L|combined: The image contains the face/frame, face mask and masked face.\n"
|
||||
|
|
@ -205,7 +279,7 @@ msgstr ""
|
|||
"L|masked: 마스크된 얼굴/프레임을 Rgba 이미지로 출력합니다.\n"
|
||||
"L|mask: 마스크를 단일 채널 이미지로만 출력합니다."
|
||||
|
||||
#: tools/mask/cli.py:170
|
||||
#: tools/mask/cli.py:235
|
||||
msgid ""
|
||||
"R|Whether to output the whole frame or only the face box when using output "
|
||||
"processing. Only has an effect when using frames as input."
|
||||
|
|
@ -213,6 +287,25 @@ msgstr ""
|
|||
"R|출력 처리를 사용할 때 전체 프레임을 출력할지 또는 페이스 박스만 출력할지 여"
|
||||
"부. 프레임을 입력으로 사용할 때만 효과가 있습니다."
|
||||
|
||||
#~ msgid ""
|
||||
#~ "R|Whether to update all masks in the alignments files, only those faces "
|
||||
#~ "that do not already have a mask of the given `mask type` or just to "
|
||||
#~ "output the masks to the `output` location.\n"
|
||||
#~ "L|all: Update the mask for all faces in the alignments file.\n"
|
||||
#~ "L|missing: Create a mask for all faces in the alignments file where a "
|
||||
#~ "mask does not previously exist.\n"
|
||||
#~ "L|output: Don't update the masks, just output them for review in the "
|
||||
#~ "given output folder."
|
||||
#~ msgstr ""
|
||||
#~ "R|alignments 파일의 모든 마스크를 업데이트할지, 지정된 '마스크 유형'의 마"
|
||||
#~ "스크가 아직 없는 페이스만 업데이트할지, 아니면 단순히 '출력' 위치로 마스크"
|
||||
#~ "를 출력할지 여부.\n"
|
||||
#~ "L|all: alignments 파일의 모든 얼굴에 대한 마스크를 업데이트합니다.\n"
|
||||
#~ "L|missing: 마스크가 없었던 alignments 파일의 모든 얼굴에 대한 마스크를 만"
|
||||
#~ "듭니다.\n"
|
||||
#~ "L|output: 마스크를 업데이트하지 말고 지정된 출력 폴더에서 검토할 수 있도"
|
||||
#~ "록 출력하십시오."
|
||||
|
||||
#~ msgid ""
|
||||
#~ "Full path to the alignments file to add the mask to. NB: if the mask "
|
||||
#~ "already exists in the alignments file it will be overwritten."
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -7,8 +7,8 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-02-20 23:42+0000\n"
|
||||
"PO-Revision-Date: 2023-04-11 16:07+0700\n"
|
||||
"POT-Creation-Date: 2024-03-11 23:45+0000\n"
|
||||
"PO-Revision-Date: 2024-03-11 23:50+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Language: ru\n"
|
||||
|
|
@ -17,30 +17,34 @@ msgstr ""
|
|||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
|
||||
"n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n"
|
||||
"X-Generator: Poedit 3.2.2\n"
|
||||
"X-Generator: Poedit 3.4.2\n"
|
||||
|
||||
#: tools/mask/cli.py:15
|
||||
msgid "This command lets you generate masks for existing alignments."
|
||||
msgid ""
|
||||
"This tool allows you to generate, import, export or preview masks for "
|
||||
"existing alignments."
|
||||
msgstr ""
|
||||
"Эта команда позволяет генерировать маски для существующих выравниваний."
|
||||
"Этот инструмент позволяет создавать, импортировать, экспортировать или "
|
||||
"просматривать маски для существующих трасс."
|
||||
|
||||
#: tools/mask/cli.py:24
|
||||
#: tools/mask/cli.py:25
|
||||
msgid ""
|
||||
"Mask tool\n"
|
||||
"Generate masks for existing alignments files."
|
||||
"Generate, import, export or preview masks for existing alignments files."
|
||||
msgstr ""
|
||||
"Инструмент \"Маска\"\n"
|
||||
"Создавайте маски для существующих файлов выравнивания."
|
||||
"Создавайте, импортируйте, экспортируйте или просматривайте маски для "
|
||||
"существующих файлов трасс."
|
||||
|
||||
#: tools/mask/cli.py:33 tools/mask/cli.py:44 tools/mask/cli.py:54
|
||||
#: tools/mask/cli.py:64
|
||||
#: tools/mask/cli.py:35 tools/mask/cli.py:47 tools/mask/cli.py:58
|
||||
#: tools/mask/cli.py:69
|
||||
msgid "data"
|
||||
msgstr "данные"
|
||||
|
||||
#: tools/mask/cli.py:36
|
||||
#: tools/mask/cli.py:39
|
||||
msgid ""
|
||||
"Full path to the alignments file to add the mask to if not at the default "
|
||||
"location. NB: If the input-type is faces and you wish to update the "
|
||||
"Full path to the alignments file that contains the masks if not at the "
|
||||
"default location. NB: If the input-type is faces and you wish to update the "
|
||||
"corresponding alignments file, then you must provide a value here as the "
|
||||
"location cannot be automatically detected."
|
||||
msgstr ""
|
||||
|
|
@ -49,11 +53,11 @@ msgstr ""
|
|||
"обновить соответствующий файл выравнивания, то вы должны указать значение "
|
||||
"здесь, так как местоположение не может быть определено автоматически."
|
||||
|
||||
#: tools/mask/cli.py:47
|
||||
#: tools/mask/cli.py:51
|
||||
msgid "Directory containing extracted faces, source frames, or a video file."
|
||||
msgstr "Папка, содержащая извлеченные лица, исходные кадры или видеофайл."
|
||||
|
||||
#: tools/mask/cli.py:56
|
||||
#: tools/mask/cli.py:61
|
||||
msgid ""
|
||||
"R|Whether the `input` is a folder of faces or a folder frames/video\n"
|
||||
"L|faces: The input is a folder containing extracted faces.\n"
|
||||
|
|
@ -63,7 +67,7 @@ msgstr ""
|
|||
"L|faces: Входом является папка, содержащая извлеченные лица.\n"
|
||||
"L|frames: Входом является папка с кадрами или видео"
|
||||
|
||||
#: tools/mask/cli.py:65
|
||||
#: tools/mask/cli.py:71
|
||||
msgid ""
|
||||
"R|Run the mask tool on multiple sources. If selected then the other options "
|
||||
"should be set as follows:\n"
|
||||
|
|
@ -87,11 +91,11 @@ msgstr ""
|
|||
"При пакетной обработке масок с типом входа \"лица\" будут обновлены только "
|
||||
"заголовки PNG в извлеченных лицах."
|
||||
|
||||
#: tools/mask/cli.py:81 tools/mask/cli.py:113
|
||||
#: tools/mask/cli.py:87 tools/mask/cli.py:119
|
||||
msgid "process"
|
||||
msgstr "обработка"
|
||||
|
||||
#: tools/mask/cli.py:82
|
||||
#: tools/mask/cli.py:89
|
||||
msgid ""
|
||||
"R|Masker to use.\n"
|
||||
"L|bisenet-fp: Relatively lightweight NN based mask that provides more "
|
||||
|
|
@ -116,9 +120,8 @@ msgid ""
|
|||
"some facial obstructions (hands and eyeglasses). Profile faces may result in "
|
||||
"sub-par performance.\n"
|
||||
"L|unet-dfl: Mask designed to provide smart segmentation of mostly frontal "
|
||||
"faces. The mask model has been trained by community members and will need "
|
||||
"testing for further description. Profile faces may result in sub-par "
|
||||
"performance."
|
||||
"faces. The mask model has been trained by community members. Profile faces "
|
||||
"may result in sub-par performance."
|
||||
msgstr ""
|
||||
"R|Маскер для использования.\n"
|
||||
"L|bisenet-fp: Относительно легкая маска на основе NN, которая обеспечивает "
|
||||
|
|
@ -146,32 +149,110 @@ msgstr ""
|
|||
"сообщества и для дальнейшего описания нуждается в тестировании. Профильные "
|
||||
"лица могут иметь низкую производительность."
|
||||
|
||||
#: tools/mask/cli.py:114
|
||||
#: tools/mask/cli.py:121
|
||||
msgid ""
|
||||
"R|Whether to update all masks in the alignments files, only those faces that "
|
||||
"do not already have a mask of the given `mask type` or just to output the "
|
||||
"masks to the `output` location.\n"
|
||||
"L|all: Update the mask for all faces in the alignments file.\n"
|
||||
"R|The Mask tool process to perform.\n"
|
||||
"L|all: Update the mask for all faces in the alignments file for the selected "
|
||||
"'masker'.\n"
|
||||
"L|missing: Create a mask for all faces in the alignments file where a mask "
|
||||
"does not previously exist.\n"
|
||||
"L|output: Don't update the masks, just output them for review in the given "
|
||||
"output folder."
|
||||
"does not previously exist for the selected 'masker'.\n"
|
||||
"L|output: Don't update the masks, just output the selected 'masker' for "
|
||||
"review/editing in external tools to the given output folder.\n"
|
||||
"L|import: Import masks that have been edited outside of faceswap into the "
|
||||
"alignments file. Note: 'custom' must be the selected 'masker' and the masks "
|
||||
"must be in the same format as the 'input-type' (frames or faces)"
|
||||
msgstr ""
|
||||
"R|Обновлять ли все маски в файлах выравнивания, только те лица, которые еще "
|
||||
"не имеют маски заданного `mask type` или просто выводить маски в место "
|
||||
"`output`.\n"
|
||||
"L|all: Обновить маску для всех лиц в файле выравнивания.\n"
|
||||
"L|missing: Создать маску для всех лиц в файле выравнивания, для которых "
|
||||
"маска ранее не существовала.\n"
|
||||
"L|output: Не обновлять маски, а просто вывести их для просмотра в указанную "
|
||||
"выходную папку."
|
||||
"R|El proceso de la herramienta Máscara a realizar.\n"
|
||||
"L|all: actualiza la máscara de todas las caras en el archivo de alineaciones "
|
||||
"para el 'masker' seleccionado.\n"
|
||||
"L|missing: crea una máscara para todas las caras en el archivo de "
|
||||
"alineaciones donde no existe previamente una máscara para el 'masker' "
|
||||
"seleccionado.\n"
|
||||
"L|output: no actualice las máscaras, simplemente envíe el 'masker' "
|
||||
"seleccionado para su revisión/edición en herramientas externas a la carpeta "
|
||||
"de salida proporcionada.\n"
|
||||
"L|import: importa máscaras que se han editado fuera de faceswap al archivo "
|
||||
"de alineaciones. Nota: 'custom' debe ser el 'masker' seleccionado y las "
|
||||
"máscaras deben tener el mismo formato que el 'input-type' (frames o faces)"
|
||||
|
||||
#: tools/mask/cli.py:127 tools/mask/cli.py:134 tools/mask/cli.py:147
|
||||
#: tools/mask/cli.py:160 tools/mask/cli.py:169
|
||||
#: tools/mask/cli.py:135 tools/mask/cli.py:154 tools/mask/cli.py:174
|
||||
msgid "import"
|
||||
msgstr "Импортировать"
|
||||
|
||||
#: tools/mask/cli.py:137
|
||||
msgid ""
|
||||
"R|Import only. The path to the folder that contains masks to be imported.\n"
|
||||
"L|How the masks are provided is not important, but they will be stored, "
|
||||
"internally, as 8-bit grayscale images.\n"
|
||||
"L|If the input are images, then the masks must be named exactly the same as "
|
||||
"input frames/faces (excluding the file extension).\n"
|
||||
"L|If the input is a video file, then the filename of the masks is not "
|
||||
"important but should contain the frame number at the end of the filename "
|
||||
"(but before the file extension). The frame number can be separated from the "
|
||||
"rest of the filename by any non-numeric character and can be padded by any "
|
||||
"number of zeros. The frame number must correspond correctly to the frame "
|
||||
"number in the original video (starting from frame 1)."
|
||||
msgstr ""
|
||||
"R|Только импорт. Путь к папке, содержащей маски для импорта.\n"
|
||||
"L|Как предоставляются маски, не важно, но они будут храниться внутри как 8-"
|
||||
"битные изображения в оттенках серого.\n"
|
||||
"L|Если входными данными являются изображения, то имена масок должны быть "
|
||||
"точно такими же, как у входных кадров/лиц (за исключением расширения "
|
||||
"файла).\n"
|
||||
"L|Если входной файл представляет собой видеофайл, то имя файла масок не "
|
||||
"важно, но должно содержать номер кадра в конце имени файла (но перед "
|
||||
"расширением файла). Номер кадра может быть отделен от остальной части имени "
|
||||
"файла любым нечисловым символом и дополнен любым количеством нулей. Номер "
|
||||
"кадра должен правильно соответствовать номеру кадра в исходном видео "
|
||||
"(начиная с кадра 1)."
|
||||
|
||||
#: tools/mask/cli.py:156
|
||||
msgid ""
|
||||
"R|Import only. The centering to use when importing masks. Note: For any job "
|
||||
"other than 'import' this option is ignored as mask centering is handled "
|
||||
"internally.\n"
|
||||
"L|face: Centers the mask on the center of the face, adjusting for pitch and "
|
||||
"yaw. Outside of requirements for full head masking/training, this is likely "
|
||||
"to be the best choice.\n"
|
||||
"L|head: Centers the mask on the center of the head, adjusting for pitch and "
|
||||
"yaw. Note: You should only select head centering if you intend to include "
|
||||
"the full head (including hair) within the mask and are looking to train a "
|
||||
"full head model.\n"
|
||||
"L|legacy: The 'original' extraction technique. Centers the mask near the of "
|
||||
"the nose with and crops closely to the face. Can result in the edges of the "
|
||||
"mask appearing outside of the training area."
|
||||
msgstr ""
|
||||
"R|Только импорт. Центрирование, используемое при импорте масок. Примечание. "
|
||||
"Для любого задания, кроме «импорта», этот параметр игнорируется, поскольку "
|
||||
"центрирование маски обрабатывается внутри.\n"
|
||||
"L|face: центрирует маску по центру лица с регулировкой угла наклона и "
|
||||
"отклонения от курса. Помимо требований к полной маскировке/тренировке "
|
||||
"головы, это, вероятно, будет лучшим выбором.\n"
|
||||
"L|head: центрирует маску по центру головы с регулировкой угла наклона и "
|
||||
"отклонения от курса. Примечание. Выбирать центрирование головы следует "
|
||||
"только в том случае, если вы собираетесь включить в маску всю голову "
|
||||
"(включая волосы) и хотите обучить модель полной головы.\n"
|
||||
"L|legacy: «Оригинальная» техника извлечения. Центрирует маску возле носа и "
|
||||
"приближает ее к лицу. Это может привести к тому, что края маски окажутся за "
|
||||
"пределами тренировочной зоны."
|
||||
|
||||
#: tools/mask/cli.py:179
|
||||
msgid ""
|
||||
"Import only. The size, in pixels to internally store the mask at.\n"
|
||||
"The default is 128 which is fine for nearly all usecases. Larger sizes will "
|
||||
"result in larger alignments files and longer processing."
|
||||
msgstr ""
|
||||
"Только импорт. Размер в пикселях для внутреннего хранения маски.\n"
|
||||
"Значение по умолчанию — 128, что подходит практически для всех случаев "
|
||||
"использования. Большие размеры приведут к увеличению размера файлов "
|
||||
"выравниваний и более длительной обработке."
|
||||
|
||||
#: tools/mask/cli.py:187 tools/mask/cli.py:195 tools/mask/cli.py:209
|
||||
#: tools/mask/cli.py:223 tools/mask/cli.py:233
|
||||
msgid "output"
|
||||
msgstr "вывод"
|
||||
|
||||
#: tools/mask/cli.py:128
|
||||
#: tools/mask/cli.py:189
|
||||
msgid ""
|
||||
"Optional output location. If provided, a preview of the masks created will "
|
||||
"be output in the given folder."
|
||||
|
|
@ -179,7 +260,7 @@ msgstr ""
|
|||
"Необязательное местоположение вывода. Если указано, предварительный просмотр "
|
||||
"созданных масок будет выведен в указанную папку."
|
||||
|
||||
#: tools/mask/cli.py:138
|
||||
#: tools/mask/cli.py:200
|
||||
msgid ""
|
||||
"Apply gaussian blur to the mask output. Has the effect of smoothing the "
|
||||
"edges of the mask giving less of a hard edge. the size is in pixels. This "
|
||||
|
|
@ -192,7 +273,7 @@ msgstr ""
|
|||
"Примечание: влияет только на предварительный просмотр. Установите значение 0 "
|
||||
"для выключения"
|
||||
|
||||
#: tools/mask/cli.py:151
|
||||
#: tools/mask/cli.py:214
|
||||
msgid ""
|
||||
"Helps reduce 'blotchiness' on some masks by making light shades white and "
|
||||
"dark shades black. Higher values will impact more of the mask. NB: Only "
|
||||
|
|
@ -203,7 +284,7 @@ msgstr ""
|
|||
"часть маски. Примечание: влияет только на предварительный просмотр. "
|
||||
"Установите значение 0 для выключения"
|
||||
|
||||
#: tools/mask/cli.py:161
|
||||
#: tools/mask/cli.py:225
|
||||
msgid ""
|
||||
"R|How to format the output when processing is set to 'output'.\n"
|
||||
"L|combined: The image contains the face/frame, face mask and masked face.\n"
|
||||
|
|
@ -216,7 +297,7 @@ msgstr ""
|
|||
"L|masked: Вывести лицо/кадр как изображение rgba с маскированным лицом.\n"
|
||||
"L|mask: Выводить только маску как одноканальное изображение."
|
||||
|
||||
#: tools/mask/cli.py:170
|
||||
#: tools/mask/cli.py:235
|
||||
msgid ""
|
||||
"R|Whether to output the whole frame or only the face box when using output "
|
||||
"processing. Only has an effect when using frames as input."
|
||||
|
|
@ -224,3 +305,22 @@ msgstr ""
|
|||
"R|Выводить ли весь кадр или только поле лица при использовании выходной "
|
||||
"обработки. Имеет значение только при использовании кадров в качестве входных "
|
||||
"данных."
|
||||
|
||||
#~ msgid ""
|
||||
#~ "R|Whether to update all masks in the alignments files, only those faces "
|
||||
#~ "that do not already have a mask of the given `mask type` or just to "
|
||||
#~ "output the masks to the `output` location.\n"
|
||||
#~ "L|all: Update the mask for all faces in the alignments file.\n"
|
||||
#~ "L|missing: Create a mask for all faces in the alignments file where a "
|
||||
#~ "mask does not previously exist.\n"
|
||||
#~ "L|output: Don't update the masks, just output them for review in the "
|
||||
#~ "given output folder."
|
||||
#~ msgstr ""
|
||||
#~ "R|Обновлять ли все маски в файлах выравнивания, только те лица, которые "
|
||||
#~ "еще не имеют маски заданного `mask type` или просто выводить маски в "
|
||||
#~ "место `output`.\n"
|
||||
#~ "L|all: Обновить маску для всех лиц в файле выравнивания.\n"
|
||||
#~ "L|missing: Создать маску для всех лиц в файле выравнивания, для которых "
|
||||
#~ "маска ранее не существовала.\n"
|
||||
#~ "L|output: Не обновлять маски, а просто вывести их для просмотра в "
|
||||
#~ "указанную выходную папку."
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-02-20 23:42+0000\n"
|
||||
"POT-Creation-Date: 2024-03-11 23:45+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
|
@ -18,40 +18,42 @@ msgstr ""
|
|||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: tools/mask/cli.py:15
|
||||
msgid "This command lets you generate masks for existing alignments."
|
||||
msgid ""
|
||||
"This tool allows you to generate, import, export or preview masks for "
|
||||
"existing alignments."
|
||||
msgstr ""
|
||||
|
||||
#: tools/mask/cli.py:24
|
||||
#: tools/mask/cli.py:25
|
||||
msgid ""
|
||||
"Mask tool\n"
|
||||
"Generate masks for existing alignments files."
|
||||
"Generate, import, export or preview masks for existing alignments files."
|
||||
msgstr ""
|
||||
|
||||
#: tools/mask/cli.py:33 tools/mask/cli.py:44 tools/mask/cli.py:54
|
||||
#: tools/mask/cli.py:64
|
||||
#: tools/mask/cli.py:35 tools/mask/cli.py:47 tools/mask/cli.py:58
|
||||
#: tools/mask/cli.py:69
|
||||
msgid "data"
|
||||
msgstr ""
|
||||
|
||||
#: tools/mask/cli.py:36
|
||||
#: tools/mask/cli.py:39
|
||||
msgid ""
|
||||
"Full path to the alignments file to add the mask to if not at the default "
|
||||
"location. NB: If the input-type is faces and you wish to update the "
|
||||
"Full path to the alignments file that contains the masks if not at the "
|
||||
"default location. NB: If the input-type is faces and you wish to update the "
|
||||
"corresponding alignments file, then you must provide a value here as the "
|
||||
"location cannot be automatically detected."
|
||||
msgstr ""
|
||||
|
||||
#: tools/mask/cli.py:47
|
||||
#: tools/mask/cli.py:51
|
||||
msgid "Directory containing extracted faces, source frames, or a video file."
|
||||
msgstr ""
|
||||
|
||||
#: tools/mask/cli.py:56
|
||||
#: tools/mask/cli.py:61
|
||||
msgid ""
|
||||
"R|Whether the `input` is a folder of faces or a folder frames/video\n"
|
||||
"L|faces: The input is a folder containing extracted faces.\n"
|
||||
"L|frames: The input is a folder containing frames or is a video"
|
||||
msgstr ""
|
||||
|
||||
#: tools/mask/cli.py:65
|
||||
#: tools/mask/cli.py:71
|
||||
msgid ""
|
||||
"R|Run the mask tool on multiple sources. If selected then the other options "
|
||||
"should be set as follows:\n"
|
||||
|
|
@ -65,11 +67,11 @@ msgid ""
|
|||
"within the extracted faces will be updated."
|
||||
msgstr ""
|
||||
|
||||
#: tools/mask/cli.py:81 tools/mask/cli.py:113
|
||||
#: tools/mask/cli.py:87 tools/mask/cli.py:119
|
||||
msgid "process"
|
||||
msgstr ""
|
||||
|
||||
#: tools/mask/cli.py:82
|
||||
#: tools/mask/cli.py:89
|
||||
msgid ""
|
||||
"R|Masker to use.\n"
|
||||
"L|bisenet-fp: Relatively lightweight NN based mask that provides more "
|
||||
|
|
@ -94,35 +96,79 @@ msgid ""
|
|||
"some facial obstructions (hands and eyeglasses). Profile faces may result in "
|
||||
"sub-par performance.\n"
|
||||
"L|unet-dfl: Mask designed to provide smart segmentation of mostly frontal "
|
||||
"faces. The mask model has been trained by community members and will need "
|
||||
"testing for further description. Profile faces may result in sub-par "
|
||||
"performance."
|
||||
"faces. The mask model has been trained by community members. Profile faces "
|
||||
"may result in sub-par performance."
|
||||
msgstr ""
|
||||
|
||||
#: tools/mask/cli.py:114
|
||||
#: tools/mask/cli.py:121
|
||||
msgid ""
|
||||
"R|Whether to update all masks in the alignments files, only those faces that "
|
||||
"do not already have a mask of the given `mask type` or just to output the "
|
||||
"masks to the `output` location.\n"
|
||||
"L|all: Update the mask for all faces in the alignments file.\n"
|
||||
"R|The Mask tool process to perform.\n"
|
||||
"L|all: Update the mask for all faces in the alignments file for the selected "
|
||||
"'masker'.\n"
|
||||
"L|missing: Create a mask for all faces in the alignments file where a mask "
|
||||
"does not previously exist.\n"
|
||||
"L|output: Don't update the masks, just output them for review in the given "
|
||||
"output folder."
|
||||
"does not previously exist for the selected 'masker'.\n"
|
||||
"L|output: Don't update the masks, just output the selected 'masker' for "
|
||||
"review/editing in external tools to the given output folder.\n"
|
||||
"L|import: Import masks that have been edited outside of faceswap into the "
|
||||
"alignments file. Note: 'custom' must be the selected 'masker' and the masks "
|
||||
"must be in the same format as the 'input-type' (frames or faces)"
|
||||
msgstr ""
|
||||
|
||||
#: tools/mask/cli.py:127 tools/mask/cli.py:134 tools/mask/cli.py:147
|
||||
#: tools/mask/cli.py:160 tools/mask/cli.py:169
|
||||
#: tools/mask/cli.py:135 tools/mask/cli.py:154 tools/mask/cli.py:174
|
||||
msgid "import"
|
||||
msgstr ""
|
||||
|
||||
#: tools/mask/cli.py:137
|
||||
msgid ""
|
||||
"R|Import only. The path to the folder that contains masks to be imported.\n"
|
||||
"L|How the masks are provided is not important, but they will be stored, "
|
||||
"internally, as 8-bit grayscale images.\n"
|
||||
"L|If the input are images, then the masks must be named exactly the same as "
|
||||
"input frames/faces (excluding the file extension).\n"
|
||||
"L|If the input is a video file, then the filename of the masks is not "
|
||||
"important but should contain the frame number at the end of the filename "
|
||||
"(but before the file extension). The frame number can be separated from the "
|
||||
"rest of the filename by any non-numeric character and can be padded by any "
|
||||
"number of zeros. The frame number must correspond correctly to the frame "
|
||||
"number in the original video (starting from frame 1)."
|
||||
msgstr ""
|
||||
|
||||
#: tools/mask/cli.py:156
|
||||
msgid ""
|
||||
"R|Import only. The centering to use when importing masks. Note: For any job "
|
||||
"other than 'import' this option is ignored as mask centering is handled "
|
||||
"internally.\n"
|
||||
"L|face: Centers the mask on the center of the face, adjusting for pitch and "
|
||||
"yaw. Outside of requirements for full head masking/training, this is likely "
|
||||
"to be the best choice.\n"
|
||||
"L|head: Centers the mask on the center of the head, adjusting for pitch and "
|
||||
"yaw. Note: You should only select head centering if you intend to include "
|
||||
"the full head (including hair) within the mask and are looking to train a "
|
||||
"full head model.\n"
|
||||
"L|legacy: The 'original' extraction technique. Centers the mask near the of "
|
||||
"the nose with and crops closely to the face. Can result in the edges of the "
|
||||
"mask appearing outside of the training area."
|
||||
msgstr ""
|
||||
|
||||
#: tools/mask/cli.py:179
|
||||
msgid ""
|
||||
"Import only. The size, in pixels to internally store the mask at.\n"
|
||||
"The default is 128 which is fine for nearly all usecases. Larger sizes will "
|
||||
"result in larger alignments files and longer processing."
|
||||
msgstr ""
|
||||
|
||||
#: tools/mask/cli.py:187 tools/mask/cli.py:195 tools/mask/cli.py:209
|
||||
#: tools/mask/cli.py:223 tools/mask/cli.py:233
|
||||
msgid "output"
|
||||
msgstr ""
|
||||
|
||||
#: tools/mask/cli.py:128
|
||||
#: tools/mask/cli.py:189
|
||||
msgid ""
|
||||
"Optional output location. If provided, a preview of the masks created will "
|
||||
"be output in the given folder."
|
||||
msgstr ""
|
||||
|
||||
#: tools/mask/cli.py:138
|
||||
#: tools/mask/cli.py:200
|
||||
msgid ""
|
||||
"Apply gaussian blur to the mask output. Has the effect of smoothing the "
|
||||
"edges of the mask giving less of a hard edge. the size is in pixels. This "
|
||||
|
|
@ -130,14 +176,14 @@ msgid ""
|
|||
"to the next odd number. NB: Only effects the output preview. Set to 0 for off"
|
||||
msgstr ""
|
||||
|
||||
#: tools/mask/cli.py:151
|
||||
#: tools/mask/cli.py:214
|
||||
msgid ""
|
||||
"Helps reduce 'blotchiness' on some masks by making light shades white and "
|
||||
"dark shades black. Higher values will impact more of the mask. NB: Only "
|
||||
"effects the output preview. Set to 0 for off"
|
||||
msgstr ""
|
||||
|
||||
#: tools/mask/cli.py:161
|
||||
#: tools/mask/cli.py:225
|
||||
msgid ""
|
||||
"R|How to format the output when processing is set to 'output'.\n"
|
||||
"L|combined: The image contains the face/frame, face mask and masked face.\n"
|
||||
|
|
@ -145,7 +191,7 @@ msgid ""
|
|||
"L|mask: Only output the mask as a single channel image."
|
||||
msgstr ""
|
||||
|
||||
#: tools/mask/cli.py:170
|
||||
#: tools/mask/cli.py:235
|
||||
msgid ""
|
||||
"R|Whether to output the whole frame or only the face box when using output "
|
||||
"processing. Only has an effect when using frames as input."
|
||||
|
|
|
|||
|
|
@ -215,7 +215,7 @@ class FromFaces(): # pylint:disable=too-few-public-methods
|
|||
alignments_path = os.path.join(self._faces_dir, fname)
|
||||
dummy_args = Namespace(alignments_path=alignments_path)
|
||||
aln = Alignments(dummy_args, is_extract=True)
|
||||
aln._data = alignments # pylint:disable=protected-access
|
||||
aln.update_from_dict(alignments)
|
||||
aln._io._version = version # pylint:disable=protected-access
|
||||
aln._io.update_legacy() # pylint:disable=protected-access
|
||||
aln.backup()
|
||||
|
|
|
|||
|
|
@ -795,7 +795,7 @@ class FaceUpdate():
|
|||
def landmarks_rotate(self,
|
||||
frame_index: int,
|
||||
face_index: int,
|
||||
angle: np.ndarray,
|
||||
angle: float,
|
||||
center: np.ndarray) -> None:
|
||||
""" Rotate the landmarks on an Extract Box rotate for the
|
||||
:class:`~lib.align.DetectedFace` object at the given frame and face indices for the
|
||||
|
|
@ -807,7 +807,7 @@ class FaceUpdate():
|
|||
The frame that the face is being set for
|
||||
face_index: int
|
||||
The face index within the frame
|
||||
angle: :class:`numpy.ndarray`
|
||||
angle: float
|
||||
The angle, in radians to rotate the points by
|
||||
center: :class:`numpy.ndarray`
|
||||
The center point of the Landmark's Extract Box
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
#!/usr/bin/env python3
|
||||
""" The Manual Tool is a tkinter driven GUI app for editing alignments files with visual tools.
|
||||
This module is the main entry point into the Manual Tool. """
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import typing as T
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from time import sleep
|
||||
|
|
@ -23,8 +26,15 @@ from .faceviewer.frame import FacesFrame
|
|||
from .frameviewer.frame import DisplayFrame
|
||||
from .thumbnails import ThumbsCreator
|
||||
|
||||
if T.TYPE_CHECKING:
|
||||
from lib.align import DetectedFace
|
||||
from lib.align.detected_face import Mask
|
||||
from lib.queue_manager import EventQueue
|
||||
|
||||
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
|
||||
|
||||
TypeManualExtractor = T.Literal["FAN", "cv2-dnn", "mask"]
|
||||
|
||||
|
||||
class Manual(tk.Tk):
|
||||
""" The main entry point for Faceswap's Manual Editor Tool. This tool is part of the Faceswap
|
||||
|
|
@ -193,7 +203,7 @@ class Manual(tk.Tk):
|
|||
|
||||
bottom = ttk.Frame(main, name="frame_bottom")
|
||||
main.add(bottom)
|
||||
retval = dict(main=main, top=top, bottom=bottom)
|
||||
retval = {"main": main, "top": top, "bottom": bottom}
|
||||
logger.debug("Created containers: %s", retval)
|
||||
return retval
|
||||
|
||||
|
|
@ -406,11 +416,11 @@ class TkGlobals():
|
|||
self._frame_count = 0 # set by FrameLoader
|
||||
self._frame_display_dims = (int(round(896 * get_config().scaling_factor)),
|
||||
int(round(504 * get_config().scaling_factor)))
|
||||
self._current_frame = dict(image=None,
|
||||
scale=None,
|
||||
interpolation=None,
|
||||
display_dims=None,
|
||||
filename=None)
|
||||
self._current_frame = {"image": None,
|
||||
"scale": None,
|
||||
"interpolation": None,
|
||||
"display_dims": None,
|
||||
"filename": None}
|
||||
logger.debug("Initialized %s", self.__class__.__name__)
|
||||
|
||||
@classmethod
|
||||
|
|
@ -640,28 +650,38 @@ class Aligner():
|
|||
A list of indices correlating to connected GPUs that Tensorflow should not use. Pass
|
||||
``None`` to not exclude any GPUs.
|
||||
"""
|
||||
def __init__(self, tk_globals, exclude_gpus):
|
||||
def __init__(self, tk_globals: TkGlobals, exclude_gpus: list[int] | None) -> None:
|
||||
logger.debug("Initializing: %s (tk_globals: %s, exclude_gpus: %s)",
|
||||
self.__class__.__name__, tk_globals, exclude_gpus)
|
||||
self._globals = tk_globals
|
||||
self._aligners = {"cv2-dnn": None, "FAN": None, "mask": None}
|
||||
self._aligner = "FAN"
|
||||
self._exclude_gpus = exclude_gpus
|
||||
self._detected_faces = None
|
||||
self._frame_index = None
|
||||
self._face_index = None
|
||||
|
||||
self._detected_faces: DetectedFaces | None = None
|
||||
self._frame_index: int | None = None
|
||||
self._face_index: int | None = None
|
||||
|
||||
self._aligners: dict[TypeManualExtractor, Extractor | None] = {"cv2-dnn": None,
|
||||
"FAN": None,
|
||||
"mask": None}
|
||||
self._aligner: TypeManualExtractor = "FAN"
|
||||
|
||||
self._init_thread = self._background_init_aligner()
|
||||
logger.debug("Initialized: %s", self.__class__.__name__)
|
||||
|
||||
@property
|
||||
def _in_queue(self):
|
||||
def _in_queue(self) -> EventQueue:
|
||||
""" :class:`queue.Queue` - The input queue to the extraction pipeline. """
|
||||
return self._aligners[self._aligner].input_queue
|
||||
aligner = self._aligners[self._aligner]
|
||||
assert aligner is not None
|
||||
return aligner.input_queue
|
||||
|
||||
@property
|
||||
def _feed_face(self):
|
||||
def _feed_face(self) -> ExtractMedia:
|
||||
""" :class:`plugins.extract.pipeline.ExtractMedia`: The current face for feeding into the
|
||||
aligner, formatted for the pipeline """
|
||||
assert self._frame_index is not None
|
||||
assert self._face_index is not None
|
||||
assert self._detected_faces is not None
|
||||
face = self._detected_faces.current_faces[self._frame_index][self._face_index]
|
||||
return ExtractMedia(
|
||||
self._globals.current_frame["filename"],
|
||||
|
|
@ -669,22 +689,28 @@ class Aligner():
|
|||
detected_faces=[face])
|
||||
|
||||
@property
|
||||
def is_initialized(self):
|
||||
def is_initialized(self) -> bool:
|
||||
""" bool: The Aligners are initialized in a background thread so that other tasks can be
|
||||
performed whilst we wait for initialization. ``True`` is returned if the aligner has
|
||||
completed initialization otherwise ``False``."""
|
||||
thread_is_alive = self._init_thread.is_alive()
|
||||
if thread_is_alive:
|
||||
logger.trace("Aligner not yet initialized")
|
||||
logger.trace("Aligner not yet initialized") # type:ignore[attr-defined]
|
||||
self._init_thread.check_and_raise_error()
|
||||
else:
|
||||
logger.trace("Aligner initialized")
|
||||
logger.trace("Aligner initialized") # type:ignore[attr-defined]
|
||||
self._init_thread.join()
|
||||
return not thread_is_alive
|
||||
|
||||
def _background_init_aligner(self):
|
||||
def _background_init_aligner(self) -> MultiThread:
|
||||
""" Launch the aligner in a background thread so we can run other tasks whilst
|
||||
waiting for initialization """
|
||||
waiting for initialization
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`lib.multithreading.MultiThread
|
||||
The background aligner loader thread
|
||||
"""
|
||||
logger.debug("Launching aligner initialization thread")
|
||||
thread = MultiThread(self._init_aligner,
|
||||
thread_count=1,
|
||||
|
|
@ -693,11 +719,11 @@ class Aligner():
|
|||
logger.debug("Launched aligner initialization thread")
|
||||
return thread
|
||||
|
||||
def _init_aligner(self):
|
||||
def _init_aligner(self) -> None:
|
||||
""" Initialize Aligner in a background thread, and set it to :attr:`_aligner`. """
|
||||
logger.debug("Initialize Aligner")
|
||||
# Make sure non-GPU aligner is allocated first
|
||||
for model in ("mask", "cv2-dnn", "FAN"):
|
||||
for model in T.get_args(TypeManualExtractor):
|
||||
logger.debug("Initializing aligner: %s", model)
|
||||
plugin = None if model == "mask" else model
|
||||
exclude_gpus = self._exclude_gpus if model == "FAN" else None
|
||||
|
|
@ -714,7 +740,7 @@ class Aligner():
|
|||
logger.debug("Initialized %s Extractor", model)
|
||||
self._aligners[model] = aligner
|
||||
|
||||
def link_faces(self, detected_faces):
|
||||
def link_faces(self, detected_faces: DetectedFaces) -> None:
|
||||
""" As the Aligner has the potential to take the longest to initialize, it is kicked off
|
||||
as early as possible. At this time :class:`~tools.manual.detected_faces.DetectedFaces` is
|
||||
not yet available.
|
||||
|
|
@ -731,7 +757,8 @@ class Aligner():
|
|||
logger.debug("Linking detected_faces: %s", detected_faces)
|
||||
self._detected_faces = detected_faces
|
||||
|
||||
def get_landmarks(self, frame_index, face_index, aligner):
|
||||
def get_landmarks(self, frame_index: int, face_index: int, aligner: TypeManualExtractor
|
||||
) -> np.ndarray:
|
||||
""" Feed the detected face into the alignment pipeline and retrieve the landmarks.
|
||||
|
||||
The face to feed into the aligner is generated from the given frame and face indices.
|
||||
|
|
@ -742,7 +769,7 @@ class Aligner():
|
|||
The frame index to extract the aligned face for
|
||||
face_index: int
|
||||
The face index within the current frame to extract the face for
|
||||
aligner: ["FAN", "cv2-dnn"]
|
||||
aligner: Literal["FAN", "cv2-dnn"]
|
||||
The aligner to use to extract the face
|
||||
|
||||
Returns
|
||||
|
|
@ -750,22 +777,37 @@ class Aligner():
|
|||
:class:`numpy.ndarray`
|
||||
The 68 point landmark alignments
|
||||
"""
|
||||
logger.trace("frame_index: %s, face_index: %s, aligner: %s",
|
||||
logger.trace("frame_index: %s, face_index: %s, aligner: %s", # type:ignore[attr-defined]
|
||||
frame_index, face_index, aligner)
|
||||
self._frame_index = frame_index
|
||||
self._face_index = face_index
|
||||
self._aligner = aligner
|
||||
self._in_queue.put(self._feed_face)
|
||||
detected_face = next(self._aligners[aligner].detected_faces()).detected_faces[0]
|
||||
logger.trace("landmarks: %s", detected_face.landmarks_xy)
|
||||
extractor = self._aligners[aligner]
|
||||
assert extractor is not None
|
||||
detected_face = next(extractor.detected_faces()).detected_faces[0]
|
||||
logger.trace("landmarks: %s", detected_face.landmarks_xy) # type:ignore[attr-defined]
|
||||
return detected_face.landmarks_xy
|
||||
|
||||
def get_masks(self, frame_index, face_index):
|
||||
def _remove_nn_masks(self, detected_face: DetectedFace) -> None:
|
||||
""" Remove any non-landmarks based masks on a landmark edit
|
||||
|
||||
Parameters
|
||||
----------
|
||||
detected_face:
|
||||
The detected face object to remove masks from
|
||||
"""
|
||||
del_masks = {m for m in detected_face.mask if m not in ("components", "extended")}
|
||||
logger.info("Removing masks after landmark update: %s", del_masks)
|
||||
for mask in del_masks:
|
||||
del detected_face.mask[mask]
|
||||
|
||||
def get_masks(self, frame_index: int, face_index: int) -> dict[str, Mask]:
|
||||
""" Feed the aligned face into the mask pipeline and retrieve the updated masks.
|
||||
|
||||
The face to feed into the aligner is generated from the given frame and face indices.
|
||||
This is to be called when a manual update is done on the landmarks, and new masks need
|
||||
generating
|
||||
generating.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
|
@ -776,30 +818,34 @@ class Aligner():
|
|||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
dict[str, :class:`~lib.align.detected_face.Mask`]
|
||||
The updated masks
|
||||
"""
|
||||
logger.trace("frame_index: %s, face_index: %s", frame_index, face_index)
|
||||
logger.trace("frame_index: %s, face_index: %s", # type:ignore[attr-defined]
|
||||
frame_index, face_index)
|
||||
self._frame_index = frame_index
|
||||
self._face_index = face_index
|
||||
self._aligner = "mask"
|
||||
self._in_queue.put(self._feed_face)
|
||||
assert self._aligners["mask"] is not None
|
||||
detected_face = next(self._aligners["mask"].detected_faces()).detected_faces[0]
|
||||
self._remove_nn_masks(detected_face)
|
||||
logger.debug("mask: %s", detected_face.mask)
|
||||
return detected_face.mask
|
||||
|
||||
def set_normalization_method(self, method):
|
||||
def set_normalization_method(self, method: T.Literal["none", "clahe", "hist", "mean"]) -> None:
|
||||
""" Change the normalization method for faces fed into the aligner.
|
||||
The normalization method is user adjustable from the GUI. When this method is triggered
|
||||
the method is updated for all aligner pipelines.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
method: str
|
||||
method: Literal["none", "clahe", "hist", "mean"]
|
||||
The normalization method to use
|
||||
"""
|
||||
logger.debug("Setting normalization method to: '%s'", method)
|
||||
for plugin, aligner in self._aligners.items():
|
||||
assert aligner is not None
|
||||
if plugin == "mask":
|
||||
continue
|
||||
logger.debug("Setting to: '%s'", method)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ _LANG = gettext.translation("tools.mask.cli", localedir="locales", fallback=True
|
|||
_ = _LANG.gettext
|
||||
|
||||
|
||||
_HELPTEXT = _("This command lets you generate masks for existing alignments.")
|
||||
_HELPTEXT = _("This tool allows you to generate, import, export or preview masks for existing "
|
||||
"alignments.")
|
||||
|
||||
|
||||
class MaskArgs(FaceSwapArgs):
|
||||
|
|
@ -21,153 +22,217 @@ class MaskArgs(FaceSwapArgs):
|
|||
@staticmethod
|
||||
def get_info():
|
||||
""" Return command information """
|
||||
return _("Mask tool\nGenerate masks for existing alignments files.")
|
||||
return _("Mask tool\nGenerate, import, export or preview masks for existing alignments "
|
||||
"files.")
|
||||
|
||||
@staticmethod
|
||||
def get_argument_list():
|
||||
argument_list = []
|
||||
argument_list.append(dict(
|
||||
opts=("-a", "--alignments"),
|
||||
action=FileFullPaths,
|
||||
type=str,
|
||||
group=_("data"),
|
||||
required=False,
|
||||
filetypes="alignments",
|
||||
help=_("Full path to the alignments file to add the mask to if not at the default "
|
||||
"location. NB: If the input-type is faces and you wish to update the "
|
||||
"corresponding alignments file, then you must provide a value here as the "
|
||||
"location cannot be automatically detected.")))
|
||||
argument_list.append(dict(
|
||||
opts=("-i", "--input"),
|
||||
action=DirOrFileFullPaths,
|
||||
type=str,
|
||||
group=_("data"),
|
||||
filetypes="video",
|
||||
required=True,
|
||||
help=_("Directory containing extracted faces, source frames, or a video file.")))
|
||||
argument_list.append(dict(
|
||||
opts=("-it", "--input-type"),
|
||||
action=Radio,
|
||||
type=str.lower,
|
||||
choices=("faces", "frames"),
|
||||
dest="input_type",
|
||||
group=_("data"),
|
||||
default="frames",
|
||||
help=_("R|Whether the `input` is a folder of faces or a folder frames/video"
|
||||
"\nL|faces: The input is a folder containing extracted faces."
|
||||
"\nL|frames: The input is a folder containing frames or is a video")))
|
||||
argument_list.append(dict(
|
||||
opts=("-B", "--batch-mode"),
|
||||
action="store_true",
|
||||
dest="batch_mode",
|
||||
default=False,
|
||||
group=_("data"),
|
||||
help=_("R|Run the mask tool on multiple sources. If selected then the other options "
|
||||
"should be set as follows:"
|
||||
"\nL|input: A parent folder containing either all of the video files to be "
|
||||
"processed, or containing sub-folders of frames/faces."
|
||||
"\nL|output-folder: If provided, then sub-folders will be created within the "
|
||||
"given location to hold the previews for each input."
|
||||
"\nL|alignments: Alignments field will be ignored for batch processing. The "
|
||||
"alignments files must exist at the default location (for frames). For batch "
|
||||
"processing of masks with 'faces' as the input type, then only the PNG header "
|
||||
"within the extracted faces will be updated.")))
|
||||
argument_list.append(dict(
|
||||
opts=("-M", "--masker"),
|
||||
action=Radio,
|
||||
type=str.lower,
|
||||
choices=PluginLoader.get_available_extractors("mask"),
|
||||
default="extended",
|
||||
group=_("process"),
|
||||
help=_("R|Masker to use."
|
||||
"\nL|bisenet-fp: Relatively lightweight NN based mask that provides more "
|
||||
"refined control over the area to be masked including full head masking "
|
||||
"(configurable in mask settings)."
|
||||
"\nL|components: Mask designed to provide facial segmentation based on the "
|
||||
"positioning of landmark locations. A convex hull is constructed around the "
|
||||
"exterior of the landmarks to create a mask."
|
||||
"\nL|custom: A dummy mask that fills the mask area with all 1s or 0s "
|
||||
"(configurable in settings). This is only required if you intend to manually "
|
||||
"edit the custom masks yourself in the manual tool. This mask does not use the "
|
||||
"GPU."
|
||||
"\nL|extended: Mask designed to provide facial segmentation based on the "
|
||||
"positioning of landmark locations. A convex hull is constructed around the "
|
||||
"exterior of the landmarks and the mask is extended upwards onto the forehead."
|
||||
"\nL|vgg-clear: Mask designed to provide smart segmentation of mostly frontal "
|
||||
"faces clear of obstructions. Profile faces and obstructions may result in "
|
||||
"sub-par performance."
|
||||
"\nL|vgg-obstructed: Mask designed to provide smart segmentation of mostly "
|
||||
"frontal faces. The mask model has been specifically trained to recognize "
|
||||
"some facial obstructions (hands and eyeglasses). Profile faces may result in "
|
||||
"sub-par performance."
|
||||
"\nL|unet-dfl: Mask designed to provide smart segmentation of mostly frontal "
|
||||
"faces. The mask model has been trained by community members and will need "
|
||||
"testing for further description. Profile faces may result in sub-par "
|
||||
"performance.")))
|
||||
argument_list.append(dict(
|
||||
opts=("-p", "--processing"),
|
||||
action=Radio,
|
||||
type=str.lower,
|
||||
choices=("all", "missing", "output"),
|
||||
default="missing",
|
||||
group=_("process"),
|
||||
help=_("R|Whether to update all masks in the alignments files, only those faces "
|
||||
"that do not already have a mask of the given `mask type` or just to output "
|
||||
"the masks to the `output` location."
|
||||
"\nL|all: Update the mask for all faces in the alignments file."
|
||||
"\nL|missing: Create a mask for all faces in the alignments file where a mask "
|
||||
"does not previously exist."
|
||||
"\nL|output: Don't update the masks, just output them for review in the given "
|
||||
"output folder.")))
|
||||
argument_list.append(dict(
|
||||
opts=("-o", "--output-folder"),
|
||||
action=DirFullPaths,
|
||||
dest="output",
|
||||
type=str,
|
||||
group=_("output"),
|
||||
help=_("Optional output location. If provided, a preview of the masks created will "
|
||||
"be output in the given folder.")))
|
||||
argument_list.append(dict(
|
||||
opts=("-b", "--blur_kernel"),
|
||||
action=Slider,
|
||||
type=int,
|
||||
group=_("output"),
|
||||
min_max=(0, 9),
|
||||
default=3,
|
||||
rounding=1,
|
||||
help=_("Apply gaussian blur to the mask output. Has the effect of smoothing the "
|
||||
"edges of the mask giving less of a hard edge. the size is in pixels. This "
|
||||
"value should be odd, if an even number is passed in then it will be rounded "
|
||||
"to the next odd number. NB: Only effects the output preview. Set to 0 for "
|
||||
"off")))
|
||||
argument_list.append(dict(
|
||||
opts=("-t", "--threshold"),
|
||||
action=Slider,
|
||||
type=int,
|
||||
group=_("output"),
|
||||
min_max=(0, 50),
|
||||
default=4,
|
||||
rounding=1,
|
||||
help=_("Helps reduce 'blotchiness' on some masks by making light shades white "
|
||||
"and dark shades black. Higher values will impact more of the mask. NB: "
|
||||
"Only effects the output preview. Set to 0 for off")))
|
||||
argument_list.append(dict(
|
||||
opts=("-ot", "--output-type"),
|
||||
action=Radio,
|
||||
type=str.lower,
|
||||
choices=("combined", "masked", "mask"),
|
||||
default="combined",
|
||||
group=_("output"),
|
||||
help=_("R|How to format the output when processing is set to 'output'."
|
||||
"\nL|combined: The image contains the face/frame, face mask and masked face."
|
||||
"\nL|masked: Output the face/frame as rgba image with the face masked."
|
||||
"\nL|mask: Only output the mask as a single channel image.")))
|
||||
argument_list.append(dict(
|
||||
opts=("-f", "--full-frame"),
|
||||
action="store_true",
|
||||
default=False,
|
||||
group=_("output"),
|
||||
help=_("R|Whether to output the whole frame or only the face box when using "
|
||||
"output processing. Only has an effect when using frames as input.")))
|
||||
argument_list.append({
|
||||
"opts": ("-a", "--alignments"),
|
||||
"action": FileFullPaths,
|
||||
"type": str,
|
||||
"group": _("data"),
|
||||
"required": False,
|
||||
"filetypes": "alignments",
|
||||
"help": _(
|
||||
"Full path to the alignments file that contains the masks if not at the "
|
||||
"default location. NB: If the input-type is faces and you wish to update the "
|
||||
"corresponding alignments file, then you must provide a value here as the "
|
||||
"location cannot be automatically detected.")})
|
||||
argument_list.append({
|
||||
"opts": ("-i", "--input"),
|
||||
"action": DirOrFileFullPaths,
|
||||
"type": str,
|
||||
"group": _("data"),
|
||||
"filetypes": "video",
|
||||
"required": True,
|
||||
"help": _(
|
||||
"Directory containing extracted faces, source frames, or a video file.")})
|
||||
argument_list.append({
|
||||
"opts": ("-it", "--input-type"),
|
||||
"action": Radio,
|
||||
"type": str.lower,
|
||||
"choices": ("faces", "frames"),
|
||||
"dest": "input_type",
|
||||
"group": _("data"),
|
||||
"default": "frames",
|
||||
"help": _(
|
||||
"R|Whether the `input` is a folder of faces or a folder frames/video"
|
||||
"\nL|faces: The input is a folder containing extracted faces."
|
||||
"\nL|frames: The input is a folder containing frames or is a video")})
|
||||
argument_list.append({
|
||||
"opts": ("-B", "--batch-mode"),
|
||||
"action": "store_true",
|
||||
"dest": "batch_mode",
|
||||
"default": False,
|
||||
"group": _("data"),
|
||||
"help": _(
|
||||
"R|Run the mask tool on multiple sources. If selected then the other options "
|
||||
"should be set as follows:"
|
||||
"\nL|input: A parent folder containing either all of the video files to be "
|
||||
"processed, or containing sub-folders of frames/faces."
|
||||
"\nL|output-folder: If provided, then sub-folders will be created within the "
|
||||
"given location to hold the previews for each input."
|
||||
"\nL|alignments: Alignments field will be ignored for batch processing. The "
|
||||
"alignments files must exist at the default location (for frames). For batch "
|
||||
"processing of masks with 'faces' as the input type, then only the PNG header "
|
||||
"within the extracted faces will be updated.")})
|
||||
argument_list.append({
|
||||
"opts": ("-M", "--masker"),
|
||||
"action": Radio,
|
||||
"type": str.lower,
|
||||
"choices": PluginLoader.get_available_extractors("mask"),
|
||||
"default": "extended",
|
||||
"group": _("process"),
|
||||
"help": _(
|
||||
"R|Masker to use."
|
||||
"\nL|bisenet-fp: Relatively lightweight NN based mask that provides more "
|
||||
"refined control over the area to be masked including full head masking "
|
||||
"(configurable in mask settings)."
|
||||
"\nL|components: Mask designed to provide facial segmentation based on the "
|
||||
"positioning of landmark locations. A convex hull is constructed around the "
|
||||
"exterior of the landmarks to create a mask."
|
||||
"\nL|custom: A dummy mask that fills the mask area with all 1s or 0s "
|
||||
"(configurable in settings). This is only required if you intend to manually "
|
||||
"edit the custom masks yourself in the manual tool. This mask does not use the "
|
||||
"GPU."
|
||||
"\nL|extended: Mask designed to provide facial segmentation based on the "
|
||||
"positioning of landmark locations. A convex hull is constructed around the "
|
||||
"exterior of the landmarks and the mask is extended upwards onto the forehead."
|
||||
"\nL|vgg-clear: Mask designed to provide smart segmentation of mostly frontal "
|
||||
"faces clear of obstructions. Profile faces and obstructions may result in "
|
||||
"sub-par performance."
|
||||
"\nL|vgg-obstructed: Mask designed to provide smart segmentation of mostly "
|
||||
"frontal faces. The mask model has been specifically trained to recognize "
|
||||
"some facial obstructions (hands and eyeglasses). Profile faces may result in "
|
||||
"sub-par performance."
|
||||
"\nL|unet-dfl: Mask designed to provide smart segmentation of mostly frontal "
|
||||
"faces. The mask model has been trained by community members. Profile faces "
|
||||
"may result in sub-par performance.")})
|
||||
argument_list.append({
|
||||
"opts": ("-p", "--processing"),
|
||||
"action": Radio,
|
||||
"type": str.lower,
|
||||
"choices": ("all", "missing", "output", "import"),
|
||||
"default": "all",
|
||||
"group": _("process"),
|
||||
"help": _(
|
||||
"R|The Mask tool process to perform."
|
||||
"\nL|all: Update the mask for all faces in the alignments file for the selected "
|
||||
"'masker'."
|
||||
"\nL|missing: Create a mask for all faces in the alignments file where a mask "
|
||||
"does not previously exist for the selected 'masker'."
|
||||
"\nL|output: Don't update the masks, just output the selected 'masker' for "
|
||||
"review/editing in external tools to the given output folder."
|
||||
"\nL|import: Import masks that have been edited outside of faceswap into the "
|
||||
"alignments file. Note: 'custom' must be the selected 'masker' and the masks must "
|
||||
"be in the same format as the 'input-type' (frames or faces)")})
|
||||
argument_list.append({
|
||||
"opts": ("-m", "--mask-path"),
|
||||
"action": DirFullPaths,
|
||||
"type": str,
|
||||
"group": _("import"),
|
||||
"help": _(
|
||||
"R|Import only. The path to the folder that contains masks to be imported."
|
||||
"\nL|How the masks are provided is not important, but they will be stored, "
|
||||
"internally, as 8-bit grayscale images."
|
||||
"\nL|If the input are images, then the masks must be named exactly the same as "
|
||||
"input frames/faces (excluding the file extension)."
|
||||
"\nL|If the input is a video file, then the filename of the masks is not "
|
||||
"important but should contain the frame number at the end of the filename (but "
|
||||
"before the file extension). The frame number can be separated from the rest of "
|
||||
"the filename by any non-numeric character and can be padded by any number of "
|
||||
"zeros. The frame number must correspond correctly to the frame number in the "
|
||||
"original video (starting from frame 1).")})
|
||||
argument_list.append({
|
||||
"opts": ("-c", "--centering"),
|
||||
"action": Radio,
|
||||
"type": str.lower,
|
||||
"choices": ("face", "head", "legacy"),
|
||||
"default": "face",
|
||||
"group": _("import"),
|
||||
"help": _(
|
||||
"R|Import only. The centering to use when importing masks. Note: For any job "
|
||||
"other than 'import' this option is ignored as mask centering is handled "
|
||||
"internally."
|
||||
"\nL|face: Centers the mask on the center of the face, adjusting for "
|
||||
"pitch and yaw. Outside of requirements for full head masking/training, this "
|
||||
"is likely to be the best choice."
|
||||
"\nL|head: Centers the mask on the center of the head, adjusting for "
|
||||
"pitch and yaw. Note: You should only select head centering if you intend to "
|
||||
"include the full head (including hair) within the mask and are looking to "
|
||||
"train a full head model."
|
||||
"\nL|legacy: The 'original' extraction technique. Centers the mask near the "
|
||||
" of the nose with and crops closely to the face. Can result in the edges of "
|
||||
"the mask appearing outside of the training area.")})
|
||||
argument_list.append({
|
||||
"opts": ("-s", "--storage-size"),
|
||||
"dest": "storage_size",
|
||||
"action": Slider,
|
||||
"type": int,
|
||||
"group": _("import"),
|
||||
"min_max": (64, 1024),
|
||||
"default": 128,
|
||||
"rounding": 64,
|
||||
"help": _(
|
||||
"Import only. The size, in pixels to internally store the mask at.\nThe default "
|
||||
"is 128 which is fine for nearly all usecases. Larger sizes will result in larger "
|
||||
"alignments files and longer processing.")})
|
||||
argument_list.append({
|
||||
"opts": ("-o", "--output-folder"),
|
||||
"action": DirFullPaths,
|
||||
"dest": "output",
|
||||
"type": str,
|
||||
"group": _("output"),
|
||||
"help": _(
|
||||
"Optional output location. If provided, a preview of the masks created will "
|
||||
"be output in the given folder.")})
|
||||
argument_list.append({
|
||||
"opts": ("-b", "--blur_kernel"),
|
||||
"action": Slider,
|
||||
"type": int,
|
||||
"group": _("output"),
|
||||
"min_max": (0, 9),
|
||||
"default": 0,
|
||||
"rounding": 1,
|
||||
"help": _(
|
||||
"Apply gaussian blur to the mask output. Has the effect of smoothing the "
|
||||
"edges of the mask giving less of a hard edge. the size is in pixels. This "
|
||||
"value should be odd, if an even number is passed in then it will be rounded "
|
||||
"to the next odd number. NB: Only effects the output preview. Set to 0 for "
|
||||
"off")})
|
||||
argument_list.append({
|
||||
"opts": ("-t", "--threshold"),
|
||||
"action": Slider,
|
||||
"type": int,
|
||||
"group": _("output"),
|
||||
"min_max": (0, 50),
|
||||
"default": 0,
|
||||
"rounding": 1,
|
||||
"help": _(
|
||||
"Helps reduce 'blotchiness' on some masks by making light shades white "
|
||||
"and dark shades black. Higher values will impact more of the mask. NB: "
|
||||
"Only effects the output preview. Set to 0 for off")})
|
||||
argument_list.append({
|
||||
"opts": ("-O", "--output-type"),
|
||||
"action": Radio,
|
||||
"type": str.lower,
|
||||
"choices": ("combined", "masked", "mask"),
|
||||
"default": "combined",
|
||||
"group": _("output"),
|
||||
"help": _(
|
||||
"R|How to format the output when processing is set to 'output'."
|
||||
"\nL|combined: The image contains the face/frame, face mask and masked face."
|
||||
"\nL|masked: Output the face/frame as rgba image with the face masked."
|
||||
"\nL|mask: Only output the mask as a single channel image.")})
|
||||
argument_list.append({
|
||||
"opts": ("-f", "--full-frame"),
|
||||
"action": "store_true",
|
||||
"default": False,
|
||||
"group": _("output"),
|
||||
"help": _(
|
||||
"R|Whether to output the whole frame or only the face box when using "
|
||||
"output processing. Only has an effect when using frames as input.")})
|
||||
|
||||
return argument_list
|
||||
|
|
|
|||
222
tools/mask/loader.py
Normal file
222
tools/mask/loader.py
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
#!/usr/bin/env python3
|
||||
""" Handles loading of faces/frames from source locations and pairing with alignments
|
||||
information """
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import typing as T
|
||||
|
||||
import numpy as np
|
||||
from tqdm import tqdm
|
||||
|
||||
from lib.align import DetectedFace, update_legacy_png_header
|
||||
from lib.align.alignments import AlignmentFileDict
|
||||
from lib.image import FacesLoader, ImagesLoader
|
||||
from plugins.extract.pipeline import ExtractMedia
|
||||
|
||||
if T.TYPE_CHECKING:
|
||||
from lib.align import Alignments
|
||||
from lib.align.alignments import PNGHeaderDict
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Loader:
|
||||
""" Loader for reading source data from disk, and yielding the output paired with alignment
|
||||
information
|
||||
|
||||
Parameters
|
||||
----------
|
||||
location: str
|
||||
Full path to the source files location
|
||||
is_faces: bool
|
||||
``True`` if the source is a folder of faceswap extracted faces
|
||||
"""
|
||||
def __init__(self, location: str, is_faces: bool) -> None:
|
||||
logger.debug("Initializing %s (location: %s, is_faces: %s)",
|
||||
self.__class__.__name__, location, is_faces)
|
||||
|
||||
self._is_faces = is_faces
|
||||
self._loader = FacesLoader(location) if is_faces else ImagesLoader(location)
|
||||
self._alignments: Alignments | None = None
|
||||
self._skip_count = 0
|
||||
|
||||
logger.debug("Initialized %s", self.__class__.__name__)
|
||||
|
||||
@property
|
||||
def file_list(self) -> list[str]:
|
||||
"""list[str]: Full file list of source files to be loaded """
|
||||
return self._loader.file_list
|
||||
|
||||
@property
|
||||
def is_video(self) -> bool:
|
||||
"""bool: ``True`` if the source is a video file otherwise ``False`` """
|
||||
return self._loader.is_video
|
||||
|
||||
@property
|
||||
def location(self) -> str:
|
||||
"""str: Full path to the source folder/video file location """
|
||||
return self._loader.location
|
||||
|
||||
@property
|
||||
def skip_count(self) -> int:
|
||||
"""int: The number of faces/frames that have been skipped due to no match in alignments
|
||||
file """
|
||||
return self._skip_count
|
||||
|
||||
def add_alignments(self, alignments: Alignments | None) -> None:
|
||||
""" Add the loaded alignments to :attr:`_alignments` for content matching
|
||||
|
||||
Parameters
|
||||
----------
|
||||
alignments: :class:`~lib.align.Alignments` | None
|
||||
The alignments file object or ``None`` if not provided
|
||||
"""
|
||||
logger.debug("Adding alignments to loader: %s", alignments)
|
||||
self._alignments = alignments
|
||||
|
||||
@classmethod
|
||||
def _get_detected_face(cls, alignment: AlignmentFileDict) -> DetectedFace:
|
||||
""" Convert an alignment dict item to a detected_face object
|
||||
|
||||
Parameters
|
||||
----------
|
||||
alignment: :class:`lib.align.alignments.AlignmentFileDict`
|
||||
The alignment dict for a face
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`~lib.align.detected_face.DetectedFace`:
|
||||
The corresponding detected_face object for the alignment
|
||||
"""
|
||||
detected_face = DetectedFace()
|
||||
detected_face.from_alignment(alignment)
|
||||
return detected_face
|
||||
|
||||
def _process_face(self,
|
||||
filename: str,
|
||||
image: np.ndarray,
|
||||
metadata: PNGHeaderDict) -> ExtractMedia | None:
|
||||
""" Process a single face when masking from face images
|
||||
|
||||
Parameters
|
||||
----------
|
||||
filename: str
|
||||
the filename currently being processed
|
||||
image: :class:`numpy.ndarray`
|
||||
The current face being processed
|
||||
metadata: dict
|
||||
The source frame metadata from the PNG header
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`plugins.pipeline.ExtractMedia` | None
|
||||
the extract media object for the processed face or ``None`` if alignment information
|
||||
could not be found
|
||||
"""
|
||||
frame_name = metadata["source"]["source_filename"]
|
||||
face_index = metadata["source"]["face_index"]
|
||||
|
||||
if self._alignments is None: # mask from PNG header
|
||||
lookup_index = 0
|
||||
alignments = [T.cast(AlignmentFileDict, metadata["alignments"])]
|
||||
else: # mask from Alignments file
|
||||
lookup_index = face_index
|
||||
alignments = self._alignments.get_faces_in_frame(frame_name)
|
||||
if not alignments or face_index > len(alignments) - 1:
|
||||
self._skip_count += 1
|
||||
logger.warning("Skipping Face not found in alignments file: '%s'", filename)
|
||||
return None
|
||||
|
||||
alignment = alignments[lookup_index]
|
||||
detected_face = self._get_detected_face(alignment)
|
||||
|
||||
retval = ExtractMedia(filename, image, detected_faces=[detected_face], is_aligned=True)
|
||||
retval.add_frame_metadata(metadata["source"])
|
||||
return retval
|
||||
|
||||
def _from_faces(self) -> T.Generator[ExtractMedia, None, None]:
|
||||
""" Load content from pre-aligned faces and pair with corresponding metadata
|
||||
|
||||
Yields
|
||||
------
|
||||
:class:`plugins.pipeline.ExtractMedia`
|
||||
the extract media object for the processed face
|
||||
"""
|
||||
log_once = False
|
||||
for filename, image, metadata in tqdm(self._loader.load(), total=self._loader.count):
|
||||
if not metadata: # Legacy faces. Update the headers
|
||||
if self._alignments is None:
|
||||
logger.error("Legacy faces have been discovered, but no alignments file "
|
||||
"provided. You must provide an alignments file for this face set")
|
||||
break
|
||||
|
||||
if not log_once:
|
||||
logger.warning("Legacy faces discovered. These faces will be updated")
|
||||
log_once = True
|
||||
|
||||
metadata = update_legacy_png_header(filename, self._alignments)
|
||||
if not metadata: # Face not found
|
||||
self._skip_count += 1
|
||||
logger.warning("Legacy face not found in alignments file. This face has not "
|
||||
"been updated: '%s'", filename)
|
||||
continue
|
||||
|
||||
if "source_frame_dims" not in metadata.get("source", {}):
|
||||
logger.error("The faces need to be re-extracted as at least some of them do not "
|
||||
"contain information required to correctly generate masks.")
|
||||
logger.error("You can re-extract the face-set by using the Alignments Tool's "
|
||||
"Extract job.")
|
||||
break
|
||||
|
||||
retval = self._process_face(filename, image, metadata)
|
||||
if retval is None:
|
||||
continue
|
||||
|
||||
yield retval
|
||||
|
||||
def _from_frames(self) -> T.Generator[ExtractMedia, None, None]:
|
||||
""" Load content from frames and and pair with corresponding metadata
|
||||
|
||||
Yields
|
||||
------
|
||||
:class:`plugins.pipeline.ExtractMedia`
|
||||
the extract media object for the processed face
|
||||
"""
|
||||
assert self._alignments is not None
|
||||
for filename, image in tqdm(self._loader.load(), total=self._loader.count):
|
||||
frame = os.path.basename(filename)
|
||||
|
||||
if not self._alignments.frame_exists(frame):
|
||||
self._skip_count += 1
|
||||
logger.warning("Skipping frame not in alignments file: '%s'", frame)
|
||||
continue
|
||||
|
||||
if not self._alignments.frame_has_faces(frame):
|
||||
logger.debug("Skipping frame with no faces: '%s'", frame)
|
||||
continue
|
||||
|
||||
faces_in_frame = self._alignments.get_faces_in_frame(frame)
|
||||
detected_faces = [self._get_detected_face(alignment) for alignment in faces_in_frame]
|
||||
retval = ExtractMedia(filename, image, detected_faces=detected_faces)
|
||||
yield retval
|
||||
|
||||
def load(self) -> T.Generator[ExtractMedia, None, None]:
|
||||
""" Load content from source and pair with corresponding alignment data
|
||||
|
||||
Yields
|
||||
------
|
||||
:class:`plugins.pipeline.ExtractMedia`
|
||||
the extract media object for the processed face
|
||||
"""
|
||||
if self._is_faces:
|
||||
iterator = self._from_faces
|
||||
else:
|
||||
iterator = self._from_frames
|
||||
|
||||
for media in iterator():
|
||||
yield media
|
||||
|
||||
if self._skip_count > 0:
|
||||
logger.warning("%s face(s) skipped due to not existing in the alignments file",
|
||||
self._skip_count)
|
||||
|
|
@ -4,31 +4,25 @@ from __future__ import annotations
|
|||
import logging
|
||||
import os
|
||||
import sys
|
||||
import typing as T
|
||||
|
||||
from argparse import Namespace
|
||||
from multiprocessing import Process
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from tqdm import tqdm
|
||||
from lib.align import Alignments
|
||||
|
||||
from lib.align import Alignments, AlignedFace, DetectedFace, update_legacy_png_header
|
||||
from lib.image import FacesLoader, ImagesLoader, ImagesSaver, encode_image
|
||||
from lib.utils import _video_extensions
|
||||
from plugins.extract.pipeline import ExtractMedia
|
||||
|
||||
from lib.multithreading import MultiThread
|
||||
from lib.utils import get_folder, _video_extensions
|
||||
from plugins.extract.pipeline import Extractor, ExtractMedia
|
||||
|
||||
if T.TYPE_CHECKING:
|
||||
from lib.align.aligned_face import CenteringType
|
||||
from lib.align.alignments import AlignmentFileDict, PNGHeaderDict
|
||||
from lib.queue_manager import EventQueue
|
||||
|
||||
logger = logging.getLogger(__name__) # pylint:disable=invalid-name
|
||||
from .loader import Loader
|
||||
from .mask_import import Import
|
||||
from .mask_generate import MaskGenerator
|
||||
from .mask_output import Output
|
||||
|
||||
|
||||
class Mask(): # pylint:disable=too-few-public-methods
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Mask: # pylint:disable=too-few-public-methods
|
||||
""" This tool is part of the Faceswap Tools suite and should be called from
|
||||
``python tools.py mask`` command.
|
||||
|
||||
|
|
@ -44,6 +38,10 @@ class Mask(): # pylint:disable=too-few-public-methods
|
|||
"""
|
||||
def __init__(self, arguments: Namespace) -> None:
|
||||
logger.debug("Initializing %s: (arguments: %s", self.__class__.__name__, arguments)
|
||||
if arguments.batch_mode and arguments.processing == "import":
|
||||
logger.error("Batch mode is not supported for 'import' processing")
|
||||
sys.exit(0)
|
||||
|
||||
self._args = arguments
|
||||
self._input_locations = self._get_input_locations()
|
||||
|
||||
|
|
@ -130,7 +128,7 @@ class Mask(): # pylint:disable=too-few-public-methods
|
|||
self._run_mask_process(arguments)
|
||||
|
||||
|
||||
class _Mask(): # pylint:disable=too-few-public-methods
|
||||
class _Mask: # pylint:disable=too-few-public-methods
|
||||
""" This tool is part of the Faceswap Tools suite and should be called from
|
||||
``python tools.py mask`` command.
|
||||
|
||||
|
|
@ -144,26 +142,36 @@ class _Mask(): # pylint:disable=too-few-public-methods
|
|||
"""
|
||||
def __init__(self, arguments: Namespace) -> None:
|
||||
logger.debug("Initializing %s: (arguments: %s)", self.__class__.__name__, arguments)
|
||||
|
||||
self._update_type = arguments.processing
|
||||
self._input_is_faces = arguments.input_type == "faces"
|
||||
self._mask_type = arguments.masker
|
||||
self._output = {"opts": {"blur_kernel": arguments.blur_kernel,
|
||||
"threshold": arguments.threshold},
|
||||
"type": arguments.output_type,
|
||||
"full_frame": arguments.full_frame,
|
||||
"suffix": self._get_output_suffix(arguments)}
|
||||
self._counts = {"face": 0, "skip": 0, "update": 0}
|
||||
|
||||
self._check_input(arguments.input)
|
||||
self._saver = self._set_saver(arguments)
|
||||
loader = FacesLoader if self._input_is_faces else ImagesLoader
|
||||
self._loader = loader(arguments.input)
|
||||
self._faces_saver: ImagesSaver | None = None
|
||||
|
||||
self._alignments = self._get_alignments(arguments)
|
||||
self._extractor = self._get_extractor(arguments.exclude_gpus)
|
||||
self._set_correct_mask_type()
|
||||
self._extractor_input_thread = self._feed_extractor()
|
||||
self._loader = Loader(arguments.input, self._input_is_faces)
|
||||
self._alignments = self._get_alignments(arguments.alignments, arguments.input)
|
||||
|
||||
self._output = Output(arguments, self._alignments, self._loader.file_list)
|
||||
|
||||
self._import = None
|
||||
if self._update_type == "import":
|
||||
self._import = Import(arguments.mask_path,
|
||||
arguments.centering,
|
||||
arguments.storage_size,
|
||||
self._input_is_faces,
|
||||
self._loader,
|
||||
self._alignments,
|
||||
arguments.input,
|
||||
arguments.masker)
|
||||
|
||||
self._mask_gen: MaskGenerator | None = None
|
||||
if self._update_type in ("all", "missing"):
|
||||
self._mask_gen = MaskGenerator(arguments.masker,
|
||||
self._update_type == "all",
|
||||
self._input_is_faces,
|
||||
self._loader,
|
||||
self._alignments,
|
||||
arguments.input,
|
||||
arguments.exclude_gpus)
|
||||
|
||||
logger.debug("Initialized %s", self.__class__.__name__)
|
||||
|
||||
|
|
@ -184,40 +192,16 @@ class _Mask(): # pylint:disable=too-few-public-methods
|
|||
sys.exit(0)
|
||||
logger.debug("input '%s' is valid", mask_input)
|
||||
|
||||
def _set_saver(self, arguments: Namespace) -> ImagesSaver | None:
|
||||
""" set the saver in a background thread
|
||||
|
||||
Parameters
|
||||
----------
|
||||
arguments: :class:`argparse.Namespace`
|
||||
The :mod:`argparse` arguments as passed in from :mod:`tools.py`
|
||||
|
||||
Returns
|
||||
-------
|
||||
``None`` or :class:`lib.image.ImagesSaver`:
|
||||
If output is requested, returns a :class:`lib.image.ImagesSaver` otherwise
|
||||
returns ``None``
|
||||
"""
|
||||
if not hasattr(arguments, "output") or arguments.output is None or not arguments.output:
|
||||
if self._update_type == "output":
|
||||
logger.error("Processing set as 'output' but no output folder provided.")
|
||||
sys.exit(0)
|
||||
logger.debug("No output provided. Not creating saver")
|
||||
return None
|
||||
output_dir = get_folder(arguments.output, make_folder=True)
|
||||
logger.info("Saving preview masks to: '%s'", output_dir)
|
||||
saver = ImagesSaver(output_dir)
|
||||
logger.debug(saver)
|
||||
return saver
|
||||
|
||||
def _get_alignments(self, arguments: Namespace) -> Alignments | None:
|
||||
def _get_alignments(self, alignments: str | None, input_location: str) -> Alignments | None:
|
||||
""" Obtain the alignments from either the given alignments location or the default
|
||||
location.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
arguments: :class:`argparse.Namespace`
|
||||
The :mod:`argparse` arguments as passed in from :mod:`tools.py`
|
||||
alignments: str | None
|
||||
Full path to the alignemnts file if provided or ``None`` if not
|
||||
input_location: str
|
||||
Full path to the source files to be used by the mask tool
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
|
@ -225,11 +209,11 @@ class _Mask(): # pylint:disable=too-few-public-methods
|
|||
If output is requested, returns a :class:`lib.image.ImagesSaver` otherwise
|
||||
returns ``None``
|
||||
"""
|
||||
if arguments.alignments:
|
||||
logger.debug("Alignments location provided: %s", arguments.alignments)
|
||||
return Alignments(os.path.dirname(arguments.alignments),
|
||||
filename=os.path.basename(arguments.alignments))
|
||||
if self._input_is_faces and arguments.processing == "output":
|
||||
if alignments:
|
||||
logger.debug("Alignments location provided: %s", alignments)
|
||||
return Alignments(os.path.dirname(alignments),
|
||||
filename=os.path.basename(alignments))
|
||||
if self._input_is_faces and self._update_type == "output":
|
||||
logger.debug("No alignments file provided for faces. Using PNG Header for output")
|
||||
return None
|
||||
if self._input_is_faces:
|
||||
|
|
@ -237,7 +221,7 @@ class _Mask(): # pylint:disable=too-few-public-methods
|
|||
"be updated in the faces' PNG Header")
|
||||
return None
|
||||
|
||||
folder = arguments.input
|
||||
folder = input_location
|
||||
if self._loader.is_video:
|
||||
logger.debug("Alignments from Video File: '%s'", folder)
|
||||
folder, filename = os.path.split(folder)
|
||||
|
|
@ -246,434 +230,74 @@ class _Mask(): # pylint:disable=too-few-public-methods
|
|||
logger.debug("Alignments from Input Folder: '%s'", folder)
|
||||
filename = "alignments"
|
||||
|
||||
return Alignments(folder, filename=filename)
|
||||
|
||||
def _get_extractor(self, exclude_gpus: list[int]) -> Extractor | None:
|
||||
""" Obtain a Mask extractor plugin and launch it
|
||||
Parameters
|
||||
----------
|
||||
exclude_gpus: list or ``None``
|
||||
A list of indices correlating to connected GPUs that Tensorflow should not use. Pass
|
||||
``None`` to not exclude any GPUs.
|
||||
Returns
|
||||
-------
|
||||
:class:`plugins.extract.pipeline.Extractor`:
|
||||
The launched Extractor
|
||||
"""
|
||||
if self._update_type == "output":
|
||||
logger.debug("Update type `output` selected. Not launching extractor")
|
||||
return None
|
||||
logger.debug("masker: %s", self._mask_type)
|
||||
extractor = Extractor(None, None, self._mask_type, exclude_gpus=exclude_gpus)
|
||||
extractor.launch()
|
||||
logger.debug(extractor)
|
||||
return extractor
|
||||
|
||||
def _set_correct_mask_type(self):
|
||||
""" Some masks have multiple variants that they can be saved as depending on config options
|
||||
so update the :attr:`_mask_type` accordingly
|
||||
"""
|
||||
if self._extractor is None or self._mask_type != "bisenet-fp":
|
||||
return
|
||||
|
||||
# Hacky look up into masker to get the type of mask
|
||||
mask_plugin = self._extractor._mask[0] # pylint:disable=protected-access
|
||||
assert mask_plugin is not None
|
||||
mtype = "head" if mask_plugin.config.get("include_hair", False) else "face"
|
||||
new_type = f"{self._mask_type}_{mtype}"
|
||||
logger.debug("Updating '%s' to '%s'", self._mask_type, new_type)
|
||||
self._mask_type = new_type
|
||||
|
||||
def _feed_extractor(self) -> MultiThread:
|
||||
""" Feed the input queue to the Extractor from a faces folder or from source frames in a
|
||||
background thread
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`lib.multithreading.Multithread`:
|
||||
The thread that is feeding the extractor.
|
||||
"""
|
||||
masker_input = getattr(self, f"_input_{'faces' if self._input_is_faces else 'frames'}")
|
||||
logger.debug("masker_input: %s", masker_input)
|
||||
|
||||
if self._update_type == "output":
|
||||
args: tuple = tuple()
|
||||
else:
|
||||
assert self._extractor is not None
|
||||
args = (self._extractor.input_queue, )
|
||||
input_thread = MultiThread(masker_input, *args, thread_count=1)
|
||||
input_thread.start()
|
||||
logger.debug(input_thread)
|
||||
return input_thread
|
||||
|
||||
def _process_face(self,
|
||||
filename: str,
|
||||
image: np.ndarray,
|
||||
metadata: PNGHeaderDict) -> ExtractMedia | None:
|
||||
""" Process a single face when masking from face images
|
||||
|
||||
filename: str
|
||||
the filename currently being processed
|
||||
image: :class:`numpy.ndarray`
|
||||
The current face being processed
|
||||
metadata: dict
|
||||
The source frame metadata from the PNG header
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`plugins.pipeline.ExtractMedia` or ``None``
|
||||
If the update type is 'output' then nothing is returned otherwise the extract media for
|
||||
the face is returned
|
||||
"""
|
||||
frame_name = metadata["source"]["source_filename"]
|
||||
face_index = metadata["source"]["face_index"]
|
||||
|
||||
if self._alignments is None: # mask from PNG header
|
||||
lookup_index = 0
|
||||
alignments = [T.cast("AlignmentFileDict", metadata["alignments"])]
|
||||
else: # mask from Alignments file
|
||||
lookup_index = face_index
|
||||
alignments = self._alignments.get_faces_in_frame(frame_name)
|
||||
if not alignments or face_index > len(alignments) - 1:
|
||||
self._counts["skip"] += 1
|
||||
logger.warning("Skipping Face not found in alignments file: '%s'", filename)
|
||||
return None
|
||||
|
||||
alignment = alignments[lookup_index]
|
||||
self._counts["face"] += 1
|
||||
|
||||
if self._check_for_missing(frame_name, face_index, alignment):
|
||||
return None
|
||||
|
||||
detected_face = self._get_detected_face(alignment)
|
||||
if self._update_type == "output":
|
||||
detected_face.image = image
|
||||
self._save(frame_name, face_index, detected_face)
|
||||
return None
|
||||
|
||||
media = ExtractMedia(filename, image, detected_faces=[detected_face], is_aligned=True)
|
||||
media.add_frame_metadata(metadata["source"])
|
||||
self._counts["update"] += 1
|
||||
return media
|
||||
|
||||
def _input_faces(self, *args: tuple | tuple[EventQueue]) -> None:
|
||||
""" Input pre-aligned faces to the Extractor plugin inside a thread
|
||||
|
||||
Parameters
|
||||
----------
|
||||
args: tuple
|
||||
The arguments that are to be loaded inside this thread. Contains the queue that the
|
||||
faces should be put to
|
||||
"""
|
||||
log_once = False
|
||||
logger.debug("args: %s", args)
|
||||
if self._update_type != "output":
|
||||
queue = T.cast("EventQueue", args[0])
|
||||
for filename, image, metadata in tqdm(self._loader.load(), total=self._loader.count):
|
||||
if not metadata: # Legacy faces. Update the headers
|
||||
if self._alignments is None:
|
||||
logger.error("Legacy faces have been discovered, but no alignments file "
|
||||
"provided. You must provide an alignments file for this face set")
|
||||
break
|
||||
|
||||
if not log_once:
|
||||
logger.warning("Legacy faces discovered. These faces will be updated")
|
||||
log_once = True
|
||||
|
||||
metadata = update_legacy_png_header(filename, self._alignments)
|
||||
if not metadata: # Face not found
|
||||
self._counts["skip"] += 1
|
||||
logger.warning("Legacy face not found in alignments file. This face has not "
|
||||
"been updated: '%s'", filename)
|
||||
continue
|
||||
|
||||
if "source_frame_dims" not in metadata.get("source", {}):
|
||||
logger.error("The faces need to be re-extracted as at least some of them do not "
|
||||
"contain information required to correctly generate masks.")
|
||||
logger.error("You can re-extract the face-set by using the Alignments Tool's "
|
||||
"Extract job.")
|
||||
break
|
||||
media = self._process_face(filename, image, metadata)
|
||||
if media is not None:
|
||||
queue.put(media)
|
||||
|
||||
if self._update_type != "output":
|
||||
queue.put("EOF")
|
||||
|
||||
def _input_frames(self, *args: tuple | tuple[EventQueue]) -> None:
|
||||
""" Input frames to the Extractor plugin inside a thread
|
||||
|
||||
Parameters
|
||||
----------
|
||||
args: tuple
|
||||
The arguments that are to be loaded inside this thread. Contains the queue that the
|
||||
faces should be put to
|
||||
"""
|
||||
assert self._alignments is not None
|
||||
logger.debug("args: %s", args)
|
||||
if self._update_type != "output":
|
||||
queue = T.cast("EventQueue", args[0])
|
||||
for filename, image in tqdm(self._loader.load(), total=self._loader.count):
|
||||
frame = os.path.basename(filename)
|
||||
if not self._alignments.frame_exists(frame):
|
||||
self._counts["skip"] += 1
|
||||
logger.warning("Skipping frame not in alignments file: '%s'", frame)
|
||||
continue
|
||||
if not self._alignments.frame_has_faces(frame):
|
||||
logger.debug("Skipping frame with no faces: '%s'", frame)
|
||||
continue
|
||||
|
||||
faces_in_frame = self._alignments.get_faces_in_frame(frame)
|
||||
self._counts["face"] += len(faces_in_frame)
|
||||
|
||||
# To keep face indexes correct/cover off where only one face in an image is missing a
|
||||
# mask where there are multiple faces we process all faces again for any frames which
|
||||
# have missing masks.
|
||||
if all(self._check_for_missing(frame, idx, alignment)
|
||||
for idx, alignment in enumerate(faces_in_frame)):
|
||||
continue
|
||||
|
||||
detected_faces = [self._get_detected_face(alignment) for alignment in faces_in_frame]
|
||||
if self._update_type == "output":
|
||||
for idx, detected_face in enumerate(detected_faces):
|
||||
detected_face.image = image
|
||||
self._save(frame, idx, detected_face)
|
||||
else:
|
||||
self._counts["update"] += len(detected_faces)
|
||||
queue.put(ExtractMedia(filename, image, detected_faces=detected_faces))
|
||||
if self._update_type != "output":
|
||||
queue.put("EOF")
|
||||
|
||||
def _check_for_missing(self, frame: str, idx: int, alignment: AlignmentFileDict) -> bool:
|
||||
""" Check if the alignment is missing the requested mask_type
|
||||
|
||||
Parameters
|
||||
----------
|
||||
frame: str
|
||||
The frame name in the alignments file
|
||||
idx: int
|
||||
The index of the face for this frame in the alignments file
|
||||
alignment: dict
|
||||
The alignment for a face
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool:
|
||||
``True`` if the update_type is "missing" and the mask does not exist in the alignments
|
||||
file otherwise ``False``
|
||||
"""
|
||||
retval = (self._update_type == "missing" and
|
||||
alignment.get("mask", None) is not None and
|
||||
alignment["mask"].get(self._mask_type, None) is not None)
|
||||
if retval:
|
||||
logger.debug("Mask pre-exists for face: '%s' - %s", frame, idx)
|
||||
retval = Alignments(folder, filename=filename)
|
||||
self._loader.add_alignments(retval)
|
||||
return retval
|
||||
|
||||
def _get_output_suffix(self, arguments: Namespace) -> str:
|
||||
""" The filename suffix, based on selected output options.
|
||||
def _save_output(self, media: ExtractMedia) -> None:
|
||||
""" Output masks to disk
|
||||
|
||||
Parameters
|
||||
----------
|
||||
arguments: :class:`argparse.Namespace`
|
||||
The command line arguments for the mask tool
|
||||
|
||||
Returns
|
||||
-------
|
||||
str:
|
||||
The suffix to be appended to the output filename
|
||||
media: :class:`~plugins.extract.pipeline.ExtractMedia`
|
||||
The extract media holding the faces to output
|
||||
"""
|
||||
sfx = "mask_preview_"
|
||||
sfx += "face_" if not arguments.full_frame or self._input_is_faces else "frame_"
|
||||
sfx += f"{arguments.output_type}.png"
|
||||
return sfx
|
||||
filename = os.path.basename(media.frame_metadata["source_filename"]
|
||||
if self._input_is_faces else media.filename)
|
||||
dims = media.frame_metadata["source_frame_dims"] if self._input_is_faces else None
|
||||
for idx, face in enumerate(media.detected_faces):
|
||||
face_idx = media.frame_metadata["face_index"] if self._input_is_faces else idx
|
||||
face.image = media.image
|
||||
self._output.save(filename, face_idx, face, frame_dims=dims)
|
||||
|
||||
@classmethod
|
||||
def _get_detected_face(cls, alignment: AlignmentFileDict) -> DetectedFace:
|
||||
""" Convert an alignment dict item to a detected_face object
|
||||
def _generate_masks(self) -> None:
|
||||
""" Generate masks from a mask plugin """
|
||||
assert self._mask_gen is not None
|
||||
|
||||
Parameters
|
||||
----------
|
||||
alignment: dict
|
||||
The alignment dict for a face
|
||||
logger.info("Generating masks")
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`lib.FacesDetect.detected_face`:
|
||||
The corresponding detected_face object for the alignment
|
||||
"""
|
||||
detected_face = DetectedFace()
|
||||
detected_face.from_alignment(alignment)
|
||||
return detected_face
|
||||
for media in self._mask_gen.process():
|
||||
if self._output.should_save:
|
||||
self._save_output(media)
|
||||
|
||||
def _import_masks(self) -> None:
|
||||
""" Import masks that have been generated outside of faceswap """
|
||||
assert self._import is not None
|
||||
logger.info("Importing masks")
|
||||
|
||||
for media in self._loader.load():
|
||||
self._import.import_mask(media)
|
||||
if self._output.should_save:
|
||||
self._save_output(media)
|
||||
|
||||
if self._alignments is not None and self._import.update_count > 0:
|
||||
self._alignments.backup()
|
||||
self._alignments.save()
|
||||
|
||||
if self._import.skip_count > 0:
|
||||
logger.warning("No masks were found for %s item(s), so these have not been imported",
|
||||
self._import.skip_count)
|
||||
|
||||
logger.info("Imported masks for %s faces of %s",
|
||||
self._import.update_count, self._import.update_count + self._import.skip_count)
|
||||
|
||||
def _output_masks(self) -> None:
|
||||
""" Output masks to selected output folder """
|
||||
for media in self._loader.load():
|
||||
self._save_output(media)
|
||||
|
||||
def process(self) -> None:
|
||||
""" The entry point for the Mask tool from :file:`lib.tools.cli`. Runs the Mask process """
|
||||
logger.debug("Starting masker process")
|
||||
updater = getattr(self, f"_update_{'faces' if self._input_is_faces else 'frames'}")
|
||||
if self._update_type != "output":
|
||||
assert self._extractor is not None
|
||||
if self._input_is_faces:
|
||||
self._faces_saver = ImagesSaver(self._loader.location, as_bytes=True)
|
||||
for extractor_output in self._extractor.detected_faces():
|
||||
self._extractor_input_thread.check_and_raise_error()
|
||||
updater(extractor_output)
|
||||
|
||||
if self._counts["update"] != 0 and self._alignments is not None:
|
||||
self._alignments.backup()
|
||||
self._alignments.save()
|
||||
if self._update_type in ("all", "missing"):
|
||||
self._generate_masks()
|
||||
|
||||
if self._input_is_faces:
|
||||
assert self._faces_saver is not None
|
||||
self._faces_saver.close()
|
||||
if self._update_type == "import":
|
||||
self._import_masks()
|
||||
|
||||
self._extractor_input_thread.join()
|
||||
if self._saver is not None:
|
||||
self._saver.close()
|
||||
if self._update_type == "output":
|
||||
self._output_masks()
|
||||
|
||||
if self._counts["skip"] != 0:
|
||||
logger.warning("%s face(s) skipped due to not existing in the alignments file",
|
||||
self._counts["skip"])
|
||||
if self._update_type != "output":
|
||||
if self._counts["update"] == 0:
|
||||
logger.warning("No masks were updated of the %s faces seen", self._counts["face"])
|
||||
else:
|
||||
logger.info("Updated masks for %s faces of %s",
|
||||
self._counts["update"], self._counts["face"])
|
||||
self._output.close()
|
||||
logger.debug("Completed masker process")
|
||||
|
||||
def _update_faces(self, extractor_output: ExtractMedia) -> None:
|
||||
""" Update alignments for the mask if the input type is a faces folder
|
||||
|
||||
If an output location has been indicated, then puts the mask preview to the save queue
|
||||
|
||||
Parameters
|
||||
----------
|
||||
extractor_output: :class:`plugins.extract.pipeline.ExtractMedia`
|
||||
The output from the :class:`plugins.extract.pipeline.Extractor` object
|
||||
"""
|
||||
assert self._faces_saver is not None
|
||||
for face in extractor_output.detected_faces:
|
||||
frame_name = extractor_output.frame_metadata["source_filename"]
|
||||
face_index = extractor_output.frame_metadata["face_index"]
|
||||
logger.trace("Saving face: (frame: %s, face index: %s)", # type: ignore
|
||||
frame_name, face_index)
|
||||
|
||||
if self._alignments is not None:
|
||||
self._alignments.update_face(frame_name, face_index, face.to_alignment())
|
||||
|
||||
metadata: PNGHeaderDict = {"alignments": face.to_png_meta(),
|
||||
"source": extractor_output.frame_metadata}
|
||||
self._faces_saver.save(extractor_output.filename,
|
||||
encode_image(extractor_output.image, ".png", metadata=metadata))
|
||||
|
||||
if self._saver is not None:
|
||||
face.image = extractor_output.image
|
||||
self._save(frame_name, face_index, face)
|
||||
|
||||
def _update_frames(self, extractor_output: ExtractMedia) -> None:
|
||||
""" Update alignments for the mask if the input type is a frames folder or video
|
||||
|
||||
If an output location has been indicated, then puts the mask preview to the save queue
|
||||
|
||||
Parameters
|
||||
----------
|
||||
extractor_output: :class:`plugins.extract.pipeline.ExtractMedia`
|
||||
The output from the :class:`plugins.extract.pipeline.Extractor` object
|
||||
"""
|
||||
assert self._alignments is not None
|
||||
frame = os.path.basename(extractor_output.filename)
|
||||
for idx, face in enumerate(extractor_output.detected_faces):
|
||||
self._alignments.update_face(frame, idx, face.to_alignment())
|
||||
if self._saver is not None:
|
||||
face.image = extractor_output.image
|
||||
self._save(frame, idx, face)
|
||||
|
||||
def _save(self, frame: str, idx: int, detected_face: DetectedFace) -> None:
|
||||
""" Build the mask preview image and save
|
||||
|
||||
Parameters
|
||||
----------
|
||||
frame: str
|
||||
The frame name in the alignments file
|
||||
idx: int
|
||||
The index of the face for this frame in the alignments file
|
||||
detected_face: `lib.FacesDetect.detected_face`
|
||||
A detected_face object for a face
|
||||
"""
|
||||
assert self._saver is not None
|
||||
if self._mask_type == "bisenet-fp":
|
||||
mask_types = [f"{self._mask_type}_{area}" for area in ("face", "head")]
|
||||
else:
|
||||
mask_types = [self._mask_type]
|
||||
|
||||
if detected_face.mask is None or not any(mask in detected_face.mask
|
||||
for mask in mask_types):
|
||||
logger.warning("Mask type '%s' does not exist for frame '%s' index %s. Skipping",
|
||||
self._mask_type, frame, idx)
|
||||
return
|
||||
|
||||
for mask_type in mask_types:
|
||||
if mask_type not in detected_face.mask:
|
||||
# If extracting bisenet mask, then skip versions which don't exist
|
||||
continue
|
||||
filename = os.path.join(
|
||||
self._saver.location,
|
||||
f"{os.path.splitext(frame)[0]}_{idx}_{mask_type}_{self._output['suffix']}")
|
||||
image = self._create_image(detected_face, mask_type)
|
||||
logger.trace("filename: '%s', image_shape: %s", filename, image.shape) # type: ignore
|
||||
self._saver.save(filename, image)
|
||||
|
||||
def _create_image(self, detected_face: DetectedFace, mask_type: str) -> np.ndarray:
|
||||
""" Create a mask preview image for saving out to disk
|
||||
|
||||
Parameters
|
||||
----------
|
||||
detected_face: `lib.FacesDetect.detected_face`
|
||||
A detected_face object for a face
|
||||
mask_type: str
|
||||
The stored mask type name to create the image for
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`numpy.ndarray`:
|
||||
A preview image depending on the output type in one of the following forms:
|
||||
- Containing 3 sub images: The original face, the masked face and the mask
|
||||
- The mask only
|
||||
- The masked face
|
||||
"""
|
||||
mask = detected_face.mask[mask_type]
|
||||
assert detected_face.image is not None
|
||||
mask.set_blur_and_threshold(**self._output["opts"])
|
||||
if not self._output["full_frame"] or self._input_is_faces:
|
||||
if self._input_is_faces:
|
||||
face = AlignedFace(detected_face.landmarks_xy,
|
||||
image=detected_face.image,
|
||||
centering=mask.stored_centering,
|
||||
size=detected_face.image.shape[0],
|
||||
is_aligned=True).face
|
||||
else:
|
||||
centering: CenteringType = ("legacy" if self._alignments is not None and
|
||||
self._alignments.version == 1.0
|
||||
else mask.stored_centering)
|
||||
detected_face.load_aligned(detected_face.image, centering=centering, force=True)
|
||||
face = detected_face.aligned.face
|
||||
assert face is not None
|
||||
imask = cv2.resize(detected_face.mask[mask_type].mask,
|
||||
(face.shape[1], face.shape[0]),
|
||||
interpolation=cv2.INTER_CUBIC)[..., None]
|
||||
else:
|
||||
face = np.array(detected_face.image) # cv2 fails if this comes as imageio.core.Array
|
||||
imask = mask.get_full_frame_mask(face.shape[1], face.shape[0])
|
||||
imask = np.expand_dims(imask, -1)
|
||||
|
||||
height, width = face.shape[:2]
|
||||
if self._output["type"] == "combined":
|
||||
masked = (face.astype("float32") * imask.astype("float32") / 255.).astype("uint8")
|
||||
imask = np.tile(imask, 3)
|
||||
for img in (face, masked, imask):
|
||||
cv2.rectangle(img, (0, 0), (width - 1, height - 1), (255, 255, 255), 1)
|
||||
out_image = np.concatenate((face, masked, imask), axis=1)
|
||||
elif self._output["type"] == "mask":
|
||||
out_image = imask
|
||||
elif self._output["type"] == "masked":
|
||||
out_image = np.concatenate([face, imask], axis=-1)
|
||||
return out_image
|
||||
|
|
|
|||
269
tools/mask/mask_generate.py
Normal file
269
tools/mask/mask_generate.py
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
#!/usr/bin/env python3
|
||||
""" Handles the generation of masks from faceswap for upating into an alignments file """
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import typing as T
|
||||
|
||||
from lib.image import encode_image, ImagesSaver
|
||||
from lib.multithreading import MultiThread
|
||||
from plugins.extract.pipeline import Extractor
|
||||
|
||||
if T.TYPE_CHECKING:
|
||||
from lib.align import Alignments, DetectedFace
|
||||
from lib.align.alignments import PNGHeaderDict
|
||||
from lib.queue_manager import EventQueue
|
||||
from plugins.extract.pipeline import ExtractMedia
|
||||
from .loader import Loader
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MaskGenerator: # pylint:disable=too-few-public-methods
|
||||
""" Uses faceswap's extract pipeline to generate masks and update them into the alignments file
|
||||
and/or extracted face PNG Headers
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mask_type: str
|
||||
The mask type to generate
|
||||
update_all: bool
|
||||
``True`` to update all faces, ``False`` to only update faces missing masks
|
||||
input_is_faces: bool
|
||||
``True`` if the input are faceswap extracted faces otherwise ``False``
|
||||
exclude_gpus: list[int]
|
||||
List of any GPU IDs that should be excluded
|
||||
loader: :class:`tools.mask.loader.Loader`
|
||||
The loader for loading source images/video from disk
|
||||
"""
|
||||
def __init__(self,
|
||||
mask_type: str,
|
||||
update_all: bool,
|
||||
input_is_faces: bool,
|
||||
loader: Loader,
|
||||
alignments: Alignments | None,
|
||||
input_location: str,
|
||||
exclude_gpus: list[int]) -> None:
|
||||
logger.debug("Initializing %s (mask_type: %s, update_all: %s, input_is_faces: %s, "
|
||||
"loader: %s, alignments: %s, input_location: %s, exclude_gpus: %s)",
|
||||
self.__class__.__name__, mask_type, update_all, input_is_faces, loader,
|
||||
alignments, input_location, exclude_gpus)
|
||||
|
||||
self._update_all = update_all
|
||||
self._is_faces = input_is_faces
|
||||
self._alignments = alignments
|
||||
|
||||
self._extractor = self._get_extractor(mask_type, exclude_gpus)
|
||||
self._mask_type = self._set_correct_mask_type(mask_type)
|
||||
self._input_thread = self._set_loader_thread(loader)
|
||||
self._saver = ImagesSaver(input_location, as_bytes=True) if input_is_faces else None
|
||||
|
||||
self._counts: dict[T.Literal["face", "update"], int] = {"face": 0, "update": 0}
|
||||
|
||||
logger.debug("Initialized %s", self.__class__.__name__)
|
||||
|
||||
def _get_extractor(self, mask_type, exclude_gpus: list[int]) -> Extractor:
|
||||
""" Obtain a Mask extractor plugin and launch it
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mask_type: str
|
||||
The mask type to generate
|
||||
exclude_gpus: list or ``None``
|
||||
A list of indices correlating to connected GPUs that Tensorflow should not use. Pass
|
||||
``None`` to not exclude any GPUs.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`plugins.extract.pipeline.Extractor`:
|
||||
The launched Extractor
|
||||
"""
|
||||
logger.debug("masker: %s", mask_type)
|
||||
extractor = Extractor(None, None, mask_type, exclude_gpus=exclude_gpus)
|
||||
extractor.launch()
|
||||
logger.debug(extractor)
|
||||
return extractor
|
||||
|
||||
def _set_correct_mask_type(self, mask_type: str) -> str:
|
||||
""" Some masks have multiple variants that they can be saved as depending on config options
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mask_type: str
|
||||
The mask type to generate
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The actual mask variant to update
|
||||
"""
|
||||
if mask_type != "bisenet-fp":
|
||||
return mask_type
|
||||
|
||||
# Hacky look up into masker to get the type of mask
|
||||
mask_plugin = self._extractor._mask[0] # pylint:disable=protected-access
|
||||
assert mask_plugin is not None
|
||||
mtype = "head" if mask_plugin.config.get("include_hair", False) else "face"
|
||||
new_type = f"{mask_type}_{mtype}"
|
||||
logger.debug("Updating '%s' to '%s'", mask_type, new_type)
|
||||
return new_type
|
||||
|
||||
def _needs_update(self, frame: str, idx: int, face: DetectedFace) -> bool:
|
||||
""" Check if the mask for the current alignment needs updating for the requested mask_type
|
||||
|
||||
Parameters
|
||||
----------
|
||||
frame: str
|
||||
The frame name in the alignments file
|
||||
idx: int
|
||||
The index of the face for this frame in the alignments file
|
||||
face: :class:`~lib.align.DetectedFace`
|
||||
The dected face object to check
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool:
|
||||
``True`` if the mask needs to be updated otherwise ``False``
|
||||
"""
|
||||
if self._update_all:
|
||||
return True
|
||||
|
||||
retval = not face.mask or face.mask.get(self._mask_type, None) is None
|
||||
|
||||
logger.trace("Needs updating: %s, '%s' - %s", # type:ignore[attr-defined]
|
||||
retval, frame, idx)
|
||||
return retval
|
||||
|
||||
def _feed_extractor(self, loader: Loader, extract_queue: EventQueue) -> None:
|
||||
""" Process to feed the extractor from inside a thread
|
||||
|
||||
Parameters
|
||||
----------
|
||||
loader: class:`tools.mask.loader.Loader`
|
||||
The loader for loading source images/video from disk
|
||||
extract_queue: :class:`lib.queue_manager.EventQueue`
|
||||
The input queue to the extraction pipeline
|
||||
"""
|
||||
for media in loader.load():
|
||||
self._counts["face"] += len(media.detected_faces)
|
||||
|
||||
if self._is_faces:
|
||||
assert len(media.detected_faces) == 1
|
||||
needs_update = self._needs_update(media.frame_metadata["source_filename"],
|
||||
media.frame_metadata["face_index"],
|
||||
media.detected_faces[0])
|
||||
else:
|
||||
# To keep face indexes correct/cover off where only one face in an image is missing
|
||||
# a mask where there are multiple faces we process all faces again for any frames
|
||||
# which have missing masks.
|
||||
needs_update = any(self._needs_update(media.filename, idx, detected_face)
|
||||
for idx, detected_face in enumerate(media.detected_faces))
|
||||
|
||||
if not needs_update:
|
||||
logger.trace("No masks need updating in '%s'", # type:ignore[attr-defined]
|
||||
media.filename)
|
||||
continue
|
||||
|
||||
logger.trace("Passing to extractor: '%s'", media.filename) # type:ignore[attr-defined]
|
||||
extract_queue.put(media)
|
||||
|
||||
logger.debug("Terminating loader thread")
|
||||
extract_queue.put("EOF")
|
||||
|
||||
def _set_loader_thread(self, loader: Loader) -> MultiThread:
|
||||
""" Set the iterator to load ExtractMedia objects into the mask extraction pipeline
|
||||
so we can just iterate through the output masks
|
||||
|
||||
Parameters
|
||||
----------
|
||||
loader: class:`tools.mask.loader.Loader`
|
||||
The loader for loading source images/video from disk
|
||||
"""
|
||||
in_queue = self._extractor.input_queue
|
||||
logger.debug("Starting load thread: (loader: %s, queue: %s)", loader, in_queue)
|
||||
in_thread = MultiThread(self._feed_extractor, loader, in_queue, thread_count=1)
|
||||
in_thread.start()
|
||||
logger.debug("Started load thread: %s", in_thread)
|
||||
return in_thread
|
||||
|
||||
def _update_from_face(self, media: ExtractMedia) -> None:
|
||||
""" Update the alignments file and/or the extracted face
|
||||
|
||||
Parameters
|
||||
----------
|
||||
media: :class:`~lib.extract.pipeline.ExtractMedia`
|
||||
The ExtractMedia object with updated masks
|
||||
"""
|
||||
assert len(media.detected_faces) == 1
|
||||
assert self._saver is not None
|
||||
|
||||
fname = media.frame_metadata["source_filename"]
|
||||
idx = media.frame_metadata["face_index"]
|
||||
face = media.detected_faces[0]
|
||||
|
||||
if self._alignments is not None:
|
||||
logger.trace("Updating face %s in frame '%s'", idx, fname) # type:ignore[attr-defined]
|
||||
self._alignments.update_face(fname, idx, face.to_alignment())
|
||||
|
||||
logger.trace("Updating extracted face: '%s'", media.filename) # type:ignore[attr-defined]
|
||||
meta: PNGHeaderDict = {"alignments": face.to_png_meta(), "source": media.frame_metadata}
|
||||
self._saver.save(media.filename, encode_image(media.image, ".png", metadata=meta))
|
||||
|
||||
def _update_from_frame(self, media: ExtractMedia) -> None:
|
||||
""" Update the alignments file
|
||||
|
||||
Parameters
|
||||
----------
|
||||
media: :class:`~lib.extract.pipeline.ExtractMedia`
|
||||
The ExtractMedia object with updated masks
|
||||
"""
|
||||
assert self._alignments is not None
|
||||
fname = os.path.basename(media.filename)
|
||||
logger.trace("Updating %s faces in frame '%s'", # type:ignore[attr-defined]
|
||||
len(media.detected_faces), fname)
|
||||
for idx, face in enumerate(media.detected_faces):
|
||||
self._alignments.update_face(fname, idx, face.to_alignment())
|
||||
|
||||
def _finalize(self) -> None:
|
||||
""" Close thread and save alignments on completion """
|
||||
logger.debug("Finalizing MaskGenerator")
|
||||
self._input_thread.join()
|
||||
|
||||
if self._counts["update"] > 0 and self._alignments is not None:
|
||||
logger.debug("Saving alignments")
|
||||
self._alignments.backup()
|
||||
self._alignments.save()
|
||||
|
||||
if self._saver is not None:
|
||||
logger.debug("Closing face saver")
|
||||
self._saver.close()
|
||||
|
||||
if self._counts["update"] == 0:
|
||||
logger.warning("No masks were updated of the %s faces seen", self._counts["face"])
|
||||
else:
|
||||
logger.info("Updated masks for %s faces of %s",
|
||||
self._counts["update"], self._counts["face"])
|
||||
|
||||
def process(self) -> T.Generator[ExtractMedia, None, None]:
|
||||
""" Process the output from the extractor pipeline
|
||||
|
||||
Yields
|
||||
------
|
||||
:class:`~lib.extract.pipeline.ExtractMedia`
|
||||
The ExtractMedia object with updated masks
|
||||
"""
|
||||
for media in self._extractor.detected_faces():
|
||||
self._input_thread.check_and_raise_error()
|
||||
self._counts["update"] += len(media.detected_faces)
|
||||
|
||||
if self._is_faces:
|
||||
self._update_from_face(media)
|
||||
else:
|
||||
self._update_from_frame(media)
|
||||
|
||||
yield media
|
||||
|
||||
self._finalize()
|
||||
logger.debug("Completed MaskGenerator process")
|
||||
406
tools/mask/mask_import.py
Normal file
406
tools/mask/mask_import.py
Normal file
|
|
@ -0,0 +1,406 @@
|
|||
#!/usr/bin/env python3
|
||||
""" Import mask processing for faceswap's mask tool """
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import typing as T
|
||||
|
||||
import cv2
|
||||
from tqdm import tqdm
|
||||
|
||||
from lib.align import AlignedFace
|
||||
from lib.image import encode_image, ImagesSaver
|
||||
from lib.utils import get_image_paths
|
||||
|
||||
if T.TYPE_CHECKING:
|
||||
import numpy as np
|
||||
from .loader import Loader
|
||||
from plugins.extract.pipeline import ExtractMedia
|
||||
from lib.align import Alignments, DetectedFace
|
||||
from lib.align.alignments import PNGHeaderDict
|
||||
from lib.align.aligned_face import CenteringType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Import: # pylint:disable=too-few-public-methods
|
||||
""" Import masks from disk into an Alignments file
|
||||
|
||||
Parameters
|
||||
----------
|
||||
import_path: str
|
||||
The path to the input images
|
||||
centering: Literal["face", "head", "legacy"]
|
||||
The centering to store the mask at
|
||||
storage_size: int
|
||||
The size to store the mask at
|
||||
input_is_faces: bool
|
||||
``True`` if the input is aligned faces otherwise ``False``
|
||||
loader: :class:`~tools.mask.loader.Loader`
|
||||
The source file loader object
|
||||
alignments: :class:`~lib.align.alignments.Alignments` | None
|
||||
The alignments file object for the faces, if provided
|
||||
mask_type: str
|
||||
The mask type to update to
|
||||
"""
|
||||
def __init__(self,
|
||||
import_path: str,
|
||||
centering: CenteringType,
|
||||
storage_size: int,
|
||||
input_is_faces: bool,
|
||||
loader: Loader,
|
||||
alignments: Alignments | None,
|
||||
input_location: str,
|
||||
mask_type: str) -> None:
|
||||
logger.debug("Initializing %s (import_path: %s, centering: %s, storage_size: %s, "
|
||||
"input_is_faces: %s, loader: %s, alignments: %s, input_location: %s, "
|
||||
"mask_type: %s)", self.__class__.__name__, import_path, centering,
|
||||
storage_size, input_is_faces, loader, alignments, input_location, mask_type)
|
||||
|
||||
self._validate_mask_type(mask_type)
|
||||
|
||||
self._centering = centering
|
||||
self._size = storage_size
|
||||
self._is_faces = input_is_faces
|
||||
self._alignments = alignments
|
||||
self._re_frame_num = re.compile(r"\d+$")
|
||||
self._mapping = self._generate_mapping(import_path, loader)
|
||||
|
||||
self._saver = ImagesSaver(input_location, as_bytes=True) if input_is_faces else None
|
||||
self._counts: dict[T.Literal["skip", "update"], int] = {"skip": 0, "update": 0}
|
||||
|
||||
logger.debug("Initialized %s", self.__class__.__name__)
|
||||
|
||||
@property
|
||||
def skip_count(self) -> int:
|
||||
""" int: Number of masks that were skipped as they do not exist for given faces """
|
||||
return self._counts["skip"]
|
||||
|
||||
@property
|
||||
def update_count(self) -> int:
|
||||
""" int: Number of masks that were skipped as they do not exist for given faces """
|
||||
return self._counts["update"]
|
||||
|
||||
@classmethod
|
||||
def _validate_mask_type(cls, mask_type: str) -> None:
|
||||
""" Validate that the mask type is 'custom' to ensure user does not accidentally overwrite
|
||||
existing masks they may have editted
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mask_type: str
|
||||
The mask type that has been selected
|
||||
"""
|
||||
if mask_type == "custom":
|
||||
return
|
||||
|
||||
logger.error("Masker 'custom' must be selected for importing masks")
|
||||
sys.exit(1)
|
||||
|
||||
@classmethod
|
||||
def _get_file_list(cls, path: str) -> list[str]:
|
||||
""" Check the nask folder exists and obtain the list of images
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path: str
|
||||
Full path to the location of mask images to be imported
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[str]
|
||||
list of full paths to all of the images in the mask folder
|
||||
"""
|
||||
if not os.path.isdir(path):
|
||||
logger.error("Mask path: '%s' is not a folder", path)
|
||||
sys.exit(1)
|
||||
paths = get_image_paths(path)
|
||||
if not paths:
|
||||
logger.error("Mask path '%s' contains no images", path)
|
||||
sys.exit(1)
|
||||
return paths
|
||||
|
||||
def _warn_extra_masks(self, file_list: list[str]) -> None:
|
||||
""" Generate a warning for each mask that exists that does not correspond to a match in the
|
||||
source input
|
||||
|
||||
Parameters
|
||||
----------
|
||||
file_list: list[str]
|
||||
List of mask files that could not be mapped to a source image
|
||||
"""
|
||||
if not file_list:
|
||||
logger.debug("All masks exist in the source data")
|
||||
return
|
||||
|
||||
for fname in file_list:
|
||||
logger.warning("Extra mask file found: '%s'", os.path.basename(fname))
|
||||
|
||||
logger.warning("%s mask file(s) do not exist in the source data so will not be imported "
|
||||
"(see above)", len(file_list))
|
||||
|
||||
def _file_list_to_frame_number(self, file_list: list[str]) -> dict[int, str]:
|
||||
""" Extract frame numbers from mask file names and return as a dictionary
|
||||
|
||||
Parameters
|
||||
----------
|
||||
file_list: list[str]
|
||||
List of full paths to masks to extract frame number from
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict[int, str]
|
||||
Dictionary of frame numbers to filenames
|
||||
"""
|
||||
retval: dict[int, str] = {}
|
||||
for filename in file_list:
|
||||
frame_num = self._re_frame_num.findall(os.path.splitext(os.path.basename(filename))[0])
|
||||
|
||||
if not frame_num or len(frame_num) > 1:
|
||||
logger.error("Could not detect frame number from mask file '%s'. "
|
||||
"Check your filenames", os.path.basename(filename))
|
||||
sys.exit(1)
|
||||
|
||||
fnum = int(frame_num[0])
|
||||
|
||||
if fnum in retval:
|
||||
logger.error("Frame number %s for mask file '%s' already exists from file: '%s'. "
|
||||
"Check your filenames",
|
||||
fnum, os.path.basename(filename), os.path.basename(retval[fnum]))
|
||||
sys.exit(1)
|
||||
|
||||
retval[fnum] = filename
|
||||
|
||||
logger.debug("Files: %s, frame_numbers: %s", len(file_list), len(retval))
|
||||
|
||||
return retval
|
||||
|
||||
def _map_video(self, file_list: list[str], source_files: list[str]) -> dict[str, str]:
|
||||
""" Generate the mapping between the source data and the masks to be imported for
|
||||
video sources
|
||||
|
||||
Parameters
|
||||
----------
|
||||
file_list: list[str]
|
||||
List of full paths to masks to be imported
|
||||
source_files: list[str]
|
||||
list of filenames withing the source file
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict[str, str]
|
||||
Source filenames mapped to full path location of mask to be imported
|
||||
"""
|
||||
retval = {}
|
||||
unmapped = []
|
||||
mask_frames = self._file_list_to_frame_number(file_list)
|
||||
for filename in tqdm(source_files, desc="Mapping masks to input", leave=False):
|
||||
src_idx = int(os.path.splitext(filename)[0].rsplit("_", maxsplit=1)[-1])
|
||||
mapped = mask_frames.pop(src_idx, "")
|
||||
if not mapped:
|
||||
unmapped.append(filename)
|
||||
continue
|
||||
retval[os.path.basename(filename)] = mapped
|
||||
|
||||
if len(unmapped) == len(source_files):
|
||||
logger.error("No masks map between the source data and the mask folder. "
|
||||
"Check your filenames")
|
||||
sys.exit(1)
|
||||
|
||||
self._warn_extra_masks(list(mask_frames.values()))
|
||||
logger.debug("Source: %s, Mask: %s, Mapped: %s",
|
||||
len(source_files), len(file_list), len(retval))
|
||||
return retval
|
||||
|
||||
def _map_images(self, file_list: list[str], source_files: list[str]) -> dict[str, str]:
|
||||
""" Generate the mapping between the source data and the masks to be imported for
|
||||
folder of image sources
|
||||
|
||||
Parameters
|
||||
----------
|
||||
file_list: list[str]
|
||||
List of full paths to masks to be imported
|
||||
source_files: list[str]
|
||||
list of filenames withing the source file
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict[str, str]
|
||||
Source filenames mapped to full path location of mask to be imported
|
||||
"""
|
||||
mask_count = len(file_list)
|
||||
retval = {}
|
||||
unmapped = []
|
||||
for filename in tqdm(source_files, desc="Mapping masks to input", leave=False):
|
||||
fname = os.path.splitext(os.path.basename(filename))[0]
|
||||
mapped = next((f for f in file_list
|
||||
if os.path.splitext(os.path.basename(f))[0] == fname), "")
|
||||
if not mapped:
|
||||
unmapped.append(filename)
|
||||
continue
|
||||
retval[os.path.basename(filename)] = file_list.pop(file_list.index(mapped))
|
||||
|
||||
if len(unmapped) == len(source_files):
|
||||
logger.error("No masks map between the source data and the mask folder. "
|
||||
"Check your filenames")
|
||||
sys.exit(1)
|
||||
|
||||
self._warn_extra_masks(file_list)
|
||||
|
||||
logger.debug("Source: %s, Mask: %s, Mapped: %s",
|
||||
len(source_files), mask_count, len(retval))
|
||||
return retval
|
||||
|
||||
def _generate_mapping(self, import_path: str, loader: Loader) -> dict[str, str]:
|
||||
""" Generate the mapping between the source data and the masks to be imported
|
||||
|
||||
Parameters
|
||||
----------
|
||||
import_path: str
|
||||
The path to the input images
|
||||
loader: :class:`~tools.mask.loader.Loader`
|
||||
The source file loader object
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict[str, str]
|
||||
Source filenames mapped to full path location of mask to be imported
|
||||
"""
|
||||
file_list = self._get_file_list(import_path)
|
||||
if loader.is_video:
|
||||
retval = self._map_video(file_list, loader.file_list)
|
||||
else:
|
||||
retval = self._map_images(file_list, loader.file_list)
|
||||
|
||||
return retval
|
||||
|
||||
def _store_mask(self, face: DetectedFace, mask: np.ndarray) -> None:
|
||||
""" Store the mask to the given DetectedFace object
|
||||
|
||||
Parameters
|
||||
----------
|
||||
face: :class:`~lib.align.detected_face.DetectedFace`
|
||||
The detected face object to store the mask to
|
||||
mask: :class:`numpy.ndarray`
|
||||
The mask to store
|
||||
"""
|
||||
aligned = AlignedFace(face.landmarks_xy,
|
||||
mask[..., None] if self._is_faces else mask,
|
||||
centering=self._centering,
|
||||
size=self._size,
|
||||
is_aligned=self._is_faces,
|
||||
dtype="float32")
|
||||
assert aligned.face is not None
|
||||
face.add_mask("custom",
|
||||
aligned.face / 255.,
|
||||
aligned.adjusted_matrix,
|
||||
aligned.interpolators[1],
|
||||
storage_size=self._size,
|
||||
storage_centering=self._centering)
|
||||
|
||||
def _store_mask_face(self, media: ExtractMedia, mask: np.ndarray) -> None:
|
||||
""" Store the mask when the input is aligned faceswap faces
|
||||
|
||||
Parameters
|
||||
----------
|
||||
media: :class:`~plugins.extract.pipeline.ExtractMedia`
|
||||
The extract media object containing the face(s) to import the mask for
|
||||
|
||||
mask: :class:`numpy.ndarray`
|
||||
The mask loaded from disk
|
||||
"""
|
||||
assert self._saver is not None
|
||||
assert len(media.detected_faces) == 1
|
||||
|
||||
logger.trace("Adding mask for '%s'", media.filename) # type:ignore[attr-defined]
|
||||
|
||||
face = media.detected_faces[0]
|
||||
self._store_mask(face, mask)
|
||||
|
||||
if self._alignments is not None:
|
||||
idx = media.frame_metadata["source_filename"]
|
||||
fname = media.frame_metadata["face_index"]
|
||||
logger.trace("Updating face %s in frame '%s'", idx, fname) # type:ignore[attr-defined]
|
||||
self._alignments.update_face(idx,
|
||||
fname,
|
||||
face.to_alignment())
|
||||
|
||||
logger.trace("Updating extracted face: '%s'", media.filename) # type:ignore[attr-defined]
|
||||
meta: PNGHeaderDict = {"alignments": face.to_png_meta(), "source": media.frame_metadata}
|
||||
self._saver.save(media.filename, encode_image(media.image, ".png", metadata=meta))
|
||||
|
||||
@classmethod
|
||||
def _resize_mask(cls, mask: np.ndarray, dims: tuple[int, int]) -> np.ndarray:
|
||||
""" Resize a mask to the given dimensions
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mask: :class:`numpy.ndarray`
|
||||
The mask to resize
|
||||
dims: tuple[int, int]
|
||||
The (height, width) target size
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`numpy.ndarray`
|
||||
The resized mask, or the original mask if no resizing required
|
||||
"""
|
||||
if mask.shape[:2] == dims:
|
||||
return mask
|
||||
logger.trace("Resizing mask from %s to %s", mask.shape, dims) # type:ignore[attr-defined]
|
||||
interp = cv2.INTER_AREA if mask.shape[0] > dims[0] else cv2.INTER_CUBIC
|
||||
|
||||
mask = cv2.resize(mask, tuple(reversed(dims)), interpolation=interp)
|
||||
return mask
|
||||
|
||||
def _store_mask_frame(self, media: ExtractMedia, mask: np.ndarray) -> None:
|
||||
""" Store the mask when the input is frames
|
||||
|
||||
Parameters
|
||||
----------
|
||||
media: :class:`~plugins.extract.pipeline.ExtractMedia`
|
||||
The extract media object containing the face(s) to import the mask for
|
||||
|
||||
mask: :class:`numpy.ndarray`
|
||||
The mask loaded from disk
|
||||
"""
|
||||
assert self._alignments is not None
|
||||
logger.trace("Adding %s mask(s) for '%s'", # type:ignore[attr-defined]
|
||||
len(media.detected_faces), media.filename)
|
||||
|
||||
mask = self._resize_mask(mask, media.image_size)
|
||||
|
||||
for idx, face in enumerate(media.detected_faces):
|
||||
self._store_mask(face, mask)
|
||||
self._alignments.update_face(os.path.basename(media.filename),
|
||||
idx,
|
||||
face.to_alignment())
|
||||
|
||||
def import_mask(self, media: ExtractMedia) -> None:
|
||||
""" Import the mask for the given Extract Media object
|
||||
|
||||
Parameters
|
||||
----------
|
||||
media: :class:`~plugins.extract.pipeline.ExtractMedia`
|
||||
The extract media object containing the face(s) to import the mask for
|
||||
"""
|
||||
mask_file = self._mapping.get(os.path.basename(media.filename))
|
||||
if not mask_file:
|
||||
self._counts["skip"] += 1
|
||||
logger.warning("No mask file found for: '%s'", os.path.basename(media.filename))
|
||||
return
|
||||
|
||||
mask = cv2.imread(mask_file, cv2.IMREAD_GRAYSCALE)
|
||||
|
||||
logger.trace("Loaded mask for frame '%s': %s", # type:ignore[attr-defined]
|
||||
os.path.basename(mask_file), mask.shape)
|
||||
|
||||
self._counts["update"] += len(media.detected_faces)
|
||||
|
||||
if self._is_faces:
|
||||
self._store_mask_face(media, mask)
|
||||
else:
|
||||
self._store_mask_frame(media, mask)
|
||||
515
tools/mask/mask_output.py
Normal file
515
tools/mask/mask_output.py
Normal file
|
|
@ -0,0 +1,515 @@
|
|||
#!/usr/bin/env python3
|
||||
""" Output processing for faceswap's mask tool """
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import typing as T
|
||||
from argparse import Namespace
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from tqdm import tqdm
|
||||
|
||||
from lib.align import AlignedFace
|
||||
from lib.align.alignments import AlignmentDict
|
||||
|
||||
from lib.image import ImagesSaver, read_image_meta_batch
|
||||
from lib.utils import get_folder
|
||||
from scripts.fsmedia import Alignments as ExtractAlignments
|
||||
|
||||
if T.TYPE_CHECKING:
|
||||
from lib.align import Alignments, DetectedFace
|
||||
from lib.align.aligned_face import CenteringType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Output:
|
||||
""" Handles outputting of masks for preview/editting to disk
|
||||
|
||||
Parameters
|
||||
----------
|
||||
arguments: :class:`argparse.Namespace`
|
||||
The command line arguments that the mask tool was called with
|
||||
alignments: :class:~`lib.align.alignments.Alignments` | None
|
||||
The alignments file object (or ``None`` if not provided and input is faces)
|
||||
file_list: list[str]
|
||||
Full file list for the loader. Used for extracting alignments from faces
|
||||
"""
|
||||
def __init__(self, arguments: Namespace,
|
||||
alignments: Alignments | None,
|
||||
file_list: list[str]) -> None:
|
||||
logger.debug("Initializing %s (arguments: %s, alignments: %s, file_list: %s)",
|
||||
self.__class__.__name__, arguments, alignments, len(file_list))
|
||||
|
||||
self._blur_kernel: int = arguments.blur_kernel
|
||||
self._threshold: int = arguments.threshold
|
||||
self._type: T.Literal["combined", "masked", "mask"] = arguments.output_type
|
||||
self._full_frame: bool = arguments.full_frame
|
||||
self._mask_type = arguments.masker
|
||||
|
||||
self._input_is_faces = arguments.input_type == "faces"
|
||||
self._saver = self._set_saver(arguments.output, arguments.processing)
|
||||
self._alignments = self._get_alignments(alignments, file_list)
|
||||
|
||||
self._full_frame_cache: dict[str, list[tuple[int, DetectedFace]]] = {}
|
||||
|
||||
logger.debug("Initialized %s", self.__class__.__name__)
|
||||
|
||||
@property
|
||||
def should_save(self) -> bool:
|
||||
"""bool: ``True`` if mask images should be output otherwise ``False`` """
|
||||
return self._saver is not None
|
||||
|
||||
def _get_subfolder(self, output: str) -> str:
|
||||
""" Obtain a subfolder within the output folder to save the output based on selected
|
||||
output options.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
output: str
|
||||
Full path to the root output folder
|
||||
|
||||
Returns
|
||||
-------
|
||||
str:
|
||||
The full path to where masks should be saved
|
||||
"""
|
||||
out_type = "frame" if self._full_frame else "face"
|
||||
retval = os.path.join(output,
|
||||
f"{self._mask_type}_{out_type}_{self._type}")
|
||||
logger.info("Saving masks to '%s'", retval)
|
||||
return retval
|
||||
|
||||
def _set_saver(self, output: str | None, processing: str) -> ImagesSaver | None:
|
||||
""" set the saver in a background thread
|
||||
|
||||
Parameters
|
||||
----------
|
||||
output: str
|
||||
Full path to the root output folder if provided
|
||||
processing: str
|
||||
The processing that has been selected
|
||||
|
||||
Returns
|
||||
-------
|
||||
``None`` or :class:`lib.image.ImagesSaver`:
|
||||
If output is requested, returns a :class:`lib.image.ImagesSaver` otherwise
|
||||
returns ``None``
|
||||
"""
|
||||
if output is None or not output:
|
||||
if processing == "output":
|
||||
logger.error("Processing set as 'output' but no output folder provided.")
|
||||
sys.exit(0)
|
||||
logger.debug("No output provided. Not creating saver")
|
||||
return None
|
||||
output_dir = get_folder(self._get_subfolder(output), make_folder=True)
|
||||
retval = ImagesSaver(output_dir)
|
||||
logger.debug(retval)
|
||||
return retval
|
||||
|
||||
def _get_alignments(self,
|
||||
alignments: Alignments | None,
|
||||
file_list: list[str]) -> Alignments | None:
|
||||
""" Obtain the alignments file. If input is faces and full frame output is requested then
|
||||
the file needs to be generated from the input faces, if not provided
|
||||
|
||||
Parameters
|
||||
----------
|
||||
alignments: :class:~`lib.align.alignments.Alignments` | None
|
||||
The alignments file object (or ``None`` if not provided and input is faces)
|
||||
file_list: list[str]
|
||||
Full paths to ihe mask tool input files
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:~`lib.align.alignments.Alignments` | None
|
||||
The alignments file if provided and/or is required otherwise ``None``
|
||||
"""
|
||||
if alignments is not None or not self._full_frame:
|
||||
return alignments
|
||||
logger.debug("Generating alignments from faces")
|
||||
|
||||
data = T.cast(dict[str, AlignmentDict], {})
|
||||
for _, meta in tqdm(read_image_meta_batch(file_list),
|
||||
desc="Reading alignments from faces",
|
||||
total=len(file_list),
|
||||
leave=False):
|
||||
fname = meta["itxt"]["source"]["source_filename"]
|
||||
aln = meta["itxt"]["alignments"]
|
||||
data.setdefault(fname, {}).setdefault("faces", # type:ignore[typeddict-item]
|
||||
[]).append(aln)
|
||||
|
||||
dummy_args = Namespace(alignments_path="/dummy/alignments.fsa")
|
||||
retval = ExtractAlignments(dummy_args, is_extract=True)
|
||||
retval.update_from_dict(data)
|
||||
return retval
|
||||
|
||||
def _get_background_frame(self, detected_faces: list[DetectedFace], frame_dims: tuple[int, int]
|
||||
) -> np.ndarray:
|
||||
""" Obtain the background image when final output is in full frame format. There will only
|
||||
ever be one background, even when there are multiple faces
|
||||
|
||||
The output image will depend on the requested output type and whether the input is faces
|
||||
or frames
|
||||
|
||||
Parameters
|
||||
----------
|
||||
detected_faces: list[:class:`~lib.align.detected_face.DetectedFace`]
|
||||
Detected face objects for the output image
|
||||
frame_dims: tuple[int, int]
|
||||
The size of the original frame
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`numpy.ndarray`
|
||||
The full frame background image for applying masks to
|
||||
"""
|
||||
if self._type == "mask":
|
||||
return np.zeros(frame_dims, dtype="uint8")
|
||||
|
||||
if not self._input_is_faces: # Frame is in the detected faces object
|
||||
assert detected_faces[0].image is not None
|
||||
return np.ascontiguousarray(detected_faces[0].image)
|
||||
|
||||
# Outputting to frames, but input is faces. Apply the face patches to an empty canvas
|
||||
retval = np.zeros((*frame_dims, 3), dtype="uint8")
|
||||
for detected_face in detected_faces:
|
||||
assert detected_face.image is not None
|
||||
face = AlignedFace(detected_face.landmarks_xy,
|
||||
image=detected_face.image,
|
||||
centering="head",
|
||||
size=detected_face.image.shape[0],
|
||||
is_aligned=True)
|
||||
border = cv2.BORDER_TRANSPARENT if len(detected_faces) > 1 else cv2.BORDER_CONSTANT
|
||||
assert face.face is not None
|
||||
cv2.warpAffine(face.face,
|
||||
face.adjusted_matrix,
|
||||
tuple(reversed(frame_dims)),
|
||||
retval,
|
||||
flags=cv2.WARP_INVERSE_MAP | face.interpolators[1],
|
||||
borderMode=border)
|
||||
return retval
|
||||
|
||||
def _get_background_face(self,
|
||||
detected_face: DetectedFace,
|
||||
mask_centering: CenteringType,
|
||||
mask_size: int) -> np.ndarray:
|
||||
""" Obtain the background images when the output is faces
|
||||
|
||||
The output image will depend on the requested output type and whether the input is faces
|
||||
or frames
|
||||
|
||||
Parameters
|
||||
----------
|
||||
detected_face: :class:`~lib.align.detected_face.DetectedFace`
|
||||
Detected face object for the output image
|
||||
mask_centering: Literal["face", "head", "legacy"]
|
||||
The centering of the stored mask
|
||||
mask_size: int
|
||||
The pixel size of the stored mask
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[]:class:`numpy.ndarray`]
|
||||
The face background image for applying masks to for each detected face object
|
||||
"""
|
||||
if self._type == "mask":
|
||||
return np.zeros((mask_size, mask_size), dtype="uint8")
|
||||
|
||||
assert detected_face.image is not None
|
||||
|
||||
if self._input_is_faces:
|
||||
retval = AlignedFace(detected_face.landmarks_xy,
|
||||
image=detected_face.image,
|
||||
centering=mask_centering,
|
||||
size=mask_size,
|
||||
is_aligned=True).face
|
||||
else:
|
||||
centering: CenteringType = ("legacy" if self._alignments is not None and
|
||||
self._alignments.version == 1.0
|
||||
else mask_centering)
|
||||
detected_face.load_aligned(detected_face.image,
|
||||
size=mask_size,
|
||||
centering=centering,
|
||||
force=True)
|
||||
retval = detected_face.aligned.face
|
||||
|
||||
assert retval is not None
|
||||
return retval
|
||||
|
||||
def _get_background(self,
|
||||
detected_faces: list[DetectedFace],
|
||||
frame_dims: tuple[int, int],
|
||||
mask_centering: CenteringType,
|
||||
mask_size: int) -> np.ndarray:
|
||||
""" Obtain the background image that the final outut will be placed on
|
||||
|
||||
Parameters
|
||||
----------
|
||||
detected_faces: list[:class:`~lib.align.detected_face.DetectedFace`]
|
||||
Detected face objects for the output image
|
||||
frame_dims: tuple[int, int]
|
||||
The size of the original frame
|
||||
mask_centering: Literal["face", "head", "legacy"]
|
||||
The centering of the stored mask
|
||||
mask_size: int
|
||||
The pixel size of the stored mask
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`numpy.ndarray`
|
||||
The background image for the mask output
|
||||
"""
|
||||
if self._full_frame:
|
||||
retval = self._get_background_frame(detected_faces, frame_dims)
|
||||
else:
|
||||
assert len(detected_faces) == 1 # If outputting faces, we should only receive 1 face
|
||||
retval = self._get_background_face(detected_faces[0], mask_centering, mask_size)
|
||||
|
||||
logger.trace("Background image (size: %s, dtype: %s)", # type:ignore[attr-defined]
|
||||
retval.shape, retval.dtype)
|
||||
return retval
|
||||
|
||||
def _get_mask(self,
|
||||
detected_faces: list[DetectedFace],
|
||||
mask_type: str,
|
||||
mask_dims: tuple[int, int]) -> np.ndarray:
|
||||
""" Generate the mask to be applied to the final output frame
|
||||
|
||||
Parameters
|
||||
----------
|
||||
detected_faces: list[:class:`~lib.align.detected_face.DetectedFace`]
|
||||
Detected face objects to generate the masks from
|
||||
mask_type: str
|
||||
The mask-type to use
|
||||
mask_dims : tuple[int, int]
|
||||
The size of the mask to output
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`numpy.ndarray`
|
||||
The final mask to apply to the output image
|
||||
"""
|
||||
retval = np.zeros(mask_dims, dtype="uint8")
|
||||
for face in detected_faces:
|
||||
mask_object = face.mask[mask_type]
|
||||
mask_object.set_blur_and_threshold(blur_kernel=self._blur_kernel,
|
||||
threshold=self._threshold)
|
||||
if self._full_frame:
|
||||
mask = mask_object.get_full_frame_mask(*reversed(mask_dims))
|
||||
else:
|
||||
mask = mask_object.mask[..., 0]
|
||||
np.maximum(retval, mask, out=retval)
|
||||
logger.trace("Final mask (shape: %s, dtype: %s)", # type:ignore[attr-defined]
|
||||
retval.shape, retval.dtype)
|
||||
return retval
|
||||
|
||||
def _build_output_image(self, background: np.ndarray, mask: np.ndarray) -> np.ndarray:
|
||||
""" Collate the mask and images for the final output image, depending on selected output
|
||||
type
|
||||
|
||||
Parameters
|
||||
----------
|
||||
background: :class:`numpy.ndarray`
|
||||
The image that the mask will be applied to
|
||||
mask: :class:`numpy.ndarray`
|
||||
The mask to output
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`numpy.ndarray`
|
||||
The final output image
|
||||
"""
|
||||
if self._type == "mask":
|
||||
return mask
|
||||
|
||||
mask = mask[..., None]
|
||||
if self._type == "masked":
|
||||
return np.concatenate([background, mask], axis=-1)
|
||||
|
||||
height, width = background.shape[:2]
|
||||
masked = (background.astype("float32") * mask.astype("float32") / 255.).astype("uint8")
|
||||
mask = np.tile(mask, 3)
|
||||
for img in (background, masked, mask):
|
||||
cv2.rectangle(img, (0, 0), (width - 1, height - 1), (255, 255, 255), 1)
|
||||
axis = 0 if background.shape[0] < background.shape[1] else 1
|
||||
retval = np.concatenate((background, masked, mask), axis=axis)
|
||||
|
||||
return retval
|
||||
|
||||
def _create_image(self,
|
||||
detected_faces: list[DetectedFace],
|
||||
mask_type: str,
|
||||
frame_dims: tuple[int, int] | None) -> np.ndarray:
|
||||
""" Create a mask preview image for saving out to disk
|
||||
|
||||
Parameters
|
||||
----------
|
||||
detected_faces: list[:class:`~lib.align.detected_face.DetectedFace`]
|
||||
Detected face objects for the output image
|
||||
mask_type: str
|
||||
The mask_type to process
|
||||
frame_dims: tuple[int, int] | None
|
||||
The size of the original frame, if input is faces otherwise ``None``
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`numpy.ndarray`:
|
||||
A preview image depending on the output type in one of the following forms:
|
||||
- Containing 3 sub images: The original face, the masked face and the mask
|
||||
- The mask only
|
||||
- The masked face
|
||||
"""
|
||||
assert detected_faces[0].image is not None
|
||||
dims = T.cast(tuple[int, int],
|
||||
frame_dims if self._input_is_faces else detected_faces[0].image.shape[:2])
|
||||
assert dims is not None and len(dims) == 2
|
||||
|
||||
mask_centering = detected_faces[0].mask[mask_type].stored_centering
|
||||
mask_size = detected_faces[0].mask[mask_type].stored_size
|
||||
|
||||
background = self._get_background(detected_faces, dims, mask_centering, mask_size)
|
||||
mask = self._get_mask(detected_faces,
|
||||
mask_type,
|
||||
dims if self._full_frame else (mask_size, mask_size))
|
||||
retval = self._build_output_image(background, mask)
|
||||
|
||||
logger.trace("Output image (shape: %s, dtype: %s)", # type:ignore[attr-defined]
|
||||
retval.shape, retval.dtype)
|
||||
return retval
|
||||
|
||||
def _handle_cache(self,
|
||||
frame: str,
|
||||
idx: int,
|
||||
detected_face: DetectedFace) -> list[tuple[int, DetectedFace]]:
|
||||
""" For full frame output, cache any faces until all detected faces have been seen. For
|
||||
face output, just return the detected_face object inside a list
|
||||
|
||||
Parameters
|
||||
----------
|
||||
frame: str
|
||||
The frame name in the alignments file
|
||||
idx: int
|
||||
The index of the face for this frame in the alignments file
|
||||
detected_face: :class:`~lib.align.detected_face.DetectedFace`
|
||||
A detected_face object for a face
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[tuple[int, :class:`~lib.align.detected_face.DetectedFace`]]
|
||||
Face index and detected face objects to be processed for this output, if any
|
||||
"""
|
||||
if not self._full_frame:
|
||||
return [(idx, detected_face)]
|
||||
|
||||
assert self._alignments is not None
|
||||
faces_in_frame = self._alignments.count_faces_in_frame(frame)
|
||||
if faces_in_frame == 1:
|
||||
return [(idx, detected_face)]
|
||||
|
||||
self._full_frame_cache.setdefault(frame, []).append((idx, detected_face))
|
||||
|
||||
if len(self._full_frame_cache[frame]) != faces_in_frame:
|
||||
logger.trace("Caching face for frame '%s'", frame) # type:ignore[attr-defined]
|
||||
return []
|
||||
|
||||
retval = self._full_frame_cache.pop(frame)
|
||||
logger.trace("Processing '%s' from cache: %s", frame, retval) # type:ignore[attr-defined]
|
||||
return retval
|
||||
|
||||
def _get_mask_types(self,
|
||||
frame: str,
|
||||
detected_faces: list[tuple[int, DetectedFace]]) -> list[str]:
|
||||
""" Get the mask type names for the select mask type. Remove any detected faces where
|
||||
the selected mask does not exist
|
||||
|
||||
Parameters
|
||||
----------
|
||||
frame: str
|
||||
The frame name in the alignments file
|
||||
idx: int
|
||||
The index of the face for this frame in the alignments file
|
||||
detected_face: list[tuple[int, :class:`~lib.align.detected_face.DetectedFace`]
|
||||
The face index and detected_face object for output
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[str]
|
||||
List of mask type names to be processed
|
||||
"""
|
||||
if self._mask_type == "bisenet-fp":
|
||||
mask_types = [f"{self._mask_type}_{area}" for area in ("face", "head")]
|
||||
else:
|
||||
mask_types = [self._mask_type]
|
||||
|
||||
final_masks = set()
|
||||
for idx in reversed(range(len(detected_faces))):
|
||||
face_idx, detected_face = detected_faces[idx]
|
||||
if detected_face.mask is None or not any(mask in detected_face.mask
|
||||
for mask in mask_types):
|
||||
logger.warning("Mask type '%s' does not exist for frame '%s' index %s. Skipping",
|
||||
self._mask_type, frame, face_idx)
|
||||
del detected_faces[idx]
|
||||
continue
|
||||
final_masks.update([m for m in detected_face.mask if m in mask_types])
|
||||
|
||||
retval = list(final_masks)
|
||||
logger.trace("Handling mask types: %s", retval) # type:ignore[attr-defined]
|
||||
return retval
|
||||
|
||||
def save(self,
|
||||
frame: str,
|
||||
idx: int,
|
||||
detected_face: DetectedFace,
|
||||
frame_dims: tuple[int, int] | None = None) -> None:
|
||||
""" Build the mask preview image and save
|
||||
|
||||
Parameters
|
||||
----------
|
||||
frame: str
|
||||
The frame name in the alignments file
|
||||
idx: int
|
||||
The index of the face for this frame in the alignments file
|
||||
detected_face: :class:`~lib.align.detected_face.DetectedFace`
|
||||
A detected_face object for a face
|
||||
frame_dims: tuple[int, int] | None, optional
|
||||
The size of the original frame, if input is faces otherwise ``None``. Default: ``None``
|
||||
"""
|
||||
assert self._saver is not None
|
||||
|
||||
faces = self._handle_cache(frame, idx, detected_face)
|
||||
if not faces:
|
||||
return
|
||||
|
||||
mask_types = self._get_mask_types(frame, faces)
|
||||
if not faces or not mask_types:
|
||||
logger.debug("No valid faces/masks to process for '%s'", frame)
|
||||
return
|
||||
|
||||
for mask_type in mask_types:
|
||||
detected_faces = [f[1] for f in faces if mask_type in f[1].mask]
|
||||
if not detected_face:
|
||||
logger.warning("No '%s' masks to output for '%s'", mask_type, frame)
|
||||
continue
|
||||
if len(detected_faces) != len(faces):
|
||||
logger.warning("Some '%s' masks are missing for '%s'", mask_type, frame)
|
||||
|
||||
image = self._create_image(detected_faces, mask_type, frame_dims)
|
||||
filename = os.path.splitext(frame)[0]
|
||||
if len(mask_types) > 1:
|
||||
filename += f"_{mask_type}"
|
||||
if not self._full_frame:
|
||||
filename += f"_{idx}"
|
||||
filename = os.path.join(self._saver.location, f"{filename}.png")
|
||||
logger.trace("filename: '%s', image_shape: %s", filename, image.shape) # type: ignore
|
||||
self._saver.save(filename, image)
|
||||
|
||||
def close(self) -> None:
|
||||
""" Shut down the image saver if it is open """
|
||||
if self._saver is None:
|
||||
return
|
||||
logger.debug("Shutting down saver")
|
||||
self._saver.close()
|
||||
Loading…
Reference in New Issue
Block a user