示例#1
0
    def __init__(self, arguments):
        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 = dict(opts=dict(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 = dict(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 = None

        self._alignments = Alignments(os.path.dirname(arguments.alignments),
                                      filename=os.path.basename(
                                          arguments.alignments))

        self._extractor = self._get_extractor(arguments.exclude_gpus)
        self._extractor_input_thread = self._feed_extractor()

        logger.debug("Initialized %s", self.__class__.__name__)
示例#2
0
    def _get_alignments(self):
        """ Obtain the alignments data and validate for methods which require it.

        Returns
        -------
        :class:`lib.align.Alignments`
            The alignments object pertaining to the data to be sorted. Returns ``None`` if an
            alignments file can't be found or is not required.
        """
        required_methods = ["face"]
        if self._args.sort_method not in required_methods:
            return None
        if self._args.alignments_path is None:
            path = os.path.join(self._args.input_dir, "alignments.fsa")
        else:
            path = self._args.alignments_path

        if not os.path.isfile(path):
            logger.warning("Alignments file not found at '%s'. Not using an alignments file will "
                           "lead to vastly inferior results.", path)
            logger.warning("It is highly recommended that you use an alignments file for sorting "
                           "by '%s'.", self._args.sort_method)
            return None

        return Alignments(*os.path.split(path))
示例#3
0
    def _get_alignments(self, alignments_path, input_location):
        """ Get the :class:`~lib.align.Alignments` object for the given location.

        Parameters
        ----------
        alignments_path: str
            Full path to the alignments file. If empty string is passed then location is calculated
            from the source folder
        input_location: str
            The location of the input folder of frames or video file

        Returns
        -------
        :class:`~lib.align.Alignments`
            The alignments object for the given input location
        """
        logger.debug("alignments_path: %s, input_location: %s", alignments_path, input_location)
        if alignments_path:
            folder, filename = os.path.split(alignments_path)
        else:
            filename = "alignments.fsa"
            if self._globals.is_video:
                folder, vid = os.path.split(os.path.splitext(input_location)[0])
                filename = "{}_{}".format(vid, filename)
            else:
                folder = input_location
        retval = Alignments(folder, filename)
        if retval.version == 1.0:
            logger.error("The Manual Tool is not compatible with legacy Alignments files.")
            logger.info("You can update legacy Alignments files by using the Extract job in the "
                        "Alignments tool to re-extract the faces in full-head format.")
            sys.exit(0)
        logger.debug("folder: %s, filename: %s, alignments: %s", folder, filename, retval)
        return retval
示例#4
0
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.

    Faceswap Masks tool. Generate masks from existing alignments files, and output masks
    for preview.

    Parameters
    ----------
    arguments: :class:`argparse.Namespace`
        The :mod:`argparse` arguments as passed in from :mod:`tools.py`
    """
    def __init__(self, arguments):
        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 = dict(opts=dict(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 = dict(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 = None

        self._alignments = Alignments(os.path.dirname(arguments.alignments),
                                      filename=os.path.basename(
                                          arguments.alignments))

        self._extractor = self._get_extractor(arguments.exclude_gpus)
        self._extractor_input_thread = self._feed_extractor()

        logger.debug("Initialized %s", self.__class__.__name__)

    def _check_input(self, mask_input):
        """ Check the input is valid. If it isn't exit with a logged error

        Parameters
        ----------
        mask_input: str
            Path to the input folder/video
        """
        if not os.path.exists(mask_input):
            logger.error("Location cannot be found: '%s'", mask_input)
            sys.exit(0)
        if os.path.isfile(mask_input) and self._input_is_faces:
            logger.error(
                "Input type 'faces' was selected but input is not a folder: '%s'",
                mask_input)
            sys.exit(0)
        logger.debug("input '%s' is valid", mask_input)

    def _set_saver(self, arguments):
        """ 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 = str(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_extractor(self, exclude_gpus):
        """ 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,
                              image_is_aligned=self._input_is_faces)
        extractor.launch()
        logger.debug(extractor)
        return extractor

    def _feed_extractor(self):
        """ 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,
            "_input_{}".format("faces" if self._input_is_faces else "frames"))
        logger.debug("masker_input: %s", masker_input)

        args = tuple() if self._update_type == "output" else (
            self._extractor.input_queue, )
        input_thread = MultiThread(masker_input, *args, thread_count=1)
        input_thread.start()
        logger.debug(input_thread)
        return input_thread

    def _input_faces(self, *args):
        """ 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 = args[0]
        for filename, image, metadata in tqdm(self._loader.load(),
                                              total=self._loader.count):
            if not metadata:  # Legacy faces. Update the headers
                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

            frame_name = metadata["source"]["source_filename"]
            face_index = metadata["source"]["face_index"]
            alignment = self._alignments.get_faces_in_frame(frame_name)
            if not alignment or face_index > len(alignment) - 1:
                self._counts["skip"] += 1
                logger.warning(
                    "Skipping Face not found in alignments file. skipping: '%s'",
                    filename)
                continue
            alignment = alignment[face_index]
            self._counts["face"] += 1

            if self._check_for_missing(frame_name, face_index, alignment):
                continue

            detected_face = self._get_detected_face(alignment)
            if self._update_type == "output":
                detected_face.image = image
                self._save(frame_name, face_index, detected_face)
            else:
                media = ExtractMedia(filename,
                                     image,
                                     detected_faces=[detected_face])
                setattr(media, "mask_tool_face_info",
                        metadata["source"])  # TODO formalize
                queue.put(media)
                self._counts["update"] += 1
        if self._update_type != "output":
            queue.put("EOF")

    def _input_frames(self, *args):
        """ 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
        """
        logger.debug("args: %s", args)
        if self._update_type != "output":
            queue = 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, idx, alignment):
        """ 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)
        return retval

    def _get_output_suffix(self, arguments):
        """ The filename suffix, based on selected output options.

        Parameters
        ----------
        arguments: :class:`argparse.Namespace`
            The command line arguments for the mask tool

        Returns
        -------
        str:
            The suffix to be appended to the output filename
        """
        sfx = "{}_mask_preview_".format(self._mask_type)
        sfx += "face_" if not arguments.full_frame or self._input_is_faces else "frame_"
        sfx += "{}.png".format(arguments.output_type)
        return sfx

    @staticmethod
    def _get_detected_face(alignment):
        """ Convert an alignment dict item to a detected_face object

        Parameters
        ----------
        alignment: dict
            The alignment dict for a face

        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

    def process(self):
        """ The entry point for the Mask tool from :file:`lib.tools.cli`. Runs the Mask process """
        logger.debug("Starting masker process")
        updater = getattr(
            self,
            "_update_{}".format("faces" if self._input_is_faces else "frames"))
        if self._update_type != "output":
            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)
            self._extractor_input_thread.join()
            if self._counts["update"] != 0:
                self._alignments.backup()
                self._alignments.save()
            if self._input_is_faces:
                self._faces_saver.close()
        else:
            self._extractor_input_thread.join()
            self._saver.close()

        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"])
        logger.debug("Completed masker process")

    def _update_faces(self, extractor_output):
        """ 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: dict
            The output from the :class:`plugins.extract.pipeline.Extractor` object
        """
        for face in extractor_output.detected_faces:
            frame_name = extractor_output.mask_tool_face_info[
                "source_filename"]
            face_index = extractor_output.mask_tool_face_info["face_index"]
            logger.trace("Saving face: (frame: %s, face index: %s)",
                         frame_name, face_index)

            self._alignments.update_face(frame_name, face_index,
                                         face.to_alignment())
            metadata = dict(alignments=face.to_png_meta(),
                            source=extractor_output.mask_tool_face_info)
            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):
        """ 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: dict
            The output from the :class:`plugins.extract.pipeline.Extractor` object
        """
        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, idx, detected_face):
        """ 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
        """
        filename = os.path.join(
            self._saver.location, "{}_{}_{}".format(
                os.path.splitext(frame)[0], idx, self._output["suffix"]))

        if detected_face.mask is None or detected_face.mask.get(
                self._mask_type, None) is None:
            logger.warning(
                "Mask type '%s' does not exist for frame '%s' index %s. Skipping",
                self._mask_type, frame, idx)
            return
        image = self._create_image(detected_face)
        logger.trace("filename: '%s', image_shape: %s", filename, image.shape)
        self._saver.save(filename, image)

    def _create_image(self, detected_face):
        """ Create a mask preview image for saving out to disk

        Parameters
        ----------
        detected_face: `lib.FacesDetect.detected_face`
            A detected_face object for a face

        Returns
        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[self._mask_type]
        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="face",
                                   size=detected_face.image.shape[0],
                                   is_aligned=True).face
            else:
                centering = "legacy" if self._alignments.version == 1.0 else "face"
                detected_face.load_aligned(detected_face.image,
                                           centering=centering)
                face = detected_face.aligned.face
            mask = cv2.resize(detected_face.mask[self._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
            mask = mask.get_full_frame_mask(face.shape[1], face.shape[0])
            mask = np.expand_dims(mask, -1)

        height, width = face.shape[:2]
        if self._output["type"] == "combined":
            masked = (face.astype("float32") * mask.astype("float32") /
                      255.).astype("uint8")
            mask = np.tile(mask, 3)
            for img in (face, masked, mask):
                cv2.rectangle(img, (0, 0), (width - 1, height - 1),
                              (255, 255, 255), 1)
                out_image = np.concatenate((face, masked, mask), axis=1)
        elif self._output["type"] == "mask":
            out_image = mask
        elif self._output["type"] == "masked":
            out_image = np.concatenate([face, mask], axis=-1)
        return out_image
示例#5
0
    def __init__(self, arguments):
        logger.debug("Initializing %s: (args: %s)", self.__class__.__name__, arguments)
        self._args = arguments

        # load faces
        faces_alignments = AlignmentsBase(self._args.faces_align_dir)
        print()
        print(f'Faces alignments: {len(faces_alignments._data.keys())}')
        print(faces_alignments._data.keys())

        self._faces = {}
        faces_loader = ImagesLoader(self._args.faces_dir)
        for filename, image in faces_loader.load():
            face_name = os.path.basename(filename)

            faces = faces_alignments.get_faces_in_frame(face_name)
            detected_faces = list()
            for rawface in faces:
                face = DetectedFace()
                face.from_alignment(rawface, image=image)

                feed_face = AlignedFace(face.landmarks_xy,
                                        image=image,
                                        centering='face',
                                        size=image.shape[0],
                                        coverage_ratio=1.0,
                                        dtype="float32")

                detected_faces.append(feed_face)

            self._faces[face_name] = (filename, image, detected_faces)

        print('Faces:', len(self._faces))
        print(self._faces.keys())
        print()

        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, self._faces, 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__)