Add Misalignment Detection

- lib.align.AlignedFace - Add average_distance property (distance from mean_face)
  - tools.manual - Add misaligned Faces filter
  - tools.sort - Add sort by distance (misaligned sort)Add "Misaligned Faces" filter to manual tool
This commit is contained in:
torzdf 2021-06-08 19:30:28 +01:00
parent e024189a5e
commit eb96da0346
14 changed files with 378 additions and 179 deletions

View File

@ -261,6 +261,18 @@ class AlignedFace():
self._cache["landmarks"][0] = lms
return self._cache["landmarks"][0]
@property
def normalized_landmarks(self):
""" :class:`numpy.ndarray`: The 68 point facial landmarks normalized to 0.0 - 1.0 as
aligned by Umeyama. """
with self._cache["landmarks_normalized"][1]:
if self._cache["landmarks_normalized"][0] is None:
lms = np.expand_dims(self._frame_landmarks, axis=1)
lms = cv2.transform(lms, self._matrices["legacy"], lms.shape).squeeze()
logger.trace("normalized landmarks: %s", lms)
self._cache["landmarks_normalized"][0] = lms
return self._cache["landmarks_normalized"][0]
@property
def interpolators(self):
""" tuple: (`interpolator` and `reverse interpolator`) for the :attr:`adjusted matrix`. """
@ -271,6 +283,18 @@ class AlignedFace():
self._cache["interpolators"][0] = interpolators
return self._cache["interpolators"][0]
@property
def average_distance(self):
""" float: The average distance of the core landmarks (18-67) from the mean face that was
used for aligning the image. """
with self._cache["average_distance"][1]:
if self._cache["average_distance"][0] is None:
# pylint:disable=unsubscriptable-object
average_distance = np.mean(np.abs(self.normalized_landmarks[17:] - _MEAN_FACE))
logger.trace("average_distance: %s", average_distance)
self._cache["average_distance"][0] = average_distance
return self._cache["average_distance"][0]
@classmethod
def _set_cache(cls):
""" Set the cache items.
@ -286,6 +310,8 @@ class AlignedFace():
return dict(pose=[None, Lock()],
original_roi=[None, Lock()],
landmarks=[None, Lock()],
landmarks_normalized=[None, Lock()],
average_distance=[None, Lock()],
adjusted_matrix=[None, Lock()],
interpolators=[None, Lock()],
head_size=[dict(), Lock()],

View File

@ -5,7 +5,7 @@
msgid ""
msgstr ""
"Project-Id-Version: faceswap.spanish\n"
"POT-Creation-Date: 2021-03-22 18:31+0000\n"
"POT-Creation-Date: 2021-06-08 19:24+0100\n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
@ -13,9 +13,9 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.4.2\n"
"X-Generator: Poedit 2.4.3\n"
#: tools/manual/cli.py:13
#: tools/manual\cli.py:13
msgid ""
"This command lets you perform various actions on frames, faces and "
"alignments files using visual tools."
@ -23,7 +23,7 @@ msgstr ""
"Este comando le permite realizar varias acciones en los archivos de "
"fotogramas, caras y alineaciones utilizando herramientas visuales."
#: tools/manual/cli.py:23
#: tools/manual\cli.py:23
msgid ""
"A tool to perform various actions on frames, faces and alignments files "
"using visual tools"
@ -31,18 +31,18 @@ msgstr ""
"Una herramienta que permite realizar diversas acciones en archivos de "
"fotogramas, caras y alineaciones mediante herramientas visuales"
#: tools/manual/cli.py:35 tools/manual/cli.py:43
#: tools/manual\cli.py:35 tools/manual\cli.py:43
msgid "data"
msgstr "datos"
#: tools/manual/cli.py:37
#: tools/manual\cli.py:37
msgid ""
"Path to the alignments file for the input, if not at the default location"
msgstr ""
"Ruta del archivo de alineaciones para la entrada, si no está en la ubicación "
"por defecto"
#: tools/manual/cli.py:44
#: tools/manual\cli.py:44
msgid ""
"Video file or directory containing source frames that faces were extracted "
"from."
@ -50,11 +50,11 @@ msgstr ""
"Archivo o directorio de vídeo que contiene los fotogramas de origen de los "
"que se extrajeron las caras."
#: tools/manual/cli.py:51 tools/manual/cli.py:59
#: tools/manual\cli.py:51 tools/manual\cli.py:59
msgid "options"
msgstr "opciones"
#: tools/manual/cli.py:52
#: tools/manual\cli.py:52
msgid ""
"Force regeneration of the low resolution jpg thumbnails in the alignments "
"file."
@ -62,7 +62,7 @@ msgstr ""
"Forzar la regeneración de las miniaturas jpg de baja resolución en el "
"archivo de alineaciones."
#: tools/manual/cli.py:60
#: tools/manual\cli.py:60
msgid ""
"The process attempts to speed up generation of thumbnails by extracting from "
"the video in parallel threads. For some videos, this causes the caching "
@ -74,26 +74,26 @@ msgstr ""
"extracción se cuelgue. Si esto sucede, entonces configure esta opción para "
"generar las miniaturas en un solo hilo más lento, pero más estable."
#: tools/manual/faceviewer\frame.py:163
#: tools/manual\faceviewer\frame.py:163
msgid "Display the landmarks mesh"
msgstr "Mostrar la malla de puntos de referencia"
#: tools/manual/faceviewer\frame.py:164
#: tools/manual\faceviewer\frame.py:164
msgid "Display the mask"
msgstr "Mostrar la máscara"
#: tools/manual/frameviewer\editor\_base.py:627
#: tools/manual/frameviewer\editor\landmarks.py:44
#: tools/manual/frameviewer\editor\mask.py:75
#: tools/manual\frameviewer\editor\_base.py:628
#: tools/manual\frameviewer\editor\landmarks.py:44
#: tools/manual\frameviewer\editor\mask.py:75
msgid "Magnify/Demagnify the View"
msgstr "Ampliar/Reducir la vista"
#: tools/manual/frameviewer\editor\bounding_box.py:33
#: tools/manual/frameviewer\editor\extract_box.py:32
#: tools/manual\frameviewer\editor\bounding_box.py:33
#: tools/manual\frameviewer\editor\extract_box.py:32
msgid "Delete Face"
msgstr "Borrar cara"
#: tools/manual/frameviewer\editor\bounding_box.py:36
#: tools/manual\frameviewer\editor\bounding_box.py:36
msgid ""
"Bounding Box Editor\n"
"Edit the bounding box being fed into the aligner to recalculate the "
@ -115,7 +115,7 @@ msgstr ""
" - Haga clic con el botón derecho del ratón en un cuadro delimitador para "
"eliminar una cara."
#: tools/manual/frameviewer\editor\bounding_box.py:70
#: tools/manual\frameviewer\editor\bounding_box.py:70
msgid ""
"Aligner to use. FAN will obtain better alignments, but cv2-dnn can be useful "
"if FAN cannot get decent alignments and you want to set a base to edit from."
@ -124,7 +124,7 @@ msgstr ""
"ser útil si FAN no puede obtener alineaciones decentes y quiere tener una "
"base inicial que luego se vaya a editar."
#: tools/manual/frameviewer\editor\bounding_box.py:83
#: tools/manual\frameviewer\editor\bounding_box.py:83
msgid ""
"Normalization method to use for feeding faces to the aligner. This can help "
"the aligner better align faces with difficult lighting conditions. Different "
@ -147,7 +147,7 @@ msgstr ""
"\thist: Iguala los histogramas en los canales RGB.\n"
"\tmean: Normaliza los colores de la cara a la media."
#: tools/manual/frameviewer\editor\extract_box.py:35
#: tools/manual\frameviewer\editor\extract_box.py:35
msgid ""
"Extract Box Editor\n"
"Move the extract box that has been generated by the aligner. Click and "
@ -166,7 +166,7 @@ msgstr ""
"referencia.\n"
" - Fuera de las esquinas para girar los puntos de referencia."
#: tools/manual/frameviewer\editor\landmarks.py:27
#: tools/manual\frameviewer\editor\landmarks.py:27
msgid ""
"Landmark Point Editor\n"
"Edit the individual landmark points.\n"
@ -180,7 +180,7 @@ msgstr ""
" - Haga clic y arrastre los puntos individuales para reubicarlos.\n"
" - Dibuje un cuadro para seleccionar varios puntos para reubicarlos."
#: tools/manual/frameviewer\editor\mask.py:33
#: tools/manual\frameviewer\editor\mask.py:33
msgid ""
"Mask Editor\n"
"Edit the mask.\n"
@ -197,90 +197,98 @@ msgstr ""
"Cualquier cambio en los puntos de referencia después de editar la máscara "
"anulará sus ediciones manuales."
#: tools/manual/frameviewer\editor\mask.py:77
#: tools/manual\frameviewer\editor\mask.py:77
msgid "Draw Tool"
msgstr "Herramienta de dibujo"
#: tools/manual/frameviewer\editor\mask.py:78
#: tools/manual\frameviewer\editor\mask.py:78
msgid "Erase Tool"
msgstr "Herramienta de borrado"
#: tools/manual/frameviewer\editor\mask.py:97
#: tools/manual\frameviewer\editor\mask.py:97
msgid "Select which mask to edit"
msgstr "Seleccionar máscara a editar"
#: tools/manual/frameviewer\editor\mask.py:104
#: tools/manual\frameviewer\editor\mask.py:104
msgid "Set the brush size. ([ - decrease, ] - increase)"
msgstr "Seleccionar el tamaño del pincel ([ - disminuir, ] - aumentar)"
#: tools/manual/frameviewer\editor\mask.py:111
#: tools/manual\frameviewer\editor\mask.py:111
msgid "Select the brush cursor color."
msgstr "Seleccionar el color del pincel."
#: tools/manual/frameviewer\frame.py:77
#: tools/manual\frameviewer\frame.py:78
msgid "Play/Pause (SPACE)"
msgstr "Reproducir/Pausa (BARRA DE ESPACIO)"
#: tools/manual/frameviewer\frame.py:78
#: tools/manual\frameviewer\frame.py:79
msgid "Go to First Frame (HOME)"
msgstr "Ir al primer cuadro (INICIO)"
#: tools/manual/frameviewer\frame.py:79
#: tools/manual\frameviewer\frame.py:80
msgid "Go to Previous Frame (Z)"
msgstr "Ir al cuadro anterior (Z)"
#: tools/manual/frameviewer\frame.py:80
#: tools/manual\frameviewer\frame.py:81
msgid "Go to Next Frame (X)"
msgstr "Ir al siguiente cuadro (X)"
#: tools/manual/frameviewer\frame.py:81
#: tools/manual\frameviewer\frame.py:82
msgid "Go to Last Frame (END)"
msgstr "Ir al último cuadro (FIN)"
#: tools/manual/frameviewer\frame.py:82
#: tools/manual\frameviewer\frame.py:83
msgid "Extract the faces to a folder... (Ctrl+E)"
msgstr "Extraer las caras a una carpeta... (Ctrl+E)"
#: tools/manual/frameviewer\frame.py:83
#: tools/manual\frameviewer\frame.py:84
msgid "Save the Alignments file (Ctrl+S)"
msgstr "Guardar el fichero de alineamientos (Ctrl+S)"
#: tools/manual/frameviewer\frame.py:84
#: tools/manual\frameviewer\frame.py:85
msgid "Filter Frames to only those Containing the Selected Item (F)"
msgstr "Mostrar cuadros que contenga únicamente el elemento seleccionado (F)"
#: tools/manual/frameviewer\frame.py:318
#: tools/manual\frameviewer\frame.py:86
msgid ""
"Set the distance from an 'average face' to be considered misaligned. Higher "
"distances are more restrictive"
msgstr ""
"Establezca la distancia desde una 'cara promedio' para que se considere "
"desalineada. Las distancias más altas son más restrictivas"
#: tools/manual\frameviewer\frame.py:391
msgid "View alignments"
msgstr "Ver alineamientos"
#: tools/manual/frameviewer\frame.py:319
#: tools/manual\frameviewer\frame.py:392
msgid "Bounding box editor"
msgstr "Editor de cuadro delimitador"
#: tools/manual/frameviewer\frame.py:320
#: tools/manual\frameviewer\frame.py:393
msgid "Location editor"
msgstr "Editor de ubicación"
#: tools/manual/frameviewer\frame.py:321
#: tools/manual\frameviewer\frame.py:394
msgid "Mask editor"
msgstr "Editor de máscara"
#: tools/manual/frameviewer\frame.py:322
#: tools/manual\frameviewer\frame.py:395
msgid "Landmark point editor"
msgstr "Editor de puntos de referencia"
#: tools/manual/frameviewer\frame.py:397
#: tools/manual\frameviewer\frame.py:470
msgid "Next"
msgstr "Siguiente"
#: tools/manual/frameviewer\frame.py:397
#: tools/manual\frameviewer\frame.py:470
msgid "Previous"
msgstr "Anterior"
#: tools/manual/frameviewer\frame.py:408
#: tools/manual\frameviewer\frame.py:481
msgid "Revert to saved Alignments ({})"
msgstr "Volver a los alineamientos guardados ({})"
#: tools/manual/frameviewer\frame.py:414
#: tools/manual\frameviewer\frame.py:487
msgid "Copy {} Alignments ({})"
msgstr "Copiar los alineamientos del cuadro {} ({})"

View File

@ -5,8 +5,8 @@
msgid ""
msgstr ""
"Project-Id-Version: faceswap.spanish\n"
"POT-Creation-Date: 2021-03-23 15:33+0000\n"
"PO-Revision-Date: 2021-03-23 15:36+0000\n"
"POT-Creation-Date: 2021-06-07 12:34+0100\n"
"PO-Revision-Date: 2021-06-07 12:38+0100\n"
"Last-Translator: \n"
"Language-Team: tokafondo\n"
"Language: es_ES\n"
@ -14,7 +14,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: pygettext.py 1.5\n"
"X-Generator: Poedit 2.4.2\n"
"X-Generator: Poedit 2.4.3\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: tools/sort/cli.py:14
@ -38,7 +38,7 @@ msgstr "Directorio de entrada de caras alineadas."
msgid "Output directory for sorted aligned faces."
msgstr "Directorio de salida para las caras alineadas ordenadas."
#: tools/sort/cli.py:50 tools/sort/cli.py:93
#: tools/sort/cli.py:50 tools/sort/cli.py:96
msgid "sort settings"
msgstr "ajustes de ordenación"
@ -46,6 +46,9 @@ msgstr "ajustes de ordenación"
msgid ""
"R|Sort by method. Choose how images are sorted. \n"
"L|'blur': Sort faces by blurriness.\n"
"L|'blur-fft': Sort faces by fft filtered blurriness.\n"
"L|'distance' Sort faces by the estimated distance of the alignments from an "
"'average' face. This can be useful for eliminating misaligned faces.\n"
"L|'face': Use VGG Face to sort by face similarity. This uses a pairwise "
"clustering algorithm to check the distances between 512 features on every "
"face in your set and order them appropriately.\n"
@ -75,6 +78,10 @@ msgid ""
msgstr ""
"R|Método de ordenación. Elige cómo se ordenan las imágenes. \n"
"L|'blur': Ordena las caras por desenfoque.\n"
"L|'blur-fft': Ordena las caras por fft filtrado desenfoque.\n"
"L|'distance' Ordene las caras por la distancia estimada de las alineaciones "
"desde una cara \"promedio\". Esto puede resultar útil para eliminar caras "
"desalineadas.\n"
"L|'face': Utiliza VGG Face para ordenar por similitud de caras. Esto utiliza "
"un algoritmo de agrupación por pares para comprobar las distancias entre 512 "
"características en cada cara en su conjunto y ordenarlos adecuadamente.\n"
@ -102,12 +109,12 @@ msgstr ""
"fuentes de menor resolución se ordenarán en último lugar.\n"
"Por defecto: face"
#: tools/sort/cli.py:82 tools/sort/cli.py:109 tools/sort/cli.py:121
#: tools/sort/cli.py:132
#: tools/sort/cli.py:85 tools/sort/cli.py:112 tools/sort/cli.py:124
#: tools/sort/cli.py:135
msgid "output"
msgstr "salida"
#: tools/sort/cli.py:83
#: tools/sort/cli.py:86
msgid ""
"Keeps the original files in the input directory. Be careful when using this "
"with rename grouping and no specified output directory as this would keep "
@ -118,7 +125,7 @@ msgstr ""
"de salida, ya que esto mantendría los archivos originales y renombrados en "
"el mismo directorio."
#: tools/sort/cli.py:95
#: tools/sort/cli.py:98
msgid ""
"Float value. Minimum threshold to use for grouping comparison with 'face-"
"cnn' and 'hist' methods. The lower the value the more discriminating the "
@ -139,7 +146,7 @@ msgstr ""
"podría resultar en la creación de muchos directorios. Por defecto: 'face-"
"cnn' = 7.2, 'hist' = 0.3"
#: tools/sort/cli.py:110
#: tools/sort/cli.py:113
msgid ""
"R|Default: rename.\n"
"L|'folders': files are sorted using the -s/--sort-by method, then they are "
@ -153,7 +160,7 @@ msgstr ""
"L|'rename': los archivos se ordenan utilizando el método -s/--sort-by y "
"luego se renombran."
#: tools/sort/cli.py:123
#: tools/sort/cli.py:126
msgid ""
"Group by method. When -fp/--final-processing by folders choose the how the "
"images are grouped after sorting. Default: hist"
@ -161,7 +168,7 @@ msgstr ""
"Método de agrupamiento. Elija la forma de agrupar las imágenes, en el caso "
"de hacerlo por carpetas, después de la clasificación. Por defecto: hist"
#: tools/sort/cli.py:134
#: tools/sort/cli.py:137
msgid ""
"Integer value. Number of folders that will be used to group by blur and face-"
"yaw. For blur folder 0 will be the least blurry, while the last folder will "
@ -182,11 +189,11 @@ msgstr ""
"uniformemente en el número de carpetas, las imágenes restantes se colocan en "
"la última carpeta. Valor por defecto: 5"
#: tools/sort/cli.py:145 tools/sort/cli.py:155
#: tools/sort/cli.py:148 tools/sort/cli.py:158
msgid "settings"
msgstr "ajustes"
#: tools/sort/cli.py:147
#: tools/sort/cli.py:150
msgid ""
"Logs file renaming changes if grouping by renaming, or it logs the file "
"copying/movement if grouping by folders. If no log file is specified with "
@ -198,7 +205,7 @@ msgstr ""
"se especifica ningún archivo de registro con '--log-file', se creará un "
"archivo 'sort_log.json' en el directorio de entrada."
#: tools/sort/cli.py:158
#: tools/sort/cli.py:161
msgid ""
"Specify a log file to use for saving the renaming or grouping information. "
"If specified extension isn't 'json' or 'yaml', then json will be used as the "

View File

@ -5,7 +5,7 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2021-03-22 18:31+0000\n"
"POT-Creation-Date: 2021-06-08 19:24+0100\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"
@ -15,58 +15,58 @@ msgstr ""
"Generated-By: pygettext.py 1.5\n"
#: ./tools/manual/cli.py:13
#: tools/manual\cli.py:13
msgid "This command lets you perform various actions on frames, faces and alignments files using visual tools."
msgstr ""
#: ./tools/manual/cli.py:23
#: tools/manual\cli.py:23
msgid "A tool to perform various actions on frames, faces and alignments files using visual tools"
msgstr ""
#: ./tools/manual/cli.py:35 ./tools/manual/cli.py:43
#: tools/manual\cli.py:35 tools/manual\cli.py:43
msgid "data"
msgstr ""
#: ./tools/manual/cli.py:37
#: tools/manual\cli.py:37
msgid "Path to the alignments file for the input, if not at the default location"
msgstr ""
#: ./tools/manual/cli.py:44
#: tools/manual\cli.py:44
msgid "Video file or directory containing source frames that faces were extracted from."
msgstr ""
#: ./tools/manual/cli.py:51 ./tools/manual/cli.py:59
#: tools/manual\cli.py:51 tools/manual\cli.py:59
msgid "options"
msgstr ""
#: ./tools/manual/cli.py:52
#: tools/manual\cli.py:52
msgid "Force regeneration of the low resolution jpg thumbnails in the alignments file."
msgstr ""
#: ./tools/manual/cli.py:60
#: tools/manual\cli.py:60
msgid "The process attempts to speed up generation of thumbnails by extracting from the video in parallel threads. For some videos, this causes the caching process to hang. If this happens, then set this option to generate the thumbnails in a slower, but more stable single thread."
msgstr ""
#: ./tools/manual/faceviewer\frame.py:163
#: tools/manual\faceviewer\frame.py:163
msgid "Display the landmarks mesh"
msgstr ""
#: ./tools/manual/faceviewer\frame.py:164
#: tools/manual\faceviewer\frame.py:164
msgid "Display the mask"
msgstr ""
#: ./tools/manual/frameviewer\editor\_base.py:627
#: ./tools/manual/frameviewer\editor\landmarks.py:44
#: ./tools/manual/frameviewer\editor\mask.py:75
#: tools/manual\frameviewer\editor\_base.py:628
#: tools/manual\frameviewer\editor\landmarks.py:44
#: tools/manual\frameviewer\editor\mask.py:75
msgid "Magnify/Demagnify the View"
msgstr ""
#: ./tools/manual/frameviewer\editor\bounding_box.py:33
#: ./tools/manual/frameviewer\editor\extract_box.py:32
#: tools/manual\frameviewer\editor\bounding_box.py:33
#: tools/manual\frameviewer\editor\extract_box.py:32
msgid "Delete Face"
msgstr ""
#: ./tools/manual/frameviewer\editor\bounding_box.py:36
#: tools/manual\frameviewer\editor\bounding_box.py:36
msgid ""
"Bounding Box Editor\n"
"Edit the bounding box being fed into the aligner to recalculate the landmarks.\n"
@ -77,11 +77,11 @@ msgid ""
" - Right click a bounding box to delete a face."
msgstr ""
#: ./tools/manual/frameviewer\editor\bounding_box.py:70
#: tools/manual\frameviewer\editor\bounding_box.py:70
msgid "Aligner to use. FAN will obtain better alignments, but cv2-dnn can be useful if FAN cannot get decent alignments and you want to set a base to edit from."
msgstr ""
#: ./tools/manual/frameviewer\editor\bounding_box.py:83
#: tools/manual\frameviewer\editor\bounding_box.py:83
msgid ""
"Normalization method to use for feeding faces to the aligner. This can help the aligner better align faces with difficult lighting conditions. Different methods will yield different results on different sets. NB: This does not impact the output face, just the input to the aligner.\n"
"\tnone: Don't perform normalization on the face.\n"
@ -90,7 +90,7 @@ msgid ""
"\tmean: Normalize the face colors to the mean."
msgstr ""
#: ./tools/manual/frameviewer\editor\extract_box.py:35
#: tools/manual\frameviewer\editor\extract_box.py:35
msgid ""
"Extract Box Editor\n"
"Move the extract box that has been generated by the aligner. Click and drag:\n"
@ -100,7 +100,7 @@ msgid ""
" - Outside of the corners to rotate the landmarks."
msgstr ""
#: ./tools/manual/frameviewer\editor\landmarks.py:27
#: tools/manual\frameviewer\editor\landmarks.py:27
msgid ""
"Landmark Point Editor\n"
"Edit the individual landmark points.\n"
@ -109,98 +109,102 @@ msgid ""
" - Draw a box to select multiple points to relocate."
msgstr ""
#: ./tools/manual/frameviewer\editor\mask.py:33
#: tools/manual\frameviewer\editor\mask.py:33
msgid ""
"Mask Editor\n"
"Edit the mask.\n"
" - NB: For Landmark based masks (e.g. components/extended) it is better to make sure the landmarks are correct rather than editing the mask directly. Any change to the landmarks after editing the mask will override your manual edits."
msgstr ""
#: ./tools/manual/frameviewer\editor\mask.py:77
#: tools/manual\frameviewer\editor\mask.py:77
msgid "Draw Tool"
msgstr ""
#: ./tools/manual/frameviewer\editor\mask.py:78
#: tools/manual\frameviewer\editor\mask.py:78
msgid "Erase Tool"
msgstr ""
#: ./tools/manual/frameviewer\editor\mask.py:97
#: tools/manual\frameviewer\editor\mask.py:97
msgid "Select which mask to edit"
msgstr ""
#: ./tools/manual/frameviewer\editor\mask.py:104
#: tools/manual\frameviewer\editor\mask.py:104
msgid "Set the brush size. ([ - decrease, ] - increase)"
msgstr ""
#: ./tools/manual/frameviewer\editor\mask.py:111
#: tools/manual\frameviewer\editor\mask.py:111
msgid "Select the brush cursor color."
msgstr ""
#: ./tools/manual/frameviewer\frame.py:77
#: tools/manual\frameviewer\frame.py:78
msgid "Play/Pause (SPACE)"
msgstr ""
#: ./tools/manual/frameviewer\frame.py:78
#: tools/manual\frameviewer\frame.py:79
msgid "Go to First Frame (HOME)"
msgstr ""
#: ./tools/manual/frameviewer\frame.py:79
#: tools/manual\frameviewer\frame.py:80
msgid "Go to Previous Frame (Z)"
msgstr ""
#: ./tools/manual/frameviewer\frame.py:80
#: tools/manual\frameviewer\frame.py:81
msgid "Go to Next Frame (X)"
msgstr ""
#: ./tools/manual/frameviewer\frame.py:81
#: tools/manual\frameviewer\frame.py:82
msgid "Go to Last Frame (END)"
msgstr ""
#: ./tools/manual/frameviewer\frame.py:82
#: tools/manual\frameviewer\frame.py:83
msgid "Extract the faces to a folder... (Ctrl+E)"
msgstr ""
#: ./tools/manual/frameviewer\frame.py:83
#: tools/manual\frameviewer\frame.py:84
msgid "Save the Alignments file (Ctrl+S)"
msgstr ""
#: ./tools/manual/frameviewer\frame.py:84
#: tools/manual\frameviewer\frame.py:85
msgid "Filter Frames to only those Containing the Selected Item (F)"
msgstr ""
#: ./tools/manual/frameviewer\frame.py:318
#: tools/manual\frameviewer\frame.py:86
msgid "Set the distance from an 'average face' to be considered misaligned. Higher distances are more restrictive"
msgstr ""
#: tools/manual\frameviewer\frame.py:391
msgid "View alignments"
msgstr ""
#: ./tools/manual/frameviewer\frame.py:319
#: tools/manual\frameviewer\frame.py:392
msgid "Bounding box editor"
msgstr ""
#: ./tools/manual/frameviewer\frame.py:320
#: tools/manual\frameviewer\frame.py:393
msgid "Location editor"
msgstr ""
#: ./tools/manual/frameviewer\frame.py:321
#: tools/manual\frameviewer\frame.py:394
msgid "Mask editor"
msgstr ""
#: ./tools/manual/frameviewer\frame.py:322
#: tools/manual\frameviewer\frame.py:395
msgid "Landmark point editor"
msgstr ""
#: ./tools/manual/frameviewer\frame.py:397
#: tools/manual\frameviewer\frame.py:470
msgid "Next"
msgstr ""
#: ./tools/manual/frameviewer\frame.py:397
#: tools/manual\frameviewer\frame.py:470
msgid "Previous"
msgstr ""
#: ./tools/manual/frameviewer\frame.py:408
#: tools/manual\frameviewer\frame.py:481
msgid "Revert to saved Alignments ({})"
msgstr ""
#: ./tools/manual/frameviewer\frame.py:414
#: tools/manual\frameviewer\frame.py:487
msgid "Copy {} Alignments ({})"
msgstr ""

View File

@ -5,7 +5,7 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2021-03-23 15:33+0000\n"
"POT-Creation-Date: 2021-06-07 12:34+0100\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"
@ -35,7 +35,7 @@ msgstr ""
msgid "Output directory for sorted aligned faces."
msgstr ""
#: tools/sort/cli.py:50 tools/sort/cli.py:93
#: tools/sort/cli.py:50 tools/sort/cli.py:96
msgid "sort settings"
msgstr ""
@ -43,6 +43,8 @@ msgstr ""
msgid ""
"R|Sort by method. Choose how images are sorted. \n"
"L|'blur': Sort faces by blurriness.\n"
"L|'blur-fft': Sort faces by fft filtered blurriness.\n"
"L|'distance' Sort faces by the estimated distance of the alignments from an 'average' face. This can be useful for eliminating misaligned faces.\n"
"L|'face': Use VGG Face to sort by face similarity. This uses a pairwise clustering algorithm to check the distances between 512 features on every face in your set and order them appropriately.\n"
"L|'face-cnn': Sort faces by their landmarks. You can adjust the threshold with the '-t' (--ref_threshold) option.\n"
"L|'face-cnn-dissim': Like 'face-cnn' but sorts by dissimilarity.\n"
@ -57,43 +59,43 @@ msgid ""
"Default: face"
msgstr ""
#: tools/sort/cli.py:82 tools/sort/cli.py:109 tools/sort/cli.py:121
#: tools/sort/cli.py:132
#: tools/sort/cli.py:85 tools/sort/cli.py:112 tools/sort/cli.py:124
#: tools/sort/cli.py:135
msgid "output"
msgstr ""
#: tools/sort/cli.py:83
#: tools/sort/cli.py:86
msgid "Keeps the original files in the input directory. Be careful when using this with rename grouping and no specified output directory as this would keep the original and renamed files in the same directory."
msgstr ""
#: tools/sort/cli.py:95
#: tools/sort/cli.py:98
msgid "Float value. Minimum threshold to use for grouping comparison with 'face-cnn' and 'hist' methods. The lower the value the more discriminating the grouping is. Leaving -1.0 will allow the program set the default value automatically. For face-cnn 7.2 should be enough, with 4 being very discriminating. For hist 0.3 should be enough, with 0.2 being very discriminating. Be careful setting a value that's too low in a directory with many images, as this could result in a lot of directories being created. Defaults: face-cnn 7.2, hist 0.3"
msgstr ""
#: tools/sort/cli.py:110
#: tools/sort/cli.py:113
msgid ""
"R|Default: rename.\n"
"L|'folders': files are sorted using the -s/--sort-by method, then they are organized into folders using the -g/--group-by grouping method.\n"
"L|'rename': files are sorted using the -s/--sort-by then they are renamed."
msgstr ""
#: tools/sort/cli.py:123
#: tools/sort/cli.py:126
msgid "Group by method. When -fp/--final-processing by folders choose the how the images are grouped after sorting. Default: hist"
msgstr ""
#: tools/sort/cli.py:134
#: tools/sort/cli.py:137
msgid "Integer value. Number of folders that will be used to group by blur and face-yaw. For blur folder 0 will be the least blurry, while the last folder will be the blurriest. For face-yaw the number of bins is by how much 180 degrees is divided. So if you use 18, then each folder will be a 10 degree increment. Folder 0 will contain faces looking the most to the left whereas the last folder will contain the faces looking the most to the right. If the number of images doesn't divide evenly into the number of bins, the remaining images get put in the last bin. Default value: 5"
msgstr ""
#: tools/sort/cli.py:145 tools/sort/cli.py:155
#: tools/sort/cli.py:148 tools/sort/cli.py:158
msgid "settings"
msgstr ""
#: tools/sort/cli.py:147
#: tools/sort/cli.py:150
msgid "Logs file renaming changes if grouping by renaming, or it logs the file copying/movement if grouping by folders. If no log file is specified with '--log-file', then a 'sort_log.json' file will be created in the input directory."
msgstr ""
#: tools/sort/cli.py:158
#: tools/sort/cli.py:161
msgid "Specify a log file to use for saving the renaming or grouping information. If specified extension isn't 'json' or 'yaml', then json will be used as the serializer, with the supplied filename. Default: sort_log.json"
msgstr ""

View File

@ -266,6 +266,8 @@ class _DiskIO(): # pylint:disable=too-few-public-methods
for item in self._alignments.data[key]["faces"]:
face = DetectedFace()
face.from_alignment(item, with_thumb=True)
face.load_aligned(None)
_ = face.aligned.average_distance # cache the distances
this_frame_faces.append(face)
self._frame_faces.append(this_frame_faces)
@ -306,6 +308,8 @@ class _DiskIO(): # pylint:disable=too-few-public-methods
for detected_face, face in zip(faces, alignments):
detected_face.from_alignment(face, with_thumb=True)
detected_face.load_aligned(None, force=True)
_ = detected_face.aligned.average_distance # cache the distances
self._updated_frame_indices.remove(frame_index)
if not self._updated_frame_indices:
@ -390,18 +394,17 @@ class _DiskIO(): # pylint:disable=too-few-public-methods
progress_queue: :class:`queue.Queue`
The queue to place incremental counts to for updating the GUI's progress bar
"""
saver = ImagesSaver(str(get_folder(output_folder)), as_bytes=True)
loader = ImagesLoader(self._input_location, count=self._alignments.frames_count)
extension = ".png"
_io = dict(saver=ImagesSaver(str(get_folder(output_folder)), as_bytes=True),
loader=ImagesLoader(self._input_location, count=self._alignments.frames_count))
for frame_idx, (filename, image) in enumerate(loader.load()):
for frame_idx, (filename, image) in enumerate(_io["loader"].load()):
logger.trace("Outputting frame: %s: %s", frame_idx, filename)
src_filename = os.path.basename(filename)
frame_name = os.path.splitext(src_filename)[0]
progress_queue.put(1)
for face_idx, face in enumerate(self._frame_faces[frame_idx]):
output = "{}_{}{}".format(frame_name, str(face_idx), extension)
output = "{}_{}{}".format(frame_name, str(face_idx), ".png")
aligned = AlignedFace(face.landmarks_xy,
image=image,
centering="head",
@ -413,9 +416,9 @@ class _DiskIO(): # pylint:disable=too-few-public-methods
source_filename=src_filename,
source_is_video=self._globals.is_video))
b_image = encode_image(aligned.face, extension, metadata=meta)
saver.save(output, b_image)
saver.close()
b_image = encode_image(aligned.face, ".png", metadata=meta)
_io["saver"].save(output, b_image)
_io["saver"].close()
class Filter():
@ -434,6 +437,16 @@ class Filter():
self._detected_faces = detected_faces
logger.debug("Initialized %s", self.__class__.__name__)
@property
def _filter_distance(self):
""" float: The currently selected distance when Misaligned Faces filter is selected. """
try:
retval = self._globals.tk_filter_distance.get()
except tk.TclError:
# Suppress error when distance box is empty
retval = 0
return retval / 100.
@property
def count(self):
""" int: The number of frames that meet the filter criteria returned by
@ -445,6 +458,10 @@ class Filter():
retval = sum(1 for fcount in face_count_per_index if fcount != 0)
elif self._globals.filter_mode == "Multiple Faces":
retval = sum(1 for fcount in face_count_per_index if fcount > 1)
elif self._globals.filter_mode == "Misaligned Faces":
distance = self._filter_distance
retval = sum(1 for frame in self._detected_faces.current_faces
if any(face.aligned.average_distance > distance for face in frame))
else:
retval = len(face_count_per_index)
logger.trace("filter mode: %s, frame count: %s", self._globals.filter_mode, retval)
@ -456,15 +473,15 @@ class Filter():
displayed face. """
frame_indices = []
face_indices = []
if self._globals.filter_mode != "No Faces":
for frame_idx, face_count in enumerate(self._detected_faces.face_count_per_index):
if face_count <= 1 and self._globals.filter_mode == "Multiple Faces":
continue
for face_idx in range(face_count):
frame_indices.append(frame_idx)
face_indices.append(face_idx)
logger.trace("frame_indices: %s, face_indices: %s", frame_indices, face_indices)
face_counts = self._detected_faces.face_count_per_index # Copy to avoid recalculations
for frame_idx in self.frames_list:
for face_idx in range(face_counts[frame_idx]):
frame_indices.append(frame_idx)
face_indices.append(face_idx)
retval = dict(frame=frame_indices, face=face_indices)
logger.trace("frame_indices: %s, face_indices: %s", frame_indices, face_indices)
return retval
@property
@ -478,6 +495,10 @@ class Filter():
retval = [idx for idx, count in enumerate(face_count_per_index) if count > 1]
elif self._globals.filter_mode == "Has Face(s)":
retval = [idx for idx, count in enumerate(face_count_per_index) if count != 0]
elif self._globals.filter_mode == "Misaligned Faces":
distance = self._filter_distance
retval = [idx for idx, frame in enumerate(self._detected_faces.current_faces)
if any(face.aligned.average_distance > distance for face in frame)]
else:
retval = range(len(face_count_per_index))
logger.trace("filter mode: %s, number_frames: %s", self._globals.filter_mode, len(retval))
@ -646,7 +667,7 @@ class FaceUpdate():
aligned = AlignedFace(face.landmarks_xy,
centering="face",
size=min(self._globals.frame_display_dims))
landmark = aligned.landmarks[landmark_index]
landmark = aligned.landmarks[landmark_index] # pylint:disable=unsubscriptable-object
landmark += (shift_x, shift_y)
matrix = aligned.adjusted_matrix
matrix = cv2.invertAffineTransform(matrix)
@ -661,7 +682,6 @@ class FaceUpdate():
face.landmarks_xy[idx] = lmk
else:
face.landmarks_xy[landmark_index] += (shift_x, shift_y)
face.mask = self._extractor.get_masks(frame_index, face_index)
self._globals.tk_update.set(True)
def landmarks(self, frame_index, face_index, shift_x, shift_y):
@ -689,7 +709,6 @@ class FaceUpdate():
face.x += shift_x
face.y += shift_y
face.landmarks_xy += (shift_x, shift_y)
face.mask = self._extractor.get_masks(frame_index, face_index)
self._globals.tk_update.set(True)
def landmarks_rotate(self, frame_index, face_index, angle, center):
@ -712,7 +731,6 @@ class FaceUpdate():
rot_mat = cv2.getRotationMatrix2D(tuple(center.astype("float32")), angle, 1.)
face.landmarks_xy = cv2.transform(np.expand_dims(face.landmarks_xy, axis=0),
rot_mat).squeeze()
face.mask = self._extractor.get_masks(frame_index, face_index)
self._globals.tk_update.set(True)
def landmarks_scale(self, frame_index, face_index, scale, center):
@ -733,7 +751,6 @@ class FaceUpdate():
"""
face = self._faces_at_frame_index(frame_index)[face_index]
face.landmarks_xy = ((face.landmarks_xy - center) * scale) + center
face.mask = self._extractor.get_masks(frame_index, face_index)
self._globals.tk_update.set(True)
def mask(self, frame_index, face_index, mask, mask_type):
@ -782,12 +799,24 @@ class FaceUpdate():
# No previous/next frame available
return
logger.debug("Copying alignments from frame %s to frame: %s", idx, frame_index)
faces.extend(deepcopy(self._faces_at_frame_index(idx)))
# aligned_face cannot be deep copied, so remove and recreate
to_copy = self._faces_at_frame_index(idx)
for face in to_copy:
face.aligned = None
copied = deepcopy(to_copy)
for old_face, new_face in zip(to_copy, copied):
old_face.load_aligned(None)
new_face.load_aligned(None)
faces.extend(copied)
self._tk_face_count_changed.set(True)
self._globals.tk_update.set(True)
def post_edit_trigger(self, frame_index, face_index):
""" Update the jpg thumbnail and the viewport thumbnail on a face edit.
""" Update the jpg thumbnail, the viewport thumbnail, the landmark masks and the aligned
face on a face edit.
Parameters
----------
@ -797,11 +826,16 @@ class FaceUpdate():
The face index within the frame
"""
face = self._frame_faces[frame_index][face_index]
face.load_aligned(None, force=True) # Update average distance
face.mask = self._extractor.get_masks(frame_index, face_index)
aligned = AlignedFace(face.landmarks_xy,
image=self._globals.current_frame["image"],
centering="head",
size=96)
face.thumbnail = generate_thumbnail(aligned.face, size=96)
if self._globals.filter_mode == "Misaligned Faces":
self._detected_faces.tk_face_count_changed.set(True)
self._tk_edited.set(True)

View File

@ -310,6 +310,7 @@ class Viewport():
size=self.face_size)
landmarks = dict(polygon=[], line=[])
for area, val in self._landmark_mapping.items():
# pylint:disable=unsubscriptable-object
points = aligned.landmarks[val[0]:val[1]] + top_left
shape = "polygon" if area.endswith("eye") or area.startswith("mouth") else "line"
landmarks[shape].append(points)
@ -908,6 +909,8 @@ class ActiveFrame():
self._assets["meshes"],
self._assets["boxes"],
self._assets["faces"])):
if det_face is None:
continue
top_left = np.array(self._canvas.coords(image_id))
coords = (*top_left, *top_left + self._size)
tk_face = self._viewport.get_tk_face(self.frame_index, face_idx, det_face)

View File

@ -24,6 +24,7 @@ class Navigation():
"""
def __init__(self, display_frame):
logger.debug("Initializing %s", self.__class__.__name__)
self._display_frame = display_frame
self._globals = display_frame._globals
self._det_faces = display_frame._det_faces
self._nav = display_frame._nav
@ -37,19 +38,23 @@ class Navigation():
return self._nav["scale"].cget("to") + 1
def nav_scale_callback(self, *args, reset_progress=True): # pylint:disable=unused-argument
""" Adjust transport slider scale for different filters.
""" Adjust transport slider scale for different filters. Hide or display optional filter
controls.
Returns
-------
bool
``True`` if the navigation scale has been updated otherwise ``False``
"""
self._display_frame.pack_threshold_slider()
if reset_progress:
self.stop_playback()
frame_count = self._det_faces.filter.count
if self._current_nav_frame_count == frame_count:
logger.trace("Filtered count has not changed. Returning")
return False
if self._globals.tk_filter_mode.get() == "Misaligned Faces":
self._det_faces.tk_face_count_changed.set(True)
max_frame = max(0, frame_count - 1)
logger.debug("Filtered frame count has changed. Updating from %s to %s",
self._current_nav_frame_count, frame_count)

View File

@ -42,6 +42,7 @@ class DisplayFrame(ttk.Frame): # pylint:disable=too-many-ancestors
self._globals = tk_globals
self._det_faces = detected_faces
self._optional_widgets = dict()
self._actions_frame = ActionsFrame(self)
main_frame = ttk.Frame(self)
@ -81,7 +82,9 @@ class DisplayFrame(ttk.Frame): # pylint:disable=too-many-ancestors
end=_("Go to Last Frame (END)"),
extract=_("Extract the faces to a folder... (Ctrl+E)"),
save=_("Save the Alignments file (Ctrl+S)"),
mode=_("Filter Frames to only those Containing the Selected Item (F)"))
mode=_("Filter Frames to only those Containing the Selected Item (F)"),
distance=_("Set the distance from an 'average face' to be considered misaligned. "
"Higher distances are more restrictive"))
@property
def _btn_action(self):
@ -131,13 +134,11 @@ class DisplayFrame(ttk.Frame): # pylint:disable=too-many-ancestors
@property
def _filter_modes(self):
""" list: The filter modes combo box values """
return ["All Frames", "Has Face(s)", "No Faces", "Multiple Faces"]
return ["All Frames", "Has Face(s)", "No Faces", "Multiple Faces", "Misaligned Faces"]
def _add_nav(self):
""" Add the slider to navigate through frames """
self._globals.tk_transport_index.trace("w", self._set_frame_index)
max_frame = self._globals.frame_count - 1
frame = ttk.Frame(self._transport_frame)
frame.pack(side=tk.TOP, fill=tk.X, pady=(0, 5))
@ -163,6 +164,7 @@ class DisplayFrame(ttk.Frame): # pylint:disable=too-many-ancestors
to=max_frame,
command=cmd)
nav.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self._globals.tk_transport_index.trace("w", self._set_frame_index)
return dict(entry=tbox, scale=nav, label=lbl)
def _set_frame_index(self, *args): # pylint:disable=unused-argument
@ -195,9 +197,10 @@ class DisplayFrame(ttk.Frame): # pylint:disable=too-many-ancestors
wgt = ttk.Button(frame, image=icons[icon], command=self._btn_action[action])
wgt.state(state)
else:
wgt = self._add_filter_mode_combo(frame)
wgt = self._add_filter_section(frame)
wgt.pack(side=side, padx=padx)
Tooltip(wgt, text=self._helptext[action])
if action != "mode":
Tooltip(wgt, text=self._helptext[action])
buttons[action] = wgt
logger.debug("Transport buttons: %s", buttons)
return buttons
@ -207,8 +210,33 @@ class DisplayFrame(ttk.Frame): # pylint:disable=too-many-ancestors
self._navigation.tk_is_playing.trace("w", self._play)
self._det_faces.tk_unsaved.trace("w", self._toggle_save_state)
def _add_filter_section(self, frame):
""" Add the section that holds the filter mode combo and any optional filter widgets
Parameters
----------
frame: :class:`tkinter.ttk.Frame`
The Frame that holds the filter section
Returns
-------
:class:`tkinter.ttk.Frame`
The filter section frame
"""
filter_frame = ttk.Frame(frame)
self._add_filter_mode_combo(filter_frame)
self._add_filter_threshold_slider(filter_frame)
filter_frame.pack(side=tk.RIGHT)
return filter_frame
def _add_filter_mode_combo(self, frame):
""" Add the navigation mode combo box to the transport frame """
""" Add the navigation mode combo box to the filter frame.
Parameters
----------
frame: :class:`tkinter.ttk.Frame`
The Filter Frame that holds the filter combo box
"""
self._globals.tk_filter_mode.set("All Frames")
self._globals.tk_filter_mode.trace("w", self._navigation.nav_scale_callback)
nav_frame = ttk.Frame(frame)
@ -220,7 +248,52 @@ class DisplayFrame(ttk.Frame): # pylint:disable=too-many-ancestors
state="readonly",
values=self._filter_modes)
combo.pack(side=tk.RIGHT)
return nav_frame
Tooltip(nav_frame, text=self._helptext["mode"])
nav_frame.pack(side=tk.RIGHT)
def _add_filter_threshold_slider(self, frame):
""" Add the optional filter threshold slider for misaligned filter to the filter frame.
Parameters
----------
frame: :class:`tkinter.ttk.Frame`
The Filter Frame that holds the filter threshold slider
"""
slider_frame = ttk.Frame(frame)
tk_var = self._globals.tk_filter_distance
min_max = (5, 20)
ctl_frame = ttk.Frame(slider_frame)
ctl_frame.pack(padx=2, side=tk.RIGHT)
lbl = ttk.Label(ctl_frame, text="Distance:", anchor=tk.W)
lbl.pack(side=tk.LEFT, anchor=tk.N, expand=True)
tbox = ttk.Entry(ctl_frame, width=6, textvariable=tk_var, justify=tk.RIGHT)
tbox.pack(padx=(0, 5), side=tk.RIGHT)
ctl = ttk.Scale(
ctl_frame,
variable=tk_var,
command=lambda val, var=tk_var, dt=int, rn=1, mm=min_max:
set_slider_rounding(val, var, dt, rn, mm))
ctl["from_"] = min_max[0]
ctl["to"] = min_max[1]
ctl.pack(padx=5, fill=tk.X, expand=True)
for item in (tbox, ctl):
Tooltip(item,
text=self._helptext["distance"],
wrap_length=200)
tk_var.trace("w", self._navigation.nav_scale_callback)
self._optional_widgets["distance_slider"] = slider_frame
def pack_threshold_slider(self):
""" Display or hide the threshold slider depending on the current filter mode. For
misaligned faces filter, display the slider. Hide for all other filters. """
if self._globals.tk_filter_mode.get() == "Misaligned Faces":
self._optional_widgets["distance_slider"].pack(side=tk.LEFT)
else:
self._optional_widgets["distance_slider"].pack_forget()
def cycle_filter_mode(self):
""" Cycle the navigation mode combo entry """

View File

@ -422,9 +422,9 @@ class TkGlobals():
The variable name as key, the variable as value
"""
retval = dict()
for name in ("frame_index", "transport_index", "face_index"):
for name in ("frame_index", "transport_index", "face_index", "filter_distance"):
var = tk.IntVar()
var.set(0)
var.set(10 if name == "filter_distance" else 0)
retval[name] = var
for name in ("update", "update_active_viewport", "is_zoomed"):
var = tk.BooleanVar()
@ -503,6 +503,12 @@ class TkGlobals():
filter mode. """
return self._tk_vars["filter_mode"]
@property
def tk_filter_distance(self):
""" :class:`tkinter.DoubleVar`: The variable holding the currently selected threshold
distance for misaligned filter mode. """
return self._tk_vars["filter_distance"]
@property
def tk_faces_size(self):
""" :class:`tkinter.StringVar`: The variable holding the currently selected Faces Viewer

View File

@ -43,15 +43,17 @@ class SortArgs(FaceSwapArgs):
opts=('-s', '--sort-by'),
action=Radio,
type=str,
choices=("blur", "blur-fft", "face", "face-cnn", "face-cnn-dissim", "face-yaw", "hist",
"hist-dissim", "color-gray", "color-luma", "color-green", "color-orange",
"size"),
choices=("blur", "blur-fft", "distance", "face", "face-cnn", "face-cnn-dissim",
"face-yaw", "hist", "hist-dissim", "color-gray", "color-luma", "color-green",
"color-orange", "size"),
dest='sort_method',
group=_("sort settings"),
default="face",
help=_("R|Sort by method. Choose how images are sorted. "
"\nL|'blur': Sort faces by blurriness."
"\nL|'blur-fft': Sort faces by fft filtered blurriness."
"\nL|'distance' Sort faces by the estimated distance of the alignments from an "
"'average' face. This can be useful for eliminating misaligned faces."
"\nL|'face': Use VGG Face to sort by face similarity. This uses a pairwise "
"clustering algorithm to check the distances between 512 features on every "
"face in your set and order them appropriately."

View File

@ -16,7 +16,7 @@ from tqdm import tqdm
# faceswap imports
from lib.serializer import get_serializer_from_filename
from lib.align import AlignedFace, DetectedFace
from lib.image import FacesLoader, read_image
from lib.image import FacesLoader, read_image, read_image_meta_batch
from lib.utils import FaceswapError
from plugins.extract.recognition.vgg_face2_keras import VGGFace2 as VGGFace
from plugins.extract.pipeline import Extractor, ExtractMedia
@ -152,6 +152,34 @@ class Sort():
logger.info("Done.")
# Methods for sorting
def sort_distance(self):
""" Sort by comparison of face landmark points to mean face by average distance of core
landmarks. """
logger.info("Sorting by average distance of landmarks...")
filenames = []
distances = []
filelist = [os.path.join(self._loader.location, fname)
for fname in os.listdir(self._loader.location)
if os.path.splitext(fname)[-1] == ".png"]
for filename, metadata in tqdm(read_image_meta_batch(filelist),
total=len(filelist),
desc="Calculating Distances"):
if not metadata:
msg = ("The images to be sorted do not contain alignment data. Images must have "
"been generated by Faceswap's Extract process.\nIf you are sorting an "
"older faceset, then you should re-extract the faces from your source "
"alignments file to generate this data.")
raise FaceswapError(msg)
alignments = metadata["itxt"]["alignments"]
aligned_face = AlignedFace(np.array(alignments["landmarks_xy"], dtype="float32"))
filenames.append(filename)
distances.append(aligned_face.average_distance)
logger.info("Sorting...")
matched_list = list(zip(filenames, distances))
img_list = sorted(matched_list, key=operator.itemgetter(1))
return img_list
def sort_blur(self):
""" Sort by blur amount """
logger.info("Sorting by estimated image blur...")
@ -176,6 +204,31 @@ class Sort():
logger.info("Sorting...")
return sorted(fft_blurs, key=lambda x: x[1], reverse=True)
def sort_color(self):
""" Score by channel average intensity """
logger.info("Sorting by channel average intensity...")
desired_channel = {'gray': 0, 'luma': 0, 'orange': 1, 'green': 2}
method = self._args.color_method
channel_to_sort = next(v for (k, v) in desired_channel.items() if method.endswith(k))
filename_list, image_list = self._get_images()
logger.info("Converting to appropriate colorspace...")
same_size = all(img.size == image_list[0].size for img in image_list)
images = np.array(image_list, dtype='float32')[None, ...] if same_size else image_list
converted_images = self._convert_color(images, same_size, method)
logger.info("Scoring each image...")
if same_size:
scores = np.average(converted_images[0], axis=(1, 2))
else:
progress_bar = tqdm(converted_images, desc="Scoring", file=sys.stdout)
scores = np.array([np.average(image, axis=(0, 1)) for image in progress_bar])
logger.info("Sorting...")
matched_list = list(zip(filename_list, scores[:, channel_to_sort]))
sorted_file_img_list = sorted(matched_list, key=operator.itemgetter(1), reverse=True)
return sorted_file_img_list
def sort_face(self):
""" Sort by identity similarity """
logger.info("Sorting by identity similarity...")
@ -327,31 +380,6 @@ class Sort():
logger.info("Sorting...")
return sorted(img_list, key=lambda x: x[2], reverse=True)
def sort_color(self):
""" Score by channel average intensity """
logger.info("Sorting by channel average intensity...")
desired_channel = {'gray': 0, 'luma': 0, 'orange': 1, 'green': 2}
method = self._args.color_method
channel_to_sort = next(v for (k, v) in desired_channel.items() if method.endswith(k))
filename_list, image_list = self._get_images()
logger.info("Converting to appropriate colorspace...")
same_size = all(img.size == image_list[0].size for img in image_list)
images = np.array(image_list, dtype='float32')[None, ...] if same_size else image_list
converted_images = self._convert_color(images, same_size, method)
logger.info("Scoring each image...")
if same_size:
scores = np.average(converted_images[0], axis=(1, 2))
else:
progress_bar = tqdm(converted_images, desc="Scoring", file=sys.stdout)
scores = np.array([np.average(image, axis=(0, 1)) for image in progress_bar])
logger.info("Sorting...")
matched_list = list(zip(filename_list, scores[:, channel_to_sort]))
sorted_file_img_list = sorted(matched_list, key=operator.itemgetter(1), reverse=True)
return sorted_file_img_list
def sort_size(self):
""" Sort the faces by largest face (in original frame) to smallest """
logger.info("Sorting by original face size...")
@ -372,7 +400,8 @@ class Sort():
centering="legacy",
is_aligned=True)
roi = aligned_face.original_roi
size = ((roi[1][0] - roi[0][0]) ** 2 + (roi[1][1] - roi[0][1]) ** 2) ** 0.5
size = ((roi[1][0] - roi[0][0]) ** 2 + # pylint:disable=unsubscriptable-object
(roi[1][1] - roi[0][1]) ** 2) ** 0.5 # pylint:disable=unsubscriptable-object
img_list.append((filename, size))
logger.info("Sorting...")