1
0
Fork 0
mirror of https://github.com/deepfakes/faceswap synced 2025-06-09 04:36:50 -04:00
faceswap/scripts/convert.py
Lev Velykoivanenko 80cde77a6d Adding new tool effmpeg ("easy"-ffmpeg) with gui support. Extend gui functionality to support filetypes. Re-opening PR. (#373)
* Pre push commit.
Add filetypes support to gui through new classes in lib/cli.py
Add various new functions to tools/effmpeg.py

* Finish developing basic effmpeg functionality.
Ready for public alpha test.

* Add ffmpy to requirements.
Fix gen-vid to allow specifying a new file in GUI.
Fix extract throwing an error when supplied with a valid directory.

Add two new gui user pop interactions: save (allows you to create new
files/directories) and nothing (disables the prompt button when it's not
needed).
Improve logic and argument processing in effmpeg.

* Fix post merge bugs.
Reformat tools.py to match the new style of faceswap.py
Fix some whitespace issues.

* Fix matplotlib.use() being called after pyplot was imported.

* Fix various effmpeg bugs and add ability do terminate nested subprocess
to GUI.

effmpeg changes:
Fix get-fps not printing to terminal.
Fix mux-audio not working.
Add verbosity option. If verbose is not specified than ffmpeg output is
reduced with the -hide_banner flag.

scripts/gui.py changes:
Add ability to terminate nested subprocesses, i.e. the following type of
process tree should now be terminated safely:
gui -> command -> command-subprocess
               -> command-subprocess -> command-sub-subprocess

* Add functionality to tools/effmpeg.py, fix some docstring and print statement issues in some files.

tools/effmpeg.py:
Transpose choices now display detailed name in GUI, while in cli they can
still be entered as a number or the full command name.
Add quiet option to effmpeg that only shows critical ffmpeg errors.
Improve user input handling.

lib/cli.py; scripts/convert.py; scripts/extract.py; scripts/train.py:
Fix some line length issues and typos in docstrings, help text and print statements.
Fix some whitespace issues.

lib/cli.py:
Add filetypes to '--alignments' argument.
Change argument action to DirFullPaths where appropriate.

* Bug fixes and improvements to tools/effmpeg.py

Fix bug where duration would not be used even when end time was not set.
Add option to specify output filetype for extraction.
Enchance gen-vid to be able to generate a video from images that were zero padded to any arbitrary number, and not just 5.
Enchance gen-vid to be able to use any of the image formats that a video can be extracted into.
Improve gen-vid output video quality.
Minor code quality improvements and ffmpeg argument formatting improvements.

* Remove dependency on psutil in scripts/gui.py and various small improvements.

lib/utils.py:
Add _image_extensions and _video_extensions as global variables to make them easily portable across all of faceswap.
Fix lack of new lines between function and class declarions to conform to PEP8.
Fix some typos and line length issues in doctsrings and comments.

scripts/convert.py:
Make tqdm print to stdout.

scripts/extract.py:
Make tqdm print to stdout.
Apply workaround for occasional TqdmSynchronisationWarning being thrown.
Fix some typos and line length issues in doctsrings and comments.

scripts/fsmedia.py:
Did TODO in scripts/fsmedia.py in Faces.load_extractor(): TODO Pass extractor_name as argument
Fix lack of new lines between function and class declarions to conform to PEP8.
Fix some typos and line length issues in doctsrings and comments.
Change 2 print statements to use format() for string formatting instead of the old '%'.

scripts/gui.py:
Refactor subprocess generation and termination to remove dependency on psutil.
Fix some typos and line length issues in comments.

tools/effmpeg.py
Refactor DataItem class to use new lib/utils.py global media file extensions.
Improve ffmpeg subprocess termination handling.
2018-05-09 18:47:17 +01:00

211 lines
8.3 KiB
Python

#!/usr/bin python3
""" The script to run the convert process of faceswap """
import re
import os
import sys
from pathlib import Path
from tqdm import tqdm
from scripts.fsmedia import Alignments, Images, Faces, Utils
from scripts.extract import Extract
from lib.utils import BackgroundGenerator, get_folder, get_image_paths
from plugins.PluginLoader import PluginLoader
class Convert(object):
""" The convert process. """
def __init__(self, arguments):
self.args = arguments
self.output_dir = get_folder(self.args.output_dir)
self.images = Images(self.args)
self.faces = Faces(self.args)
self.alignments = Alignments(self.args)
self.opts = OptionalActions(self.args, self.images.input_images)
def process(self):
""" Original & LowMem models go with Adjust or Masked converter
Note: GAN prediction outputs a mask + an image, while other
predicts only an image. """
Utils.set_verbosity(self.args.verbose)
if not self.alignments.have_alignments_file:
self.generate_alignments()
self.faces.faces_detected = self.alignments.read_alignments()
model = self.load_model()
converter = self.load_converter(model)
batch = BackgroundGenerator(self.prepare_images(), 1)
for item in batch.iterator():
self.convert(converter, item)
Utils.finalize(self.images.images_found,
self.faces.num_faces_detected,
self.faces.verify_output)
def generate_alignments(self):
""" Generate an alignments file if one does not already
exist. Does not save extracted faces """
print('Alignments file not found. Generating at default values...')
extract = Extract(self.args)
extract.export_face = False
extract.process()
def load_model(self):
""" Load the model requested for conversion """
model_name = self.args.trainer
model_dir = get_folder(self.args.model_dir)
num_gpus = self.args.gpus
model = PluginLoader.get_model(model_name)(model_dir, num_gpus)
if not model.load(self.args.swap_model):
print("Model Not Found! A valid model must be provided to continue!")
exit(1)
return model
def load_converter(self, model):
""" Load the requested converter for conversion """
args = self.args
conv = args.converter
converter = PluginLoader.get_converter(conv)(model.converter(False),
trainer=args.trainer,
blur_size=args.blur_size,
seamless_clone=args.seamless_clone,
sharpen_image=args.sharpen_image,
mask_type=args.mask_type,
erosion_kernel_size=args.erosion_kernel_size,
match_histogram=args.match_histogram,
smooth_mask=args.smooth_mask,
avg_color_adjust=args.avg_color_adjust)
return converter
def prepare_images(self):
""" Prepare the images for conversion """
filename = ""
for filename in tqdm(self.images.input_images, file=sys.stdout):
if not self.check_alignments(filename):
continue
image = Utils.cv2_read_write('read', filename)
faces = self.faces.get_faces_alignments(filename, image)
if not faces:
continue
yield filename, image, faces
def check_alignments(self, filename):
""" If we have no alignments for this image, skip it """
have_alignments = self.faces.have_face(filename)
if not have_alignments:
tqdm.write("No alignment found for {}, skipping".format(os.path.basename(filename)))
return have_alignments
def convert(self, converter, item):
""" Apply the conversion transferring faces onto frames """
try:
filename, image, faces = item
skip = self.opts.check_skipframe(filename)
if not skip:
for idx, face in faces:
image = self.convert_one_face(converter, (filename, image, idx, face))
if skip != "discard":
filename = str(self.output_dir / Path(filename).name)
Utils.cv2_read_write('write', filename, image)
except Exception as err:
print("Failed to convert image: {}. Reason: {}".format(filename, err))
def convert_one_face(self, converter, imagevars):
""" Perform the conversion on the given frame for a single face """
filename, image, idx, face = imagevars
if self.opts.check_skipface(filename, idx):
return image
image = self.images.rotate_image(image, face.r)
# TODO: This switch between 64 and 128 is a hack for now.
# We should have a separate cli option for size
image = converter.patch_image(image,
face,
64 if "128" not in self.args.trainer else 128)
image = self.images.rotate_image(image, face.r, reverse=True)
return image
class OptionalActions(object):
""" Process the optional actions for convert """
def __init__(self, args, input_images):
self.args = args
self.input_images = input_images
self.faces_to_swap = self.get_aligned_directory()
self.frame_ranges = self.get_frame_ranges()
self.imageidxre = re.compile(r"(\d+)(?!.*\d)")
### SKIP FACES ###
def get_aligned_directory(self):
""" Check for the existence of an aligned directory for identifying
which faces in the target frames should be swapped """
faces_to_swap = None
input_aligned_dir = self.args.input_aligned_dir
if input_aligned_dir is None:
print("Aligned directory not specified. All faces listed in the "
"alignments file will be converted")
elif not os.path.isdir(input_aligned_dir):
print("Aligned directory not found. All faces listed in the "
"alignments file will be converted")
else:
faces_to_swap = [Path(path) for path in get_image_paths(input_aligned_dir)]
if not faces_to_swap:
print("Aligned directory is empty, no faces will be converted!")
elif len(faces_to_swap) <= len(self.input_images) / 3:
print("Aligned directory contains an amount of images much "
"less than the input, are you sure this is the right "
"directory?")
return faces_to_swap
### SKIP FRAME RANGES ###
def get_frame_ranges(self):
""" split out the frame ranges and parse out 'min' and 'max' values """
if not self.args.frame_ranges:
return None
minmax = {"min": 0, # never any frames less than 0
"max": float("inf")}
rng = [tuple(map(lambda q: minmax[q] if q in minmax.keys() else int(q), v.split("-")))
for v in self.args.frame_ranges]
return rng
def check_skipframe(self, filename):
""" Check whether frame is to be skipped """
if not self.frame_ranges:
return None
idx = int(self.imageidxre.findall(filename)[0])
skipframe = not any(map(lambda b: b[0] <= idx <= b[1], self.frame_ranges))
if skipframe and self.args.discard_frames:
skipframe = "discard"
return skipframe
def check_skipface(self, filename, face_idx):
""" Check whether face is to be skipped """
if self.faces_to_swap is None:
return False
face_name = "{}_{}{}".format(Path(filename).stem, face_idx, Path(filename).suffix)
face_file = Path(self.args.input_aligned_dir) / Path(face_name)
skip_face = face_file not in self.faces_to_swap
if skip_face:
print("face {} for frame {} was deleted, skipping".format(
face_idx, os.path.basename(filename)))
return skip_face