示例#1
0
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()
示例#2
0
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()