          logger.debug("Re-loading alignments")
          self._data = self._load()
          logger.debug("Re-loaded alignments")
  
      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
  
      def set_filename(self, filename):
          """ Set the :attr:`_file` to the given filename.
  
          Parameters
          ----------
 
</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 

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             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) </s> remove         seen_hash_dupes = set()
        for item in tqdm(self.items, desc=self.output_message):
            filename = item["face_fullname"]
            f_hash = item["face_hash"]
            frame_idx = list(self._alignments.hashes_to_frame[f_hash].items())

            if len(frame_idx) > 1:
                # If the same hash exists in multiple frames, select arbitrary frame
                # and add to seen_hash_dupes so it is not selected again
                logger.trace("Dupe hashes: %s", frame_idx)
                frame_idx = [f_i for f_i in frame_idx if f_i not in seen_hash_dupes][0]
                seen_hash_dupes.add(frame_idx)
                frame_idx = [frame_idx]

            frame_name, idx = frame_idx[0]
            if not self._alignments.frame_has_multiple_faces(frame_name):
 </s> add         for item in tqdm(self._items, desc=self.output_message):
            if not self._alignments.frame_has_multiple_faces(item["source_filename"]): </s> remove         """ Export the faces to the output folder and update the alignments file with
        new hashes. """
 </s> add         """ Export the faces to the output folder. """
          logger.debug("Re-loading alignments")
          self._data = self._load()
          logger.debug("Re-loaded alignments")
  













      def set_filename(self, filename):
          """ Set the :attr:`_file` to the given filename.
  
          Parameters
          ----------
keep keep keep keep 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/media.py