          rename_mappings = self._build_rename_list()
          rename_count = self._rename_faces(rename_mappings)
          logger.info("%s faces renamed", rename_count)
  
      def _build_rename_list(self):
          """ Build a list of source and destination filenames for renaming.
  
          Validates that all files in the faces folder have a corresponding match in the alignments
          file. Orders the rename list by destination filename to avoid potential for filename clash.
  
          Returns
          -------
          list
              List of tuples of (`source filename`, `destination filename`) ordered by destination
              filename
          """
          source_filenames = []
          dest_filenames = []
          errors = []
          pbar = tqdm(desc="Building Rename Lists", total=self._faces.count)
          for disk_hash, disk_faces in self._faces.items.items():
              align_faces = self._alignments.hashes_to_frame.get(disk_hash, None)
              face_error = self._validate_hash_match(disk_faces, align_faces)
              if face_error is not None:
                  errors.extend(face_error)
                  pbar.update(len(disk_faces))
                  continue
              src_faces, dst_faces = self._get_filename_mapping(disk_faces, align_faces)
              source_filenames.extend(src_faces)
              dest_filenames.extend(dst_faces)
              pbar.update(len(src_faces))
          pbar.close()
          if errors:
              logger.error("There are faces in the given folder that do not correspond to entries "
                           "in the alignments file. Please check your data, and if neccesarry run "
                           "the `remove-faces` job. To get a list of faces missing alignments "
                           "entries, run with VERBOSE logging")
              logger.verbose("Files in faces folder not in alignments file: %s", errors)
              sys.exit(1)
          return self._sort_mappings(source_filenames, dest_filenames)
  
      @staticmethod
      def _validate_hash_match(disk_faces, align_faces):
          """ Validate that the hash has returned corresponding faces from disk and alignments file.
  
          Parameters
          ----------
          disk_faces: list
              List of tuples of (`file name`, `file extension`) for all faces that exist for the
              current hash
          align_faces: dict
              `frame filename`: `index` for all faces that exist in the alignments file for the
              current hash
  
          Returns
          -------
          list
              List of disk_faces that do not correspond to a matching entry in the alignments file.
              Returns `None` if there is a valid match
          """
          if align_faces is None:
              logger.debug("No matching hash found for faces: %s", disk_faces)
              return [face[0] + face[1] for face in disk_faces]
          if len(disk_faces) != len(align_faces):
              logger.debug("Number of faces mismatch for hash: (disk_faces: %s, align_faces: %s)",
                           disk_faces, align_faces)
              return [face[0] + face[1] for face in disk_faces[: len(align_faces)]]
          return None
  
      @staticmethod
      def _get_filename_mapping(disk_faces, align_faces):
          """ Map the source filenames for this hash to the destination filenames.
  
          Parameters
          ----------
          disk_faces: list
              List of tuples of (`file name`, `file extension`) for all faces that exist for the
              current hash
          align_faces: dict
              `frame filename`: `index` for all faces that exist in the alignments file for the
              current hash
  
          Returns
          -------
          source_filenames: list
              List of source filenames to be renamed for this hash
          dest_filenames: list
              List of destination filenames that faces for this hash are to be renamed to
              List of disk_faces that do not correspond to a matching entry in the alignments file.
              Returns `None` if there is a valid match
          """
          source_filenames = []
          dest_filenames = []
          # Force deterministic order on alignments dict for multi hash faces
          sorted_aligned = sorted(list(align_faces.items()))
          for disk_face, align_face in zip(disk_faces, sorted_aligned):
              extension = disk_face[1]
              src_fname = disk_face[0] + extension
  
              dst_frame = os.path.splitext(align_face[0])[0]
              dst_fname = "{}_{}{}".format(dst_frame, align_face[1], extension)
              logger.debug("Mapping rename from '%s' to '%s'", src_fname, dst_fname)
              source_filenames.append(src_fname)
              dest_filenames.append(dst_fname)
          return source_filenames, dest_filenames
  
      @staticmethod
      def _sort_mappings(sources, destinations):
          """ Sort the mapping lists by destinations to avoid filename clash.
  
          Parameters
          ----------
          sources: list
              List of source filenames in the same order as :attr:`destinations`
          destinations: dict
              List of destination filenames in the same order as :attr:`sources`
  
          Returns
          -------
          list
              List of tuples of (`source filename`, `destination filename`) ordered by destination
              filename
          """
          sorted_indices = [idx for idx, _ in sorted(enumerate(destinations), key=lambda x: x[1])]
          mappings = [(sources[idx], destinations[idx]) for idx in sorted_indices]
          logger.trace("filename mappings: %s", mappings)
          return mappings
  
      def _rename_faces(self, filename_mappings):
          """ Rename faces back to their original name as exists in the alignments file.
  
          If the source and destination filename are the same then skip that file.
  
 
</s> remove         rename_mappings = self._build_rename_list()
 </s> add         rename_mappings = sorted([(face["current_filename"], face["original_filename"])
                                  for face in self._faces.file_list_sorted
                                  if face["current_filename"] != face["original_filename"]],
                                 key=lambda x: x[1]) </s> remove class Merge():  # pylint:disable=too-few-public-methods
    """ Merge multiple alignments files into one.

    Parameters
    ----------
    alignments: :class:`tools.lib_alignments.media.AlignmentData`
        The alignments data loaded from an alignments file for this rename job
    arguments: :class:`argparse.Namespace`
        The :mod:`argparse` arguments as passed in from :mod:`tools.py`
    """
    def __init__(self, alignments, arguments):
        self._alignments = alignments
        self._check_versions()
        self._faces = self._get_faces(arguments)
        self._final_alignments = alignments[0]
        self._process_alignments = alignments[1:]
        self._hashes_to_frame = None

    def _check_versions(self):
        """ Ensure all alignments files are compatible versions. If not, exit with error. """
        versions = [al.version for al in self._alignments]
        logger.debug(versions)
        if any(vers < 2.0 for vers in versions) and any(vers >= 2.0 for vers in versions):
            logger.error("You have selected incompatible alignments files for merging. You cannot "
                         "merge alignments files for legacy extracted faces with aligments files "
                         "for full-head extracted faces.")
            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)

    @staticmethod
    def _get_faces(arguments):
        """ If faces argument is specified, load the faces folder otherwise return ``None``.

        Parameters
        ----------
        arguments: :class:`argparse.Namespace`
            The :mod:`argparse` arguments as passed in from :mod:`tools.py`

        Returns
        -------
        :class:`tools.alignments.media.Faces` or ``None``
            The faces object or ``None`` if not faces folder specified
        """
        if not hasattr(arguments, "faces_dir") or not arguments.faces_dir:
            return None
        return Faces(arguments.faces_dir)

    def process(self):
        """Run the alignments file merging process. """
        logger.info("[MERGE ALIGNMENTS]")  # Tidy up cli output
        if self._faces is not None:
            self._remove_faces()
        self._hashes_to_frame = self._final_alignments.hashes_to_frame
        skip_count = 0
        merge_count = 0
        total_count = sum([alignments.frames_count for alignments in self._process_alignments])

        with tqdm(desc="Merging Alignments", total=total_count) as pbar:
            for alignments in self._process_alignments:
                for _, src_alignments, _, frame in alignments.yield_faces():
                    for idx, alignment in enumerate(src_alignments):
                        if not alignment.get("hash", None):
                            logger.warning("Alignment '%s':%s has no Hash! Skipping", frame, idx)
                            skip_count += 1
                            continue
                        if self._check_exists(frame, alignment, idx):
                            skip_count += 1
                            continue
                        self._merge_alignment(frame, alignment, idx)
                        merge_count += 1
                    pbar.update(1)
        logger.info("Alignments Merged: %s", merge_count)
        logger.info("Alignments Skipped: %s", skip_count)
        if merge_count != 0:
            self._set_destination_filename()
            self._final_alignments.save()

    def _remove_faces(self):
        """ Removes faces from the alignments file if a faces folder has been provided and the
        faces do not exist within each alignments file. """
        face_hashes = list(self._faces.items.keys())
        del_faces_count = 0
        del_frames_count = 0
        if not face_hashes:
            logger.error("No face hashes. This would remove all faces from your alignments file.")
            return
        for alignments in tqdm(self._alignments, desc="Filtering out faces"):
            pre_face_count = alignments.faces_count
            pre_frames_count = alignments.frames_count
            alignments.filter_hashes(face_hashes, filter_out=False)
            # Remove frames with no faces
            frames = list(alignments.data.keys())
            for frame in frames:
                if not alignments.frame_has_faces(frame):
                    del alignments.data[frame]
            post_face_count = alignments.faces_count
            post_frames_count = alignments.frames_count
            removed_faces = pre_face_count - post_face_count
            removed_frames = pre_frames_count - post_frames_count
            del_faces_count += removed_faces
            del_frames_count += removed_frames
            logger.verbose("Removed %s faces and %s frames from %s",
                           removed_faces, removed_frames, os.path.basename(alignments.file))
        logger.info("Total removed - faces: %s, frames: %s", del_faces_count, del_frames_count)

    def _check_exists(self, frame, alignment, index):
        """ Remove duplicate faces from the alignments file when an instance of the face already
        exists.

        Parameters
        ----------
        frame: str
            The frame name for the current frame being processed
        alignment: dict
            The alignment dictionary for the current face in the frame
        index: int
            The face index for the current face in the frame

        Returns
        -------
        bool
            ``True`` if the face has been already been seen, otherwise ``False``
        """
        existing_frame = self._hashes_to_frame.get(alignment["hash"], None)
        if not existing_frame:
            return False
        if frame in existing_frame.keys():
            logger.verbose("Face '%s': %s already exists in destination at position %s. "
                           "Skipping", frame, index, existing_frame[frame])
        elif frame not in existing_frame.keys():
            logger.verbose("Face '%s': %s exists in destination as: %s. "
                           "Skipping", frame, index, existing_frame)
        return True

    def _merge_alignment(self, frame, alignment, idx):
        """ Merge the source alignment into the destination final alignments dictionary

        Parameters
        ----------
        frame: str
            The frame name for the current frame being processed
        alignment: dict
            The alignment dictionary for the current face in the frame
        index: int
            The face index for the current face in the frame
        """
        logger.debug("Merging alignment: (frame: %s, src_idx: %s, hash: %s)",
                     frame, idx, alignment["hash"])
        self._hashes_to_frame.setdefault(alignment["hash"], dict())[frame] = idx
        self._final_alignments.data.setdefault(frame,
                                               dict()).setdefault("faces", []).append(alignment)

    def _set_destination_filename(self):
        """ Set the destination filename """
        folder = os.path.split(self._final_alignments.file)[0]
        ext = os.path.splitext(self._final_alignments.file)[1]
        now = datetime.now().strftime("%Y%m%d_%H%M%S")
        filename = os.path.join(folder, "alignments_merged_{}{}".format(now, ext))
        logger.debug("Output set to: '%s'", filename)
        self._final_alignments.set_filename(filename)


 </s> add  </s> remove         input_sizes: dict
            The training image sizes in pixels for side `a` and `b` as they are saved on disk
 </s> add         output_warning: bool, optional
            If ``True`` outputs a warning that images are missing alignments data.

        Returns
        -------
        bool
            ``True`` if all images have valid metadata otherwise ``False``
        """
        all_valid = {side: all(val.values()) for side, val in self._detected_faces.items()}
        if all(all_valid.values()):
            return True
        if not output_warning:
            return False

        for side, valid in all_valid.items():
            if valid:
                continue
            invalid = [filename
                       for filename, meta in self._detected_faces[side].items() if not meta]

            logger.warning("Data for training side '%s' contains %s faces that do not contain "
                           "valid metadata and will be excluded from training.",
                           side.upper(), len(invalid))
            logger.warning("Run in VERBOSE mode if you wish to see a list of these files.")
            logger.verbose("Side '%s' images missing metadata: %s", side.upper(),
                           sorted(os.path.basename(fname) for fname in invalid))
            # Remove images without metadata
            self._detected_faces[side] = {key: val
                                          for key, val in self._detected_faces[side].items()
                                          if val}
        return False

    def _validate_masks(self):
        """ Validate the the loaded metadata all contain the masks required for training.

        Raises
        ------
        FaceswapError
            If at least one face in the training data does not contain the selected mask type
        """
        mask_type = self._config["mask_type"]
        invalid = {side: [filename for filename, detected_face in faces.items()
                          if mask_type not in detected_face.mask]
                   for side, faces in self._detected_faces.items()}
        if any(invalid.values()):
            msg = ("You have selected the Mask Type '{}' in your training configuration options "
                   "but at least one face does not have this mask type stored for it.\nYou should "
                   "select a mask type that exists within your face data, or generate the "
                   "required masks with the Mask Tool.".format(mask_type))
            for side, filenames in invalid.items():
                available = set(mask
                                for det_face in self._detected_faces[side].values()
                                for mask in det_face.mask)
                msg += ("\n{} faces in side {} do not contain the mask '{}'. Available "
                        "masks: {}".format(len(filenames), side.upper(), mask_type, available))
            raise FaceswapError(msg)

    # <<< LOAD REQUIRED DATA FOR TRAINING >>>
    def _get_aligned_faces(self):
        """ Pre-generate aligned faces as they are needed for all training functions. </s> remove         hash_list: list
            List of SHA1 hashes in `str` format to use as a filter against :attr:`data`
 </s> add         filter_dict: dict
            Dictionary of source filenames ask key with a list of face indices to filter as value. </s> remove             The alignments paths for each of the source and destination faces. Key is the
            side, value is the path to the alignments file
 </s> add             For keys "a" and "b" the values are a ``dict`` with the key being the filename of the
            training image and the value being the :class:`lib.align.DetectedFace` object
        """
        metadata = dict()
        for side, filelist in image_list.items():
            meta_side = dict()
            logger.debug("side: %s, file count: %s", side, len(filelist))
            for filename, meta in tqdm(read_image_meta_batch(filelist),
                                       desc="Reading training images ({})".format(side.upper()),
                                       total=len(filelist),
                                       leave=False):

                self._validate_image_size(side, filename, meta["width"], meta["height"])

                if "itxt" not in meta or "alignments" not in meta["itxt"]:
                    meta_side[filename] = None
                else:
                    alignments_version = meta["itxt"]["source"]["alignments_version"]
                    self._alignments_version.setdefault(side, set()).add(alignments_version)
                    detected_face = DetectedFace()
                    detected_face.from_png_meta(meta["itxt"]["alignments"])
                    meta_side[filename] = detected_face
            metadata[side] = meta_side
        return metadata

    def _validate_image_size(self, side, filename, width, height):
        """ Validate that the images are square and that the sizes for all image in a side are
        the same.

        Parameters
        ----------
        side: ["a" or "b"]
            The training side that is being processed
        filename: str
            The filename of the image that is being validated
        width: int
            The width of the image to be validated
        height: int
            The height of the image to be validated
          rename_mappings = self._build_rename_list()
          rename_count = self._rename_faces(rename_mappings)
          logger.info("%s faces renamed", rename_count)
  




























































































































      def _rename_faces(self, filename_mappings):
          """ Rename faces back to their original name as exists in the alignments file.
  
          If the source and destination filename are the same then skip that file.
  
keep keep keep keep replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace replace keep keep keep keep keep
 Update extracted faces to use PNG EXIF data (#1123)

Documentation
  - Update Usage.md, align.rst and image.rst
lib.image.py
  - read_image - Remove hash return, add metadata return
  - Remove read_image_hash functions
  - Add read_image_meta functios
  - Replace encode_image_with_hash with encode_image (to store metadata)
  - Add png meta reading and writing functions
  - Update Image Loaders/Savers to handle metadata rather than hashes
lib.training_data
  - Naming updates to remove references to hashes
lib.align.Alignments
  - Add versioning notes
  - Increment alignments version to 2.1
  - Deprecate hashing lookup functions
  - Replace filter_hashes with filter_faces
lib.align.detected_face
  - DetectedFace
    - Remove hash property
    - Add png header data serializing/deserializing functions
  - Mask
    - Add png header data serializing/deserializing functions
  - add update_legacy_png_header function to update png meta data
lib.cli.args - Deprecate alignments files for training
- plugins.train.trainer
  - Update alignments/mask code to read png header data
- scripts.convert
  - Aligned images folder - read data from png headers
- scripts.extract
  - Write png header information and no longer store hash of face
- tools.alignments
  - remove leftover-faces, merge and update-hashes jobs
  - Update jobs to use png meta data rather than hashes
- tools.manual
  - Update extract code to output png meta data and don't store hashes
  - Perform check on launch that tool is not pointing at a faces folder
tools.mask
  - Update to use png meta data
tools.sort
  - Update to use png meta data 
tools/alignments/jobs.py