          if self._is_legacy:
              faces = self._process_legacy(filename, image, faces)
  
          for idx, face in enumerate(faces):
              output = "{}_{}{}".format(frame_name, str(idx), extension)
              if self._arguments.large:
                  self._frames.save_image(self._faces_dir, output, face.aligned.face)
              else:
                  output = os.path.join(self._faces_dir, output)
                  f_hash = self._extracted_faces.save_face_with_hash(output,
                                                                     extension,
                                                                     face.aligned.face)
                  if self._is_legacy:  # Generate the new thumbnail and store new face data for save
                      face.thumbnail = generate_thumbnail(face.aligned.face, size=96, quality=60)
                      self._alignments.data[filename]["faces"][idx] = face.to_alignment()
                  self._alignments.data[filename]["faces"][idx]["hash"] = f_hash
              face_count += 1
          return face_count
  
      def _select_valid_faces(self, frame, image):
          """ Return the aligned faces from a frame that meet the selection criteria,
 
</s> remove         extension = ".png"
 </s> add  </s> remove             frames = self._alignments.hashes_to_frame[hsh]
            if len(frames) > 1:
                # Filter the output by filename in case of multiple frames with the same face
                logger.debug("Filtering multiple hashes to current filename: (filename: '%s', "
                             "frames: %s", filename, frames)
                lookup = os.path.splitext(os.path.basename(filename))[0]
                frames = {k: v
                          for k, v in frames.items()
                          if lookup.startswith(os.path.splitext(k)[0])}
                logger.debug("Filtered: (filename: '%s', frame: '%s')", filename, frames)

            for frame, idx in frames.items():
                self._counts["face"] += 1
                alignment = self._alignments.get_faces_in_frame(frame)[idx]
                if self._check_for_missing(frame, idx, alignment):
                    continue
                detected_face = self._get_detected_face(alignment)
                if self._update_type == "output":
                    detected_face.image = image
                    self._save(frame, idx, detected_face)
                else:
                    queue.put(ExtractMedia(filename, image, detected_faces=[detected_face]))
                    self._counts["update"] += 1
 </s> add             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 </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             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)
            output = "{}_{}.png".format(frame_name, str(idx))
            meta = dict(alignments=face.to_png_meta(),
                        source=dict(alignments_version=self._alignments.version,
                                    original_filename=output,
                                    face_index=idx,
                                    source_filename=filename,
                                    source_is_video=self._frames.is_video))
            self._saver.save(output, encode_image(face.aligned.face, ".png", metadata=meta))
            if not self._arguments.large and self._is_legacy:
                face.thumbnail = generate_thumbnail(face.aligned.face, size=96, quality=60)
                self._alignments.data[filename]["faces"][idx] = face.to_alignment()
          if self._is_legacy:
              faces = self._process_legacy(filename, image, faces)
  
          for idx, face in enumerate(faces):
            output = "{}_{}.png".format(frame_name, str(idx))
            meta = dict(alignments=face.to_png_meta(),
                        source=dict(alignments_version=self._alignments.version,
                                    original_filename=output,
                                    face_index=idx,
                                    source_filename=filename,
                                    source_is_video=self._frames.is_video))
            self._saver.save(output, encode_image(face.aligned.face, ".png", metadata=meta))
            if not self._arguments.large and self._is_legacy:
                face.thumbnail = generate_thumbnail(face.aligned.face, size=96, quality=60)
                self._alignments.data[filename]["faces"][idx] = face.to_alignment()
            output = "{}_{}.png".format(frame_name, str(idx))
            meta = dict(alignments=face.to_png_meta(),
                        source=dict(alignments_version=self._alignments.version,
                                    original_filename=output,
                                    face_index=idx,
                                    source_filename=filename,
                                    source_is_video=self._frames.is_video))
            self._saver.save(output, encode_image(face.aligned.face, ".png", metadata=meta))
            if not self._arguments.large and self._is_legacy:
                face.thumbnail = generate_thumbnail(face.aligned.face, size=96, quality=60)
                self._alignments.data[filename]["faces"][idx] = face.to_alignment()
            output = "{}_{}.png".format(frame_name, str(idx))
            meta = dict(alignments=face.to_png_meta(),
                        source=dict(alignments_version=self._alignments.version,
                                    original_filename=output,
                                    face_index=idx,
                                    source_filename=filename,
                                    source_is_video=self._frames.is_video))
            self._saver.save(output, encode_image(face.aligned.face, ".png", metadata=meta))
            if not self._arguments.large and self._is_legacy:
                face.thumbnail = generate_thumbnail(face.aligned.face, size=96, quality=60)
                self._alignments.data[filename]["faces"][idx] = face.to_alignment()
            output = "{}_{}.png".format(frame_name, str(idx))
            meta = dict(alignments=face.to_png_meta(),
                        source=dict(alignments_version=self._alignments.version,
                                    original_filename=output,
                                    face_index=idx,
                                    source_filename=filename,
                                    source_is_video=self._frames.is_video))
            self._saver.save(output, encode_image(face.aligned.face, ".png", metadata=meta))
            if not self._arguments.large and self._is_legacy:
                face.thumbnail = generate_thumbnail(face.aligned.face, size=96, quality=60)
                self._alignments.data[filename]["faces"][idx] = face.to_alignment()
            output = "{}_{}.png".format(frame_name, str(idx))
            meta = dict(alignments=face.to_png_meta(),
                        source=dict(alignments_version=self._alignments.version,
                                    original_filename=output,
                                    face_index=idx,
                                    source_filename=filename,
                                    source_is_video=self._frames.is_video))
            self._saver.save(output, encode_image(face.aligned.face, ".png", metadata=meta))
            if not self._arguments.large and self._is_legacy:
                face.thumbnail = generate_thumbnail(face.aligned.face, size=96, quality=60)
                self._alignments.data[filename]["faces"][idx] = face.to_alignment()
            output = "{}_{}.png".format(frame_name, str(idx))
            meta = dict(alignments=face.to_png_meta(),
                        source=dict(alignments_version=self._alignments.version,
                                    original_filename=output,
                                    face_index=idx,
                                    source_filename=filename,
                                    source_is_video=self._frames.is_video))
            self._saver.save(output, encode_image(face.aligned.face, ".png", metadata=meta))
            if not self._arguments.large and self._is_legacy:
                face.thumbnail = generate_thumbnail(face.aligned.face, size=96, quality=60)
                self._alignments.data[filename]["faces"][idx] = face.to_alignment()
            output = "{}_{}.png".format(frame_name, str(idx))
            meta = dict(alignments=face.to_png_meta(),
                        source=dict(alignments_version=self._alignments.version,
                                    original_filename=output,
                                    face_index=idx,
                                    source_filename=filename,
                                    source_is_video=self._frames.is_video))
            self._saver.save(output, encode_image(face.aligned.face, ".png", metadata=meta))
            if not self._arguments.large and self._is_legacy:
                face.thumbnail = generate_thumbnail(face.aligned.face, size=96, quality=60)
                self._alignments.data[filename]["faces"][idx] = face.to_alignment()
            output = "{}_{}.png".format(frame_name, str(idx))
            meta = dict(alignments=face.to_png_meta(),
                        source=dict(alignments_version=self._alignments.version,
                                    original_filename=output,
                                    face_index=idx,
                                    source_filename=filename,
                                    source_is_video=self._frames.is_video))
            self._saver.save(output, encode_image(face.aligned.face, ".png", metadata=meta))
            if not self._arguments.large and self._is_legacy:
                face.thumbnail = generate_thumbnail(face.aligned.face, size=96, quality=60)
                self._alignments.data[filename]["faces"][idx] = face.to_alignment()
            output = "{}_{}.png".format(frame_name, str(idx))
            meta = dict(alignments=face.to_png_meta(),
                        source=dict(alignments_version=self._alignments.version,
                                    original_filename=output,
                                    face_index=idx,
                                    source_filename=filename,
                                    source_is_video=self._frames.is_video))
            self._saver.save(output, encode_image(face.aligned.face, ".png", metadata=meta))
            if not self._arguments.large and self._is_legacy:
                face.thumbnail = generate_thumbnail(face.aligned.face, size=96, quality=60)
                self._alignments.data[filename]["faces"][idx] = face.to_alignment()
            output = "{}_{}.png".format(frame_name, str(idx))
            meta = dict(alignments=face.to_png_meta(),
                        source=dict(alignments_version=self._alignments.version,
                                    original_filename=output,
                                    face_index=idx,
                                    source_filename=filename,
                                    source_is_video=self._frames.is_video))
            self._saver.save(output, encode_image(face.aligned.face, ".png", metadata=meta))
            if not self._arguments.large and self._is_legacy:
                face.thumbnail = generate_thumbnail(face.aligned.face, size=96, quality=60)
                self._alignments.data[filename]["faces"][idx] = face.to_alignment()
            output = "{}_{}.png".format(frame_name, str(idx))
            meta = dict(alignments=face.to_png_meta(),
                        source=dict(alignments_version=self._alignments.version,
                                    original_filename=output,
                                    face_index=idx,
                                    source_filename=filename,
                                    source_is_video=self._frames.is_video))
            self._saver.save(output, encode_image(face.aligned.face, ".png", metadata=meta))
            if not self._arguments.large and self._is_legacy:
                face.thumbnail = generate_thumbnail(face.aligned.face, size=96, quality=60)
                self._alignments.data[filename]["faces"][idx] = face.to_alignment()
            output = "{}_{}.png".format(frame_name, str(idx))
            meta = dict(alignments=face.to_png_meta(),
                        source=dict(alignments_version=self._alignments.version,
                                    original_filename=output,
                                    face_index=idx,
                                    source_filename=filename,
                                    source_is_video=self._frames.is_video))
            self._saver.save(output, encode_image(face.aligned.face, ".png", metadata=meta))
            if not self._arguments.large and self._is_legacy:
                face.thumbnail = generate_thumbnail(face.aligned.face, size=96, quality=60)
                self._alignments.data[filename]["faces"][idx] = face.to_alignment()
              face_count += 1
          return face_count
  
      def _select_valid_faces(self, frame, image):
          """ Return the aligned faces from a frame that meet the selection criteria,
keep keep keep keep 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