import cv2
import numpy as np
import tensorflow as tf
import math
import os


class BlazeFace:
    """
    Detects faces in image and returns scores, keypoints, and bounding boxes.

    Methods
    -------
    initialize_model(self, model)
        initializes tensorflow lite interpreter

    get_model_details(self)
        extracts model details

    generate_ssd_anchors(self, model)
        Generates anchors for SSD model

    generate_blazeface_anchors(self, model)
        Generates anchors for BlazeFace model

    preprocess(self, img)
        converts format, resizes, and normalizes input image, returns a tensor of the right dimensions

    inference(self, input_tensor)


    threshold_detections(self, output1)


    extract_boxes_keypoints(self, output0, indices)


    non_max_suppression(self, boxes, keypoints, scores)


    weighted_mean(self, boxes, keypoints, scores)


    detect(self, img)
        detects faces in image and returns score, keypoints, and bounding boxes

    annotate(self, img, results)
        annotates image with score, keypoints, and bounding boxes

    testSelf(self)
        tests FaceDetector class
    """

    def __init__(
        self,
        model="back",
        score_threshold=0.70,
        iou_threshold=0.30,
        number_keypoints=6,
        max_faces=10,
        postprocess="weighted_mean",
    ):
        """
        Constructs BlazeFace detector.

        Parameters
        ----------
        model: string
            BlazeFace tflite model to be used, available models are "front", back", and "short_range"

        score_threshold: float
            minimum score to be considered as valid

        iou_threshold: float
            maximum intersection over union for two bounding boxes to be considered the same face

        number_keypoints: int
            number of keypoints to be returned, maximum of 6

        max_faces: int
            maximum number of faces to be returned

        postprocess: string
            method to be used for anchor postprocessing, either "non_max_suppression" or "weighted_mean"
        """
        self.model = model
        self.score_threshold = score_threshold
        self.score_threshold_raw = np.log(
            self.score_threshold / (1 - self.score_threshold)
        )
        self.iou_threshold = iou_threshold
        self.number_keypoints = number_keypoints
        self.max_faces = max_faces
        self.postprocess = postprocess

        # Initialize model
        self.initialize_model(model)

        # Get model details
        self.get_model_details()

        # Generate anchors
        # self.generate_ssd_anchors(model)
        self.generate_blazeface_anchors(model)

    def initialize_model(self, model):
        """
        Initializes tensorflow lite interpreter.

        Parameters
        ----------
        model: string
            BlazeFace tflite model to be used, available models are "front", back", and "short_range"
        """
        path = os.path.dirname(__file__)
        if model == "front":
            self.interpreter = tf.lite.Interpreter(
                model_path=f"{path}/face_detection_front.tflite"
            )
        elif model == "back":
            self.interpreter = tf.lite.Interpreter(
                model_path=f"{path}/face_detection_back.tflite"
            )
        elif model == "short_range":
            self.interpreter = tf.lite.Interpreter(
                model_path=f"{path}/face_detection_short_range.tflite"
            )
        self.interpreter.allocate_tensors()

    def get_model_details(self):
        """Extracts model details."""
        self.input_details = self.interpreter.get_input_details()
        input_shape = self.input_details[0]["shape"]
        self.input_height = input_shape[1]
        self.input_width = input_shape[2]
        self.channels = input_shape[3]
        self.output_details = self.interpreter.get_output_details()

    def generate_ssd_anchors(self, model):
        """
        Generates anchors for SSD model.

        Parameters
        ----------
        model: string
            BlazeFace tflite model to be used, available models are "front", back", and "short_range"
        """
        if model == "front" or model == "short_range":
            ssd_anchor_parameters = SSDAnchorParameters(
                input_size_width=128,
                input_size_height=128,
                min_scale=0.1484375,
                max_scale=0.75,
                number_feature_maps=8,
                feature_map_width=[],
                feature_map_height=[],
                strides=[8, 8, 16, 16, 16, 16, 16, 16],
                aspect_ratios=[1.0],
                pixel_anchors=True,
            )
        elif model == "back":
            ssd_anchor_parameters = SSDAnchorParameters(
                input_size_width=256,
                input_size_height=256,
                min_scale=0.15625,
                max_scale=0.75,
                number_feature_maps=8,
                feature_map_width=[],
                feature_map_height=[],
                strides=[16, 16, 32, 32, 32, 32, 32, 32],
                aspect_ratios=[1.0],
                pixel_anchors=True,
            )
        self.anchors = get_ssd_anchors(ssd_anchor_parameters)

    def generate_blazeface_anchors(self, model):
        """
        Generates anchors for BlazeFace model.

        Parameters
        ----------
        model: string
            BlazeFace tflite model to be used, available models are "front", back", and "short_range"
        """
        if model == "front" or model == "short_range":
            blazeface_anchor_parameters = BlazeFaceAnchorParameters(
                input_size_height_width=128, strides=[8, 16]
            )
        elif model == "back":
            blazeface_anchor_parameters = BlazeFaceAnchorParameters(
                input_size_height_width=256, strides=[16, 32]
            )
        self.anchors = get_blazeface_anchors(blazeface_anchor_parameters)

    def preprocess(self, img):
        """
        Converts format, resizes, and normalizes input image, returns a tensor of the right dimensions.

        Parameters
        ----------
        img: numpy array
            square BGR image

        Return
        ------
        tensor: numpy array
            tensor of the right image format, size, normalization, and dimensions
        """
        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        self.img_height, self.img_width, self.img_channels = img.shape
        # Resize to 128 x 128 pixels for front model and 256 x 256 pixels for back model
        img = cv2.resize(img, (self.input_height, self.input_width))
        # Add batch dimension to fit BlazeFace input size
        tensor = np.expand_dims(img, axis=0)
        # Normalize to -1 to 1
        tensor = tf.convert_to_tensor(tensor, dtype=tf.float32)
        tensor /= 127.5
        tensor -= 1
        return tensor

    def inference(self, input_tensor):
        """
        Runs inference on input tensor, returns detection results.

        Parameters
        ----------
        input_tensor: numpy array
            tensor of the right image format, size, normalization, and dimensions

        Return
        ------
        output0: numpy array
            2D array of size (896 x 16):
            2 anchors for each (16x16) feature map grid cell + 6 anchors for each (8x8) feature map grid cell,
            1 point for bounding box center + width + height + 6 point for keypoints for each anchor

        output1: numpy array
            1D array of size (896), confidence scores
        """
        self.interpreter.set_tensor(self.input_details[0]["index"], input_tensor)
        self.interpreter.invoke()
        output0 = np.squeeze(
            self.interpreter.get_tensor(self.output_details[0]["index"])
        )
        # print("Box & Keypoints:", output0[0])
        output1 = np.squeeze(
            self.interpreter.get_tensor(self.output_details[1]["index"])
        )

        # # Suppress 16x16 feature maps
        # output1[:512] = 0

        # # Suppress 8x8 feature maps
        # output1[512:] = 0

        # # Keep 1 anchor only
        # keep_id = 271
        # output1[:keep_id] = 0
        # output1[keep_id + 1:] = 0
        # print("output1:", output1[keep_id])
        # print("output0:", output0[keep_id])

        # # Suppress second anchor of 16x16 feature maps
        # output1_16 = output1[:512]
        # output1_16[::2] = 0
        # # Suppress second to sixth anchors of 8x8 feature maps
        # output1_8 = output1[512:]
        # output1_8[1::6] = 0
        # output1_8[2::6] = 0
        # output1_8[3::6] = 0
        # output1_8[4::6] = 0
        # output1_8[5::6] = 0
        # output1 = np.concatenate((output1_16, output1_8))

        # print("Max score anchor:", np.argmax(output1))
        return output0, output1

    def threshold_detections(self, output1):
        """
        Thresholds detection results based on score.

        Parameters
        ----------
        output1: numpy array
            1D array of size (896), confidence scores

        Return
        ------
        scores: numpy array
            filtered confidence scores as probabilities

        indices: numpy array
            indices of scores greater than threshold
        """
        # Threshold scores
        indices = np.where(output1 > self.score_threshold_raw)[0]
        # Convert scores to probability
        scores = 1.0 / (1.0 + np.exp(-output1[indices]))
        return scores, indices

    def extract_boxes_keypoints(self, output0, indices):
        """
        Extracts keypoints and bounding boxes for detections passed thresholding.

        Parameters
        ----------
        output0: numpy array
            2D array of size (896 x 16):
            2 anchors for each (16x16) feature map grid cell + 6 anchors for each (8x8) feature map grid cell,
            1 point for bounding box center + width + height + 6 point for keypoints for each anchor

        indices: numpy array
            indices of filtered scores greater than threshold

        Return
        ------
        boxes: numpy array
            filtered bounding boxes converted from (center, width, height) in reference to feature map grid cell,
            to 2 diagonal points in reference and normalized to input image

        keypoints: numpy array
            requested/filtered keypoints converted from being in reference to feature map grid cell, to be
            in reference and normalized to input image
        """
        number_indices = indices.shape[0]
        keypoints = np.zeros((number_indices, self.number_keypoints, 2))
        boxes = np.zeros((number_indices, 4))
        # Convert detections to be in reference to input image
        for idx, detection_idx in enumerate(indices):
            # Convert box coordinates
            anchor = self.anchors[detection_idx]
            x = output0[detection_idx, 0]
            y = output0[detection_idx, 1]
            w = output0[detection_idx, 2]
            h = output0[detection_idx, 3]
            # Convert to be in reference to center of image instead of center of grid cell
            x += anchor.x_center * self.input_width
            y += anchor.y_center * self.input_height
            # Normalize to image size
            x /= self.input_width
            y /= self.input_height
            w /= self.input_width
            h /= self.input_height
            # Convert box coordinates from center point, width and height to 2 diagonal points
            boxes[idx, :] = np.array(
                [x - w * 0.5, y - h * 0.5, x + w * 0.5, y + h * 0.5]
            )
            # Convert keypoint coordinates
            for j in range(self.number_keypoints):
                x = output0[detection_idx, 4 + 2 * j]
                y = output0[detection_idx, 4 + 2 * j + 1]
                # Convert to be in reference to center of image instead of center of grid cell
                x += anchor.x_center * self.input_width
                y += anchor.y_center * self.input_height
                # Normalize to image size
                x /= self.input_width
                y /= self.input_height
                keypoints[idx, j, :] = np.array([x, y])
        return boxes, keypoints

    def non_max_suppression(self, boxes, keypoints, scores):
        """
        Filters bounding boxes, keypoints, and scores using non-maximum suppression.

        Parameters
        ----------
        boxes: numpy array
            filtered bounding boxes in reference and normalized to input image

        keypoints: numpy array
            requested/filtered keypoints in reference and normalized to input image

        scores: numpy array
            filtered confidence scores as probabilities

        Return
        ------
        results: Results object
            contains detected scores, keypoints, and bounding boxes
        """
        nms_indices = tf.image.non_max_suppression(
            boxes, scores, self.max_faces, self.iou_threshold
        )
        nms_boxes = tf.gather(boxes, nms_indices).numpy()
        # print(nms_boxes)
        nms_keypoints = tf.gather(keypoints, nms_indices).numpy()
        # print(nms_keypoints)
        nms_scores = tf.gather(scores, nms_indices).numpy()
        # print(nms_scores)
        results = Results(nms_boxes, nms_keypoints, nms_scores)
        return results

    def weighted_mean(self, boxes, keypoints, scores):
        """
        Filters bounding boxes, keypoints, and scores using weighted mean.

        Parameters
        ----------
        boxes: numpy array
            filtered bounding boxes in reference and normalized to input image

        keypoints: numpy array
            requested/filtered keypoints in reference and normalized to input image

        scores: numpy array
            filtered confidence scores as probabilities

        Return
        ------
        results: Results object
            contains detected scores, keypoints, and bounding boxes
        """
        pass

    def detect(self, img):
        """
        detects faces in image and returns score, keypoints, and bounding boxes.

        Parameters
        ----------
        img: numpy array
            square BGR image

        Return
        ------
        results: Results object
            contains detected scores, keypoints, and bounding boxes
        """
        # Preprocess image
        input_tensor = self.preprocess(img)
        output0, output1 = self.inference(input_tensor)
        # Threshold scores
        scores, indices = self.threshold_detections(output1)
        # print("scores:", scores)
        # print("indices:", indices)
        # Extract boxes and keypoints
        boxes, keypoints = self.extract_boxes_keypoints(output0, indices)
        # print("boxes:", boxes)
        # print("keypoints:", keypoints)
        # Non-maximum suppression or blending
        if self.postprocess == "non_max_suppression":
            results = self.non_max_suppression(boxes, keypoints, scores)
        elif self.postprocess == "weighted_mean":
            results = weighted_mean(
                boxes,
                keypoints,
                scores,
                score_threshold=self.score_threshold,
                iou_threshold=self.iou_threshold,
                max_faces=self.max_faces,
            )
        return results

    def annotate(
        self, img, results, box_color=(70, 140, 255), keypoint_color=(150, 255, 80)
    ):
        """
        Annotates image with score, keypoints, and bounding boxes.

        Parameters
        ----------
        img: numpy array
            square BGR image

        results: Results object
            contains detected scores, keypoints, and boxes

        box_color: tuple
            BGR values of bounding box color

        keypoint_color: tuple
            BGR values of keypoint color

        Return
        ------
        img: numpy array
            square BGR image annotated with scores, keypoints, and bounding boxes
        """
        boxes = results.boxes
        keypoints = results.keypoints
        scores = results.scores
        # Add bounding boxes
        for boxes, keypoints, score in zip(boxes, keypoints, scores):
            x1 = round(self.img_width * boxes[0])
            x2 = round(self.img_width * boxes[2])
            y1 = round(self.img_height * boxes[1])
            y2 = round(self.img_height * boxes[3])
            cv2.rectangle(img, (x1, y1), (x2, y2), box_color, 2)
            cv2.putText(
                img,
                "{:.2f}".format(score),
                (x1, y1 - 6),
                cv2.FONT_HERSHEY_SIMPLEX,
                0.6,
                box_color,
                2,
            )
            # Add keypoints
            for keypoint in keypoints:
                keypoint_x = round(keypoint[0] * self.img_width)
                keypoint_y = round(keypoint[1] * self.img_height)
                cv2.circle(img, (keypoint_x, keypoint_y), 4, keypoint_color, -1)
        return img


class SSDAnchorParameters:
    """SSD anchor generation parameters."""

    def __init__(
        self,
        input_size_width,
        input_size_height,
        min_scale,
        max_scale,
        number_feature_maps,
        feature_map_width,
        feature_map_height,
        strides,
        aspect_ratios,
        anchor_offset_x=0.5,
        anchor_offset_y=0.5,
        pixel_anchors=False,
    ):
        """
        Constructs SSDAnchorParameters object.

        Parameters
        ----------
        input_size_width: int
            size of input image width for SSD model

        input_size_height: int
            size of input image height for SSD model

        min_scale: float
            minimum scale for generating anchor boxes on feature maps

        max_scale: float
            Maximum scale for generating anchor boxes on feature maps

        number_feature_maps: int
            number of feature maps

        feature_map_width: list
            width of feature maps, either feature map size or stride should be provided

        feature_map_height: list
            height of feature maps, either feature map size or stride should be provided

        strides: list
            strides of feature maps, either feature map size or stride should be provided

        aspect_ratios: list
            aspect ratio for anchors

        anchor_offset_x: float
            x offset for anchor center, the value is in scale of stride

        anchor_offset_y: float
            y offset for anchor center, the value is in scale of stride

        pixel_anchors: boolean
            predicted anchors in pixels
        """
        self.input_size_width = input_size_width
        self.input_size_height = input_size_height
        self.min_scale = min_scale
        self.max_scale = max_scale
        self.number_feature_maps = number_feature_maps
        self.feature_map_width = feature_map_width
        self.feature_map_height = feature_map_height
        self.feature_map_width_size = len(feature_map_width)
        self.feature_map_height_size = len(feature_map_height)
        self.strides = strides
        self.strides_size = len(strides)
        self.aspect_ratios = aspect_ratios
        self.aspect_ratios_size = len(aspect_ratios)
        self.anchor_offset_x = anchor_offset_x
        self.anchor_offset_y = anchor_offset_y
        self.pixel_anchors = pixel_anchors

    def to_string(self):
        """Print parameters."""
        return (
            f"input_size_width: {self.input_size_width}\ninput_size_height: {self.input_size_height}\n"
            f"min_scale: {self.min_scale}\nmax_scale: {self.max_scale}\nanchor_offset_x: {self.anchor_offset_x}\n"
            f"anchor_offset_y: {self.anchor_offset_y}\nnumber_feature_maps: {self.number_feature_maps}\n"
            f"feature_map_width: {self.feature_map_width}\nfeature_map_height: {self.feature_map_height}\n"
            f"strides: {self.strides}\naspect_ratios: {self.aspect_ratios}\npixel_anchors: {self.pixel_anchors}"
        )


class BlazeFaceAnchorParameters:
    """BlazeFace anchor generation parameters."""

    def __init__(self, input_size_height_width, strides):
        """
        Constructs BlazeFaceAnchorParameters object.

        Parameters
        ----------
        input_size_height_width: int
            size of input image, height and width are always equal for BlazeFace

        strides: list
            strides of unique feature maps
        """
        self.input_size_height_width = input_size_height_width
        self.strides = strides

    def to_string(self):
        """Print parameters."""
        return f"input_size_height_width: {self.input_size_height_width}\nstrides: {self.strides}"


class Anchor:
    """Wraps anchors."""

    def __init__(self, x_center, y_center, h, w):
        """
        Constructs results class.

        Parameters
        ----------
        x_center: float
            bounding box center x coordinate

        y_center: float
            bounding box center y coordinate

        h: float
            bounding box height

        w: float
            bounding box width
        """
        self.x_center = x_center
        self.y_center = y_center
        self.h = h
        self.w = w

    def to_string(self):
        """Print anchor."""
        return f"x_center: {self.x_center}, y_center: {self.y_center}, h: {self.h}, w: {self.w}"


def get_ssd_anchors(parameters):
    """
    Generates

    Parameters
    ----------
    parameters: SSDAnchorParameters object
        parameters to be used for anchor generation

    Return
    ------
    anchors: list
        list of Anchor objects
    """
    anchors = []
    if parameters.strides_size != parameters.number_feature_maps:
        print("Number of strides must be equal to feature maps!")
        return []

    map_id = 0
    while map_id < parameters.strides_size:
        anchor_height = []
        anchor_width = []
        aspect_ratios = []
        scales = []
        # For same strides, merge the anchors in the same order
        last_same_stride_map = map_id
        while (
            last_same_stride_map < parameters.strides_size
            and parameters.strides[last_same_stride_map] == parameters.strides[map_id]
        ):
            scale = (
                parameters.min_scale
                + (parameters.max_scale - parameters.min_scale)
                * last_same_stride_map
                / parameters.strides_size
            )
            for aspect_ratio_id in range(parameters.aspect_ratios_size):
                aspect_ratios.append(parameters.aspect_ratios[aspect_ratio_id])
                scales.append(scale)

            last_same_stride_map += 1

        # Get anchor height and width
        for i in range(len(aspect_ratios)):
            ratio_sqrts = math.sqrt(aspect_ratios[i])
            anchor_height.append(scales[i] / ratio_sqrts)
            anchor_width.append(scales[i] * ratio_sqrts)

        # Get feature map height and width
        if parameters.feature_map_height_size > 0:
            feature_map_height = parameters.feature_map_height[map_id]
            feature_map_width = parameters.feature_map_width[map_id]
        else:
            stride = parameters.strides[map_id]
            feature_map_height = math.ceil(parameters.input_size_height / stride)
            feature_map_width = math.ceil(parameters.input_size_width / stride)

        #
        for y in range(feature_map_height):
            for x in range(feature_map_width):
                for anchor_id in range(len(anchor_height)):
                    x_center = (x + parameters.anchor_offset_x) / feature_map_width
                    y_center = (y + parameters.anchor_offset_y) / feature_map_height
                    if parameters.pixel_anchors:
                        w = 1.0
                        h = 1.0
                    else:
                        w = anchor_width[anchor_id]
                        h = anchor_height[anchor_id]
                    new_anchor = Anchor(x_center, y_center, h, w)
                    anchors.append(new_anchor)
        map_id = last_same_stride_map
    return anchors


def get_blazeface_anchors(parameters):
    """
    Generates

    Parameters
    ----------
    parameters: BlazeFaceAnchorParameters object
        parameters to be used for anchor generation

    Return
    ------
    anchors: list
        list of Anchor objects
    """
    assert len(parameters.strides) == 2, "Number of strides must be 2!"
    anchors = []
    # Anchors per unique feature map sizes
    number_anchors = [2, 6]
    for stride, number_anchor in zip(parameters.strides, number_anchors):
        # Get feature map height and width
        feature_map_height_width = parameters.input_size_height_width // stride
        for y in range(feature_map_height_width):
            for x in range(feature_map_height_width):
                # find center coordinates and normalize to input image size
                x_center = (
                    x * stride + stride / 2
                ) / parameters.input_size_height_width
                y_center = (
                    y * stride + stride / 2
                ) / parameters.input_size_height_width
                w = 1.0
                h = 1.0
                anchor = Anchor(x_center, y_center, h, w)
                for _ in range(number_anchor):
                    anchors.append(anchor)
    return anchors


def iou(box_1, box_2):
    """Calculates intersection over union of 2 given bounding boxes."""
    x_left = max(box_1[0], box_2[0])
    y_top = max(box_1[1], box_2[1])
    x_right = min(box_1[2], box_2[2])
    y_bottom = min(box_1[3], box_2[3])
    if x_right <= x_left or y_bottom <= y_top:
        intersection = 0.0
    else:
        intersection = (x_right - x_left) * (y_bottom - y_top)
    union = (
        (box_1[2] - box_1[0]) * (box_1[3] - box_1[1])
        + (box_2[2] - box_2[0]) * (box_2[3] - box_2[1])
        - intersection
    )
    iou = intersection / union
    return iou


def weighted_mean(
    boxes, keypoints, scores, score_threshold=0.70, iou_threshold=0.3, max_faces=10
):
    if len(boxes) == 0:
        results = Results(boxes, keypoints, scores)
        return results
    # Sort detections (descending) based on scores
    boxes = [
        box for _, box in sorted(zip(scores, boxes), reverse=True, key=lambda x: x[0])
    ]
    keypoints = [
        keypoint
        for _, keypoint in sorted(
            zip(scores, keypoints), reverse=True, key=lambda x: x[0]
        )
    ]
    scores = sorted(scores, reverse=True)
    # Keep tracking of processed anchors
    i_processed = set()
    # Grouping of intersected anchors
    box_groups = []
    keypoint_groups = []
    score_groups = []
    # Final weighted mean detections
    boxes_wm = []
    keypoints_wm = []
    # Go over all anchors and group the intersected ones
    for i in range(len(boxes)):
        if i in i_processed:
            continue
        boxes_intersected = []
        keypoints_intersected = []
        scores_intersected = []
        boxes_intersected.append(boxes[i])
        keypoints_intersected.append(keypoints[i])
        scores_intersected.append(scores[i])
        i_processed.add(i)
        # Check all remaining unprocessed anchors
        for j in range(i + 1, len(boxes)):
            # Calculate IOU and if it's equal or greater than a threshold then add to group
            boxes_iou = iou(boxes[i], boxes[j])
            if boxes_iou >= iou_threshold:
                i_processed.add(j)
                boxes_intersected.append(boxes[j])
                keypoints_intersected.append(keypoints[j])
                scores_intersected.append(scores[j])
        box_groups.append(np.array(boxes_intersected))
        keypoint_groups.append(np.array(keypoints_intersected))
        score_groups.append(np.array(scores_intersected))
    # Weighted mean of detections
    for i in range(len(box_groups)):
        score_group = score_groups[i].copy()
        score_group -= score_threshold
        weight_group = score_group / sum(score_group)
        # Weighted mean of box groups
        box_group_wm = [sum(box_groups[i][:, j] * weight_group) for j in range(4)]
        box_group_wm = np.array(box_group_wm)
        boxes_wm.append(box_group_wm)
        # Weighted mean of keypoint groups
        keypoint_group_wm = [
            [sum(keypoint_groups[i][:, j, k] * weight_group) for k in range(2)]
            for j in range(6)
        ]
        keypoint_group_wm = np.array(keypoint_group_wm)
        keypoints_wm.append(keypoint_group_wm)
    boxes_wm = np.array(boxes_wm)
    keypoints_wm = np.array(keypoints_wm)
    # Max score of score groups
    scores_max = [max(scores) for scores in score_groups[:max_faces]]
    scores_max = np.array(scores_max)
    # Return boxes and keypoints up to max_face
    boxes_wm = boxes_wm[:max_faces]
    keypoints_wm = keypoints_wm[:max_faces]
    # Wrap detections in Results object
    results = Results(boxes_wm, keypoints_wm, scores_max)
    return results


class Results:
    """Wraps BlazeFace detection results."""

    def __init__(self, boxes, keypoints, scores):
        """
        Constructs results class.

        Parameters
        ----------
        boxes: list
            coordinates of bounding boxes of detected faces

        keypoints: list
            eyes, ears, nose, and mouth location of detected faces

        scores: list
            BlazeFace confidence scores of detected faces
        """
        self.boxes = boxes
        self.keypoints = keypoints
        self.scores = scores
