              # pylint:disable=protected-access
              mask._affine_matrix = detected_face.mask["components"].affine_matrix
  
  
  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)
  
  
  class RemoveFaces():  # pylint:disable=too-few-public-methods
      """ Remove items from alignments file.
  
      Parameters
      ---------
 
</s> remove     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

 </s> add  </s> remove     """ Sort alignments' index by the order they appear in an image """
 </s> add     """ Sort alignments' index by the order they appear in an image in left to right order.

    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`
    """ </s> remove 

class UpdateHashes():
    """ Update hashes in an alignments file """
    def __init__(self, alignments, arguments):
        logger.debug("Initializing %s: (arguments: %s)", self.__class__.__name__, arguments)
        self._alignments = alignments
        self.faces = Faces(arguments.faces_dir).file_list_sorted
        self.face_hashes = dict()
        logger.debug("Initialized %s", self.__class__.__name__)

    def process(self):
        """ Update Face Hashes to the alignments file """
        logger.info("[UPDATE FACE HASHES]")  # Tidy up cli output
        self.get_hashes()
        updated = self.update_hashes()
        if updated == 0:
            logger.info("No hashes were updated. Exiting")
            return
        self._alignments.save()
        logger.info("%s frame(s) had their face hashes updated.", updated)

    def get_hashes(self):
        """ Read the face hashes from the faces """
        logger.info("Getting original filenames, indexes and hashes...")
        for face in self.faces:
            filename = face["face_name"]
            extension = face["face_extension"]
            if "_" not in face["face_name"]:
                logger.warning("Unable to determine index of file. Skipping: '%s'", filename)
                continue
            index = filename[filename.rfind("_") + 1:]
            if not index.isdigit():
                logger.warning("Unable to determine index of file. Skipping: '%s'", filename)
                continue
            orig_frame = filename[:filename.rfind("_")] + extension
            self.face_hashes.setdefault(orig_frame, dict())[int(index)] = face["face_hash"]

    def update_hashes(self):
        """ Update hashes to alignments """
        logger.info("Updating hashes to alignments...")
        updated = 0
        for frame, hashes in self.face_hashes.items():
            if not self._alignments.frame_exists(frame):
                logger.warning("Frame not found in alignments file. Skipping: '%s'", frame)
                continue
            if not self._alignments.frame_has_faces(frame):
                logger.warning("Frame does not have faces. Skipping: '%s'", frame)
                continue
            existing = [face.get("hash", None)
                        for face in self._alignments.get_faces_in_frame(frame)]
            if any(hsh not in existing for hsh in list(hashes.values())):
                self._alignments.add_face_hashes(frame, hashes)
                updated += 1
        return updated
 </s> add  </s> remove     def add_face_hashes(self, frame_name, hashes):
        """ Recalculate face hashes """
        logger.trace("Adding face hash: (frame: '%s', hashes: %s)", frame_name, hashes)
        faces = self.get_faces_in_frame(frame_name)
        count_match = len(faces) - len(hashes)
        if count_match != 0:
            msg = "more" if count_match > 0 else "fewer"
            logger.warning("There are %s %s face(s) in the alignments file than exist in the "
                           "faces folder. Check your sources for frame '%s'.",
                           abs(count_match), msg, frame_name)
        for idx, i_hash in hashes.items():
            faces[idx]["hash"] = i_hash

 </s> add  </s> remove             for frame, idx in self._alignments.hashes_to_frame[face.hash].items():
                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)
 </s> add             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)
              # pylint:disable=protected-access
              mask._affine_matrix = detected_face.mask["components"].affine_matrix
  
  



































































































































































  class RemoveFaces():  # pylint:disable=too-few-public-methods
      """ Remove items from alignments file.
  
      Parameters
      ---------
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 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