              if len(face_hashes) <= len(self.input_images) / 3:
                  logger.warning("Aligned directory contains far fewer images than the input "
                                 "directory, are you sure this is the right folder?")
          return face_hashes
  
  
  class Legacy():
      """ Update legacy alignments:
          - Rotate landmarks and bounding boxes on legacy alignments
            and remove the 'r' parameter
          - Add face hashes to alignments file
          """
      def __init__(self, alignments, frames, faces_dir):
          self.alignments = alignments
          self.frames = {os.path.basename(frame): frame
                         for frame in frames}
          self.process(faces_dir)
  
      def process(self, faces_dir):
          """ Run the rotate alignments process """
          rotated = self.alignments.get_legacy_rotation()
          hashes = self.alignments.get_legacy_no_hashes()
          if not rotated and not hashes:
              return
          if rotated:
              logger.info("Legacy rotated frames found. Converting...")
              self.rotate_landmarks(rotated)
              self.alignments.save()
          if hashes and faces_dir:
              logger.info("Legacy alignments found. Adding Face Hashes...")
              self.add_hashes(hashes, faces_dir)
              self.alignments.save()
  
      def rotate_landmarks(self, rotated):
          """ Rotate the landmarks """
          for rotate_item in tqdm(rotated, desc="Rotating Landmarks"):
              frame = self.frames.get(rotate_item, None)
              if frame is None:
                  logger.debug("Skipping missing frame: '%s'", rotate_item)
                  continue
              self.alignments.rotate_existing_landmarks(rotate_item, frame)
  
      def add_hashes(self, hashes, faces_dir):
          """ Add Face Hashes to the alignments file """
          all_faces = dict()
          face_files = sorted(face for face in os.listdir(faces_dir) if "_" in face)
          for face in face_files:
              filename, extension = os.path.splitext(face)
              index = filename[filename.rfind("_") + 1:]
              if not index.isdigit():
                  continue
              orig_frame = filename[:filename.rfind("_")] + extension
              all_faces.setdefault(orig_frame, dict())[int(index)] = os.path.join(faces_dir, face)
  
          for frame in tqdm(hashes):
              if frame not in all_faces.keys():
                  logger.warning("Skipping missing frame: '%s'", frame)
                  continue
              hash_faces = all_faces[frame]
              for index, face_path in hash_faces.items():
                  hash_faces[index] = read_image_hash(face_path)
              self.alignments.add_face_hashes(frame, hash_faces)
 
</s> remove class Legacy():
    """ Update legacy alignments:
        - Rotate landmarks and bounding boxes on legacy alignments
          and remove the 'r' parameter
        - Add face hashes to alignments file
    """

    def __init__(self, alignments, arguments, frames=None, faces=None, child_process=False):
        logger.debug("Initializing %s: (arguments: %s, child_process: %s)",
                     self.__class__.__name__, arguments, child_process)
        self.alignments = alignments
        if child_process:
            self.frames = frames
            self.faces = faces
        else:
            self.frames = Frames(arguments.frames_dir)
            self.faces = Faces(arguments.faces_dir)
        logger.debug("Initialized %s", self.__class__.__name__)

    def process(self):
        """ Run the rotate alignments process """
        rotated = self.alignments.get_legacy_rotation()
        hashes = self.alignments.get_legacy_no_hashes()
        if (not self.frames or not rotated) and (not self.faces or not hashes):
            return
        logger.info("[UPDATE LEGACY LANDMARKS]")  # Tidy up cli output
        if rotated and self.frames:
            logger.info("Legacy rotated frames found. Converting...")
            self.rotate_landmarks(rotated)
            self.alignments.save()
        if hashes and self.faces:
            logger.info("Legacy alignments found. Adding Face Hashes...")
            self.add_hashes(hashes)
            self.alignments.save()

    def rotate_landmarks(self, rotated):
        """ Rotate the landmarks """
        for rotate_item in tqdm(rotated, desc="Rotating Landmarks"):
            frame = self.frames.get(rotate_item, None)
            if frame is None:
                continue
            self.alignments.rotate_existing_landmarks(rotate_item, frame)

    def add_hashes(self, hashes):
        """ Add Face Hashes to the alignments file """
        all_faces = dict()
        logger.info("Getting original filenames, indexes and hashes...")
        for face in self.faces.file_list_sorted:
            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
            all_faces.setdefault(orig_frame, dict())[int(index)] = face["face_hash"]

        logger.info("Updating hashes to alignments...")
        for frame in hashes:
            if frame not in all_faces.keys():
                logger.warning("Skipping missing frame: '%s'", frame)
                continue
            self.alignments.add_face_hashes(frame, all_faces[frame])


 </s> add  </s> remove     def check_legacy(self):
        """ Legacy rotated alignments will not have the correct x, y
            positions. Faces without hashes won't process.
            Check for these and generate a warning and exit """
        rotated = self.alignments.get_legacy_rotation()
        hashes = self.alignments.get_legacy_no_hashes()
        if rotated or hashes:
            logger.error("Legacy alignments found. Sort cannot continue. You should run legacy "
                         "tool to update the file prior to running sort: 'python tools.py "
                         "alignments -j legacy -a <alignments_file> -fr <frames_folder> -fc "
                         "<faces_folder>'")
            exit(0)

 </s> add  </s> remove     @staticmethod
    def get_original_rotation_matrix(dimensions, angle):
        """ Calculate original rotation matrix and invert """
        logger.trace("Getting original rotation matrix: (dimensions: %s, angle: %s)",
                     dimensions, angle)
        height, width = dimensions
        center = (width/2, height/2)
        r_mat = cv2.getRotationMatrix2D(  # pylint: disable=no-member
            center, -1.0 * angle, 1.)

        abs_cos = abs(r_mat[0, 0])
        abs_sin = abs(r_mat[0, 1])
        rotated_width = int(height*abs_sin + width*abs_cos)
        rotated_height = int(height*abs_cos + width*abs_sin)
        r_mat[0, 2] += rotated_width/2 - center[0]
        r_mat[1, 2] += rotated_height/2 - center[1]
        logger.trace("Returning rotation matrix: %s", r_mat)
        return r_mat

    # <Face Hashes> #
    # The old index based method of face matching is problematic.
    # The SHA1 Hash of the extracted face is now stored in the alignments file.
    # This has it's own issues, but they are far reduced from the index/filename method
    # This can eventually be removed
    def get_legacy_no_hashes(self):
        """ Get alignments without face hashes """
        logger.debug("Getting alignments without face hashes")
        keys = list()
        for key, val in self.data.items():
            for alignment in val:
                if "hash" not in alignment.keys():
                    keys.append(key)
                    break
        logger.debug("Got alignments without face hashes: %s", len(keys))
        return keys

    def add_face_hashes(self, frame_name, hashes):
        """ Backward compatability fix. Add face hash to alignments """
        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     # <File Format> #
    # Serializer is now a compressed pickle .fsa format. This used to be any number of serializers
    def update_file_format(self, folder, filename):
        """ Convert old style alignments format to new style format """
        logger.info("Reformatting legacy alignments file...")
        old_location = os.path.join(str(folder), filename)
        new_location = "{}.{}".format(os.path.splitext(old_location)[0],
                                      self.serializer.file_extension)
        logger.info("Old location: '%s', New location: '%s'", old_location, new_location)

        load_serializer = get_serializer_from_filename(old_location)
        data = load_serializer.load(old_location)
        self.serializer.save(new_location, data)
        return os.path.basename(new_location) </s> remove     # < Rotation > #
    # The old rotation method would rotate the image to find a face, then
    # store the rotated landmarks along with a rotation value to tell the
    # convert process that it had to rotate the frame to find the landmarks.
    # This is problematic for numerous reasons. The process now rotates the
    # landmarks to correctly correspond with the original frame. The below are
    # functions to convert legacy alignments to the currently supported
    # infrastructure.
    # This can eventually be removed

    def get_legacy_rotation(self):
        """ Return a list of frames with legacy rotations
            Looks for an 'r' value in the alignments file that
            is not zero """
        logger.debug("Getting alignments containing legacy rotations")
        keys = list()
        for key, val in self.data.items():
            if any(alignment.get("r", None) for alignment in val):
                keys.append(key)
        logger.debug("Got alignments containing legacy rotations: %s", len(keys))
        return keys

    def rotate_existing_landmarks(self, frame_name, frame):
        """ Backwards compatability fix. Rotates the landmarks to
            their correct position and deletes r

            NB: The original frame must be passed in otherwise
            the transformation cannot be performed """
        logger.trace("Rotating existing landmarks for frame: '%s'", frame_name)
        dims = frame.shape[:2]
        for face in self.get_faces_in_frame(frame_name):
            angle = face.get("r", 0)
            if not angle:
                logger.trace("Landmarks do not require rotation: '%s'", frame_name)
                return
            logger.trace("Rotating landmarks: (frame: '%s', angle: %s)", frame_name, angle)
            r_mat = self.get_original_rotation_matrix(dims, angle)
            rotate_landmarks(face, r_mat)
            del face["r"]
        logger.trace("Rotatated existing landmarks for frame: '%s'", frame_name)
 </s> add  </s> remove 

def rotate_landmarks(face, rotation_matrix):
    """ Rotates the 68 point landmarks and detection bounding box around the given rotation matrix.

    Parameters
    ----------
    face: DetectedFace or dict
        A :class:`DetectedFace` or an `alignments file` ``dict`` containing the 68 point landmarks
        and the `x`, `w`, `y`, `h` detection bounding box points.
    rotation_matrix: numpy.ndarray
        The rotation matrix to rotate the given object by.

    Returns
    -------
    DetectedFace or dict
        The rotated :class:`DetectedFace` or `alignments file` ``dict`` with the landmarks and
        detection bounding box points rotated by the given matrix. The return type is the same as
        the input type for ``face``
    """
    logger.trace("Rotating landmarks: (rotation_matrix: %s, type(face): %s",
                 rotation_matrix, type(face))
    rotated_landmarks = None
    # Detected Face Object
    if isinstance(face, DetectedFace):
        bounding_box = [[face.x, face.y],
                        [face.x + face.w, face.y],
                        [face.x + face.w, face.y + face.h],
                        [face.x, face.y + face.h]]
        landmarks = face.landmarks_xy

    # Alignments Dict
    elif isinstance(face, dict) and "x" in face:
        bounding_box = [[face.get("x", 0), face.get("y", 0)],
                        [face.get("x", 0) + face.get("w", 0),
                         face.get("y", 0)],
                        [face.get("x", 0) + face.get("w", 0),
                         face.get("y", 0) + face.get("h", 0)],
                        [face.get("x", 0),
                         face.get("y", 0) + face.get("h", 0)]]
        landmarks = face.get("landmarks_xy", list())

    else:
        raise ValueError("Unsupported face type")

    logger.trace("Original landmarks: %s", landmarks)

    rotation_matrix = cv2.invertAffineTransform(
        rotation_matrix)
    rotated = list()
    for item in (bounding_box, landmarks):
        if not item:
            continue
        points = np.array(item, np.int32)
        points = np.expand_dims(points, axis=0)
        transformed = cv2.transform(points,
                                    rotation_matrix).astype(np.int32)
        rotated.append(transformed.squeeze())

    # Bounding box should follow x, y planes, so get min/max
    # for non-90 degree rotations
    pt_x = min([pnt[0] for pnt in rotated[0]])
    pt_y = min([pnt[1] for pnt in rotated[0]])
    pt_x1 = max([pnt[0] for pnt in rotated[0]])
    pt_y1 = max([pnt[1] for pnt in rotated[0]])
    width = pt_x1 - pt_x
    height = pt_y1 - pt_y

    if isinstance(face, DetectedFace):
        face.x = int(pt_x)
        face.y = int(pt_y)
        face.w = int(width)
        face.h = int(height)
        face.r = 0
        if len(rotated) > 1:
            rotated_landmarks = [tuple(point) for point in rotated[1].tolist()]
            face.landmarks_xy = rotated_landmarks
    else:
        face["left"] = int(pt_x)
        face["top"] = int(pt_y)
        face["right"] = int(pt_x1)
        face["bottom"] = int(pt_y1)
        rotated_landmarks = face

    logger.trace("Rotated landmarks: %s", rotated_landmarks)
    return face
 </s> add 
              if len(face_hashes) <= len(self.input_images) / 3:
                  logger.warning("Aligned directory contains far fewer images than the input "
                                 "directory, are you sure this is the right folder?")
          return face_hashes


























































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
 Legacy alignments update

- Remove legacy update hashes
- Remove legacy job from alignment-tools
- Remove legacy landmark rotation
- Add rotate face method to plugins/extract/detect
- Update travis test for new alignments extension
- Alignments format to .fsa
- Remove serializer option from alignments-tool
- Auto update legacy format alignment files to new format 
scripts/convert.py