class Convert(): """ The convert process. """ def __init__(self, arguments): logger.debug("Initializing %s: (args: %s)", self.__class__.__name__, arguments) self.args = arguments Utils.set_verbosity(self.args.loglevel) self.patch_threads = None self.images = Images(self.args) self.validate() self.alignments = Alignments(self.args, False, self.images.is_video) self.opts = OptionalActions(self.args, self.images.input_images, self.alignments) self.add_queues() self.disk_io = DiskIO(self.alignments, self.images, arguments) self.predictor = Predict(self.disk_io.load_queue, self.queue_size, arguments) configfile = self.args.configfile if hasattr(self.args, "configfile") else None self.converter = Converter(get_folder(self.args.output_dir), self.predictor.output_size, self.predictor.has_predicted_mask, self.disk_io.draw_transparent, self.disk_io.pre_encode, arguments, configfile=configfile) logger.debug("Initialized %s", self.__class__.__name__) @property def queue_size(self): """ Set 16 for singleprocess otherwise 32 """ if self.args.singleprocess: retval = 16 else: retval = 32 logger.debug(retval) return retval @property def pool_processes(self): """ return the maximum number of pooled processes to use """ if self.args.singleprocess: retval = 1 elif self.args.jobs > 0: retval = min(self.args.jobs, total_cpus(), self.images.images_found) else: retval = min(total_cpus(), self.images.images_found) retval = 1 if retval == 0 else retval logger.debug(retval) return retval def validate(self): """ Make the output folder if it doesn't exist and check that video flag is a valid choice """ if (self.args.writer == "ffmpeg" and not self.images.is_video and self.args.reference_video is None): raise FaceswapError( "Output as video selected, but using frames as input. You must " "provide a reference video ('-ref', '--reference-video').") output_dir = get_folder(self.args.output_dir) logger.info("Output Directory: %s", output_dir) def add_queues(self): """ Add the queues for convert """ logger.debug("Adding queues. Queue size: %s", self.queue_size) for qname in ("convert_in", "convert_out", "patch"): queue_manager.add_queue(qname, self.queue_size) def process(self): """ Process the conversion """ logger.debug("Starting Conversion") # queue_manager.debug_monitor(5) try: self.convert_images() self.disk_io.save_thread.join() queue_manager.terminate_queues() Utils.finalize(self.images.images_found, self.predictor.faces_count, self.predictor.verify_output) logger.debug("Completed Conversion") except MemoryError as err: msg = ( "Faceswap ran out of RAM running convert. Conversion is very system RAM " "heavy, so this can happen in certain circumstances when you have a lot of " "cpus but not enough RAM to support them all." "\nYou should lower the number of processes in use by either setting the " "'singleprocess' flag (-sp) or lowering the number of parallel jobs (-j)." ) raise FaceswapError(msg) from err def convert_images(self): """ Convert the images """ logger.debug("Converting images") save_queue = queue_manager.get_queue("convert_out") patch_queue = queue_manager.get_queue("patch") self.patch_threads = MultiThread(self.converter.process, patch_queue, save_queue, thread_count=self.pool_processes, name="patch") self.patch_threads.start() while True: self.check_thread_error() if self.disk_io.completion_event.is_set(): logger.debug("DiskIO completion event set. Joining Pool") break if self.patch_threads.completed(): logger.debug("All patch threads completed") break sleep(1) self.patch_threads.join() logger.debug("Putting EOF") save_queue.put("EOF") logger.debug("Converted images") def check_thread_error(self): """ Check and raise thread errors """ for thread in (self.predictor.thread, self.disk_io.load_thread, self.disk_io.save_thread, self.patch_threads): thread.check_and_raise_error()
class Convert(): # pylint:disable=too-few-public-methods """ The Faceswap Face Conversion Process. The conversion process is responsible for swapping the faces on source frames with the output from a trained model. It leverages a series of user selected post-processing plugins, executed from :class:`lib.convert.Converter`. The convert process is self contained and should not be referenced by any other scripts, so it contains no public properties. Parameters ---------- arguments: :class:`argparse.Namespace` The arguments to be passed to the convert process as generated from Faceswap's command line arguments """ def __init__(self, arguments): logger.debug("Initializing %s: (args: %s)", self.__class__.__name__, arguments) self._args = arguments self._patch_threads = None self._images = ImagesLoader(self._args.input_dir, fast_count=True) self._alignments = Alignments(self._args, False, self._images.is_video) if self._alignments.version == 1.0: logger.error( "The alignments file format has been updated since the given alignments " "file was generated. You need to update the file to proceed.") logger.error( "To do this run the 'Alignments Tool' > 'Extract' Job.") sys.exit(1) self._opts = OptionalActions(self._args, self._images.file_list, self._alignments) self._add_queues() self._disk_io = DiskIO(self._alignments, self._images, arguments) self._predictor = Predict(self._disk_io.load_queue, self._queue_size, arguments) self._validate() get_folder(self._args.output_dir) configfile = self._args.configfile if hasattr(self._args, "configfile") else None self._converter = Converter(self._predictor.output_size, self._predictor.coverage_ratio, self._predictor.centering, self._disk_io.draw_transparent, self._disk_io.pre_encode, arguments, configfile=configfile) logger.debug("Initialized %s", self.__class__.__name__) @property def _queue_size(self): """ int: Size of the converter queues. 16 for single process otherwise 32 """ if self._args.singleprocess: retval = 16 else: retval = 32 logger.debug(retval) return retval @property def _pool_processes(self): """ int: The number of threads to run in parallel. Based on user options and number of available processors. """ if self._args.singleprocess: retval = 1 elif self._args.jobs > 0: retval = min(self._args.jobs, total_cpus(), self._images.count) else: retval = min(total_cpus(), self._images.count) retval = 1 if retval == 0 else retval logger.debug(retval) return retval def _validate(self): """ Validate the Command Line Options. Ensure that certain cli selections are valid and won't result in an error. Checks: * If frames have been passed in with video output, ensure user supplies reference video. * If "on-the-fly" and an NN mask is selected, output warning and switch to 'extended' * If a mask-type is selected, ensure it exists in the alignments file. * If a predicted mask-type is selected, ensure model has been trained with a mask otherwise attempt to select first available masks, otherwise raise error. Raises ------ FaceswapError If an invalid selection has been found. """ if (self._args.writer == "ffmpeg" and not self._images.is_video and self._args.reference_video is None): raise FaceswapError( "Output as video selected, but using frames as input. You must " "provide a reference video ('-ref', '--reference-video').") if (self._args.on_the_fly and self._args.mask_type not in ("none", "extended", "components")): logger.warning( "You have selected an incompatible mask type ('%s') for On-The-Fly " "conversion. Switching to 'extended'", self._args.mask_type) self._args.mask_type = "extended" if (not self._args.on_the_fly and self._args.mask_type not in ("none", "predicted") and not self._alignments.mask_is_valid(self._args.mask_type)): msg = ( "You have selected the Mask Type `{}` but at least one face does not have this " "mask stored in the Alignments File.\nYou should generate the required masks " "with the Mask Tool or set the Mask Type option to an existing Mask Type.\nA " "summary of existing masks is as follows:\nTotal faces: {}, Masks: " "{}".format(self._args.mask_type, self._alignments.faces_count, self._alignments.mask_summary)) raise FaceswapError(msg) if self._args.mask_type == "predicted" and not self._predictor.has_predicted_mask: available_masks = [ k for k, v in self._alignments.mask_summary.items() if k != "none" and v == self._alignments.faces_count ] if not available_masks: msg = ( "Predicted Mask selected, but the model was not trained with a mask and no " "masks are stored in the Alignments File.\nYou should generate the " "required masks with the Mask Tool or set the Mask Type to `none`." ) raise FaceswapError(msg) mask_type = available_masks[0] logger.warning( "Predicted Mask selected, but the model was not trained with a " "mask. Selecting first available mask: '%s'", mask_type) self._args.mask_type = mask_type def _add_queues(self): """ Add the queues for in, patch and out. """ logger.debug("Adding queues. Queue size: %s", self._queue_size) for qname in ("convert_in", "convert_out", "patch"): queue_manager.add_queue(qname, self._queue_size) def process(self): """ The entry point for triggering the Conversion Process. Should only be called from :class:`lib.cli.launcher.ScriptExecutor` """ logger.debug("Starting Conversion") # queue_manager.debug_monitor(5) try: self._convert_images() self._disk_io.save_thread.join() queue_manager.terminate_queues() finalize(self._images.count, self._predictor.faces_count, self._predictor.verify_output) logger.debug("Completed Conversion") except MemoryError as err: msg = ( "Faceswap ran out of RAM running convert. Conversion is very system RAM " "heavy, so this can happen in certain circumstances when you have a lot of " "cpus but not enough RAM to support them all." "\nYou should lower the number of processes in use by either setting the " "'singleprocess' flag (-sp) or lowering the number of parallel jobs (-j)." ) raise FaceswapError(msg) from err def _convert_images(self): """ Start the multi-threaded patching process, monitor all threads for errors and join on completion. """ logger.debug("Converting images") save_queue = queue_manager.get_queue("convert_out") patch_queue = queue_manager.get_queue("patch") self._patch_threads = MultiThread(self._converter.process, patch_queue, save_queue, thread_count=self._pool_processes, name="patch") self._patch_threads.start() while True: self._check_thread_error() if self._disk_io.completion_event.is_set(): logger.debug("DiskIO completion event set. Joining Pool") break if self._patch_threads.completed(): logger.debug("All patch threads completed") break sleep(1) self._patch_threads.join() logger.debug("Putting EOF") save_queue.put("EOF") logger.debug("Converted images") def _check_thread_error(self): """ Monitor all running threads for errors, and raise accordingly. """ for thread in (self._predictor.thread, self._disk_io.load_thread, self._disk_io.save_thread, self._patch_threads): thread.check_and_raise_error()