                      "\nL|'missing-alignments': Identify frames that do not exist in the "
                      "alignments file." + output_opts + frames_dir +
                      "\nL|'missing-frames': Identify frames in the alignments file that do not "
                      "appear within the frames folder/video." + output_opts + frames_dir +
                      "\nL|'legacy': This updates legacy alignments to the latest format by "
                      "rotating the landmarks and bounding boxes and adding face_hashes." +
                      frames_and_faces_dir +
                      "\nL|'leftover-faces': Identify faces in the faces folder that do not exist "
                      "in the alignments file." + output_opts + faces_dir +
                      "\nL|'multi-faces': Identify where multiple faces exist within the alignments "
                      "file." + output_opts + frames_or_faces_dir +
                      "\nL|'no-faces': Identify frames that exist within the alignment file but no "
 
</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  </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, 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> add  </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     @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         argument_list.append({"opts": ("-fmt", "--alignment_format"),
                              "type": str,
                              "choices": ("json", "pickle", "yaml"),
                              "group": "data",
                              "help": "The file format to save the alignment "
                                      "data in. Defaults to same as source."})
 </s> add 
                      "\nL|'missing-alignments': Identify frames that do not exist in the "
                      "alignments file." + output_opts + frames_dir +
                      "\nL|'missing-frames': Identify frames in the alignments file that do not "
                      "appear within the frames folder/video." + output_opts + frames_dir +



                      "\nL|'leftover-faces': Identify faces in the faces folder that do not exist "
                      "in the alignments file." + output_opts + faces_dir +
                      "\nL|'multi-faces': Identify where multiple faces exist within the alignments "
                      "file." + output_opts + frames_or_faces_dir +
                      "\nL|'no-faces': Identify frames that exist within the alignment file but no "
keep keep keep keep replace replace replace keep keep keep keep keep
 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 
tools/cli.py