          ------
          FaceswapError
              If at least one alignments file does not exist
          """
          retval = dict()
          for side in ("a", "b"):
              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):
                  raise FaceswapError("Alignments file does not exist: `{}`".format(alignments_path))
              retval[side] = alignments_path
          logger.debug("Alignments paths: %s", retval)
          return retval
  
      def _get_aligned_faces(self, input_sizes):
          """ Pre-generate aligned faces as they are needed for all training functions.
  
          Parameters
 
</s> remove             If at least one alignments file does not exist
 </s> add             If the image to be checked is not square or is of a different size of any other image
            for the current side, an error is raised. </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         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             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 </s> remove             retval = (filename, idx)
 </s> add             retval = (item["current_filename"], item["face_index"])
        # Add the image size to the sizes dictionary if this is the first image
        if not self._image_sizes[side]:
            self._image_sizes[side] = width

        # Validate image is square
        if width != height:
            msg = ("Training images must be created by the extraction process and must be "
                   "square.\nThe image '{}' has dimensions {}x{} so the process cannot "
                   "continue.\nThere may be more images with these issues. Please double "
                   "check your dataset".format(filename, width, height))
            raise FaceswapError(msg)
          ------
          FaceswapError
              If at least one alignments file does not exist
          """
        # Add the image size to the sizes dictionary if this is the first image
        if not self._image_sizes[side]:
            self._image_sizes[side] = width

        # Validate image is square
        if width != height:
            msg = ("Training images must be created by the extraction process and must be "
                   "square.\nThe image '{}' has dimensions {}x{} so the process cannot "
                   "continue.\nThere may be more images with these issues. Please double "
                   "check your dataset".format(filename, width, height))
            raise FaceswapError(msg)
        # Add the image size to the sizes dictionary if this is the first image
        if not self._image_sizes[side]:
            self._image_sizes[side] = width

        # Validate image is square
        if width != height:
            msg = ("Training images must be created by the extraction process and must be "
                   "square.\nThe image '{}' has dimensions {}x{} so the process cannot "
                   "continue.\nThere may be more images with these issues. Please double "
                   "check your dataset".format(filename, width, height))
            raise FaceswapError(msg)
        # Add the image size to the sizes dictionary if this is the first image
        if not self._image_sizes[side]:
            self._image_sizes[side] = width

        # Validate image is square
        if width != height:
            msg = ("Training images must be created by the extraction process and must be "
                   "square.\nThe image '{}' has dimensions {}x{} so the process cannot "
                   "continue.\nThere may be more images with these issues. Please double "
                   "check your dataset".format(filename, width, height))
            raise FaceswapError(msg)
        # Add the image size to the sizes dictionary if this is the first image
        if not self._image_sizes[side]:
            self._image_sizes[side] = width

        # Validate image is square
        if width != height:
            msg = ("Training images must be created by the extraction process and must be "
                   "square.\nThe image '{}' has dimensions {}x{} so the process cannot "
                   "continue.\nThere may be more images with these issues. Please double "
                   "check your dataset".format(filename, width, height))
            raise FaceswapError(msg)
        # Add the image size to the sizes dictionary if this is the first image
        if not self._image_sizes[side]:
            self._image_sizes[side] = width

        # Validate image is square
        if width != height:
            msg = ("Training images must be created by the extraction process and must be "
                   "square.\nThe image '{}' has dimensions {}x{} so the process cannot "
                   "continue.\nThere may be more images with these issues. Please double "
                   "check your dataset".format(filename, width, height))
            raise FaceswapError(msg)
        # Add the image size to the sizes dictionary if this is the first image
        if not self._image_sizes[side]:
            self._image_sizes[side] = width

        # Validate image is square
        if width != height:
            msg = ("Training images must be created by the extraction process and must be "
                   "square.\nThe image '{}' has dimensions {}x{} so the process cannot "
                   "continue.\nThere may be more images with these issues. Please double "
                   "check your dataset".format(filename, width, height))
            raise FaceswapError(msg)
        # Add the image size to the sizes dictionary if this is the first image
        if not self._image_sizes[side]:
            self._image_sizes[side] = width

        # Validate image is square
        if width != height:
            msg = ("Training images must be created by the extraction process and must be "
                   "square.\nThe image '{}' has dimensions {}x{} so the process cannot "
                   "continue.\nThere may be more images with these issues. Please double "
                   "check your dataset".format(filename, width, height))
            raise FaceswapError(msg)
        # Add the image size to the sizes dictionary if this is the first image
        if not self._image_sizes[side]:
            self._image_sizes[side] = width

        # Validate image is square
        if width != height:
            msg = ("Training images must be created by the extraction process and must be "
                   "square.\nThe image '{}' has dimensions {}x{} so the process cannot "
                   "continue.\nThere may be more images with these issues. Please double "
                   "check your dataset".format(filename, width, height))
            raise FaceswapError(msg)
        # Add the image size to the sizes dictionary if this is the first image
        if not self._image_sizes[side]:
            self._image_sizes[side] = width

        # Validate image is square
        if width != height:
            msg = ("Training images must be created by the extraction process and must be "
                   "square.\nThe image '{}' has dimensions {}x{} so the process cannot "
                   "continue.\nThere may be more images with these issues. Please double "
                   "check your dataset".format(filename, width, height))
            raise FaceswapError(msg)
        # Add the image size to the sizes dictionary if this is the first image
        if not self._image_sizes[side]:
            self._image_sizes[side] = width

        # Validate image is square
        if width != height:
            msg = ("Training images must be created by the extraction process and must be "
                   "square.\nThe image '{}' has dimensions {}x{} so the process cannot "
                   "continue.\nThere may be more images with these issues. Please double "
                   "check your dataset".format(filename, width, height))
            raise FaceswapError(msg)
        # Add the image size to the sizes dictionary if this is the first image
        if not self._image_sizes[side]:
            self._image_sizes[side] = width

        # Validate image is square
        if width != height:
            msg = ("Training images must be created by the extraction process and must be "
                   "square.\nThe image '{}' has dimensions {}x{} so the process cannot "
                   "continue.\nThere may be more images with these issues. Please double "
                   "check your dataset".format(filename, width, height))
            raise FaceswapError(msg)
  
      def _get_aligned_faces(self, input_sizes):
          """ Pre-generate aligned faces as they are needed for all training functions.
  
          Parameters
keep keep keep keep 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 
plugins/train/trainer/_base.py