          """ Pre-generate aligned faces as they are needed for all training functions.
  
          Parameters
          ----------
          input_sizes: dict
              The training image sizes in pixels for side `a` and `b` as they are saved on disk
  
          Returns
          -------
          dict
              The "a", "b" keys for each side, containing a sub-dictionary with the
 
</s> remove     def _get_aligned_faces(self, input_sizes):
        """ Pre-generate aligned faces as they are needed for all training functions.
 </s> add         # Validate image is the same size as the other images for the side
        if width != self._image_sizes[side]:
            msg = ("All training images for each side must be of the same size.\nImages "
                   "in side '{}' have mismatched sizes {} and {}.\nPlease double check "
                   "your dataset".format(side.upper(), self._image_sizes[side], width))
            raise FaceswapError(msg)

    def _update_legacy_facesets(self):
        """ Update the png header data for legacy face sets that do not contain the meta data in
        the exif header.
        """
        if self._validate_metadata(output_warning=False):
            logger.debug("All faces contain valid header information")
            return

        for side, png_meta in self._detected_faces.items():
            if all(png_meta.values()):
                continue
            filenames = [filename for filename, meta in png_meta.items() if not meta]
            logger.info("Legacy faces discovered for side '%s'. Updating %s images...",
                        side.upper(), len(filenames))
            alignments = Alignments(*os.path.split(self._get_alignments_path(side)))
            self._alignments_version.setdefault(side, set()).add(alignments.version)

            executor = futures.ThreadPoolExecutor()
            with executor:
                images = {executor.submit(update_legacy_png_header, filename, alignments): filename
                          for filename in filenames}

                for future in tqdm(
                        futures.as_completed(images),
                        desc="Updating legacy training images ({})".format(side.upper()),
                        total=len(filenames),
                        leave=False):
                    detected_face = DetectedFace()
                    detected_face.from_png_meta(future.result())
                    png_meta[images[future]] = detected_face

    def _get_alignments_path(self, side):
        """ Obtain the path to an alignments file for the given training side.

        Used for updating legacy facesets to contain the meta information within the image header

        Parameters
        ----------
        side: ["a" or "b"]
            The training side to obtain the alignments file for.

        Returns
        -------
        str
            The full path to the training alignments file

        Raises
        ------
        FaceswapError
            If an alignments file cannot be located
        """
        alignments_path = getattr(self._args, "alignments_path_{}".format(side))
        if not alignments_path:
            image_path = getattr(self._args, "input_{}".format(side))
            alignments_path = os.path.join(image_path, "alignments.fsa")
        if not os.path.exists(alignments_path):
            msg = ("You are using a legacy faceset that does not contain embedded "
                   "meta-information. An alignments file must be provided so that these files can "
                   "be updated.\n"
                   f"Alignments file does not exist: '{alignments_path}'")
            raise FaceswapError(msg)
        return alignments_path

    # <<< VALIDATE LOADED DETECTED FACE INFORMATION >>>
    def _validity_check(self):
        """ Check the validity of the finally loaded data.

        Ensure that each side has consistent alignments versions.
        Ensure that each side has a full compliment of metadata.
        """
        invalid = [side.upper()
                   for side, version in self._alignments_version.items() if len(version) > 1]
        if invalid:
            raise FaceswapError("Mixing legacy and full head extracted facesets is not supported. "
                                "The following side(s) contain a mix of extracted face "
                                "types: {}".format(invalid))
        # Replace check alignments version sets with actual floats
        self._alignments_version = {key: val.pop()
                                    for key, val in self._alignments_version.items()}

        if 1.0 in self._alignments_version.values() and self._config["centering"] != "legacy":
            logger.warning("You are using legacy extracted faces but have selected '%s' "
                           "centering which is incompatible. Switching centering to 'legacy'",
                           self._config["centering"])
            self._config["centering"] = "legacy"

        self._validate_metadata(output_warning=True)
        self._validate_masks()

    def _validate_metadata(self, output_warning=True):
        """ Validate that all images to be trained on have associated alignments data. If not
        generate a warning. </s> remove             filename as key and :class:`lib.faces.detected_face.AlignedFace` object as value.
 </s> add             filename as key and :class:`lib.align.AlignedFace` object as value. </s> remove     @property
    def aligned_faces(self):
        """ dict: The "a", "b" keys for each side, containing a sub-dictionary with the
        filename as key and :class:`lib.faces.detected_face.aligned` object as value. """
        return self._aligned_faces

 </s> add  </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
        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.
          """ Pre-generate aligned faces as they are needed for all training functions.
  
          Parameters
          ----------
        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.
        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.
  
          Returns
          -------
          dict
              The "a", "b" keys for each side, containing a sub-dictionary with the
keep keep keep keep 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 
plugins/train/trainer/_base.py