from typing import Any, Optional, List, Tuple
from time import sleep
import cv2
import numpy
import onnxruntime

import deepfuze.globals
from deepfuze import process_manager
from deepfuze.common_helper import get_first
from deepfuze.face_helper import estimate_matrix_by_face_landmark_5, warp_face_by_face_landmark_5, warp_face_by_translation, create_static_anchors, distance_to_face_landmark_5, distance_to_bounding_box, convert_face_landmark_68_to_5, apply_nms, categorize_age, categorize_gender
from deepfuze.face_store import get_static_faces, set_static_faces
from deepfuze.execution import apply_execution_provider_options
from deepfuze.download import conditional_download
from deepfuze.filesystem import resolve_relative_path, is_file
from deepfuze.thread_helper import thread_lock, thread_semaphore, conditional_thread_semaphore
from deepfuze.typing import VisionFrame, Face, FaceSet, FaceAnalyserOrder, FaceAnalyserAge, FaceAnalyserGender, ModelSet, BoundingBox, FaceLandmarkSet, FaceLandmark5, FaceLandmark68, Score, FaceScoreSet, Embedding
from deepfuze.vision import resize_frame_resolution, unpack_resolution

FACE_ANALYSER = None
MODELS : ModelSet =\
{
	'face_detector_retinaface':
	{
		'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/retinaface_10g.onnx',
		'path': resolve_relative_path('../../../models/deepfuze/retinaface_10g.onnx')
	},
	'face_detector_scrfd':
	{
		'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/scrfd_2.5g.onnx',
		'path': resolve_relative_path('../../../models/deepfuze/scrfd_2.5g.onnx')
	},
	'face_detector_yoloface':
	{
		'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/yoloface_8n.onnx',
		'path': resolve_relative_path('../../../models/deepfuze/yoloface_8n.onnx')
	},
	'face_detector_yunet':
	{
		'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/yunet_2023mar.onnx',
		'path': resolve_relative_path('../../../models/deepfuze/yunet_2023mar.onnx')
	},
	'face_recognizer_arcface_blendswap':
	{
		'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/arcface_w600k_r50.onnx',
		'path': resolve_relative_path('../../../models/deepfuze/arcface_w600k_r50.onnx')
	},
	'face_recognizer_arcface_inswapper':
	{
		'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/arcface_w600k_r50.onnx',
		'path': resolve_relative_path('../../../models/deepfuze/arcface_w600k_r50.onnx')
	},
	'face_recognizer_arcface_simswap':
	{
		'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/arcface_simswap.onnx',
		'path': resolve_relative_path('../../../models/deepfuze/arcface_simswap.onnx')
	},
	'face_recognizer_arcface_uniface':
	{
		'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/arcface_w600k_r50.onnx',
		'path': resolve_relative_path('../../../models/deepfuze/arcface_w600k_r50.onnx')
	},
	'face_landmarker_68':
	{
		'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/2dfan4.onnx',
		'path': resolve_relative_path('../../../models/deepfuze/2dfan4.onnx')
	},
	'face_landmarker_68_5':
	{
		'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/face_landmarker_68_5.onnx',
		'path': resolve_relative_path('../../../models/deepfuze/face_landmarker_68_5.onnx')
	},
	'gender_age':
	{
		'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/gender_age.onnx',
		'path': resolve_relative_path('../../../models/deepfuze/gender_age.onnx')
	}
}


def get_face_analyser() -> Any:
	global FACE_ANALYSER

	face_detectors = {}
	face_landmarkers = {}

	with thread_lock():
		while process_manager.is_checking():
			sleep(0.5)
		if FACE_ANALYSER is None:
			if deepfuze.globals.face_detector_model in [ 'many', 'retinaface' ]:
				face_detectors['retinaface'] = onnxruntime.InferenceSession(MODELS.get('face_detector_retinaface').get('path'), providers = apply_execution_provider_options(deepfuze.globals.execution_device_id, deepfuze.globals.execution_providers))
			if deepfuze.globals.face_detector_model in [ 'many', 'scrfd' ]:
				face_detectors['scrfd'] = onnxruntime.InferenceSession(MODELS.get('face_detector_scrfd').get('path'), providers = apply_execution_provider_options(deepfuze.globals.execution_device_id, deepfuze.globals.execution_providers))
			if deepfuze.globals.face_detector_model in [ 'many', 'yoloface' ]:
				face_detectors['yoloface'] = onnxruntime.InferenceSession(MODELS.get('face_detector_yoloface').get('path'), providers = apply_execution_provider_options(deepfuze.globals.execution_device_id, deepfuze.globals.execution_providers))
			if deepfuze.globals.face_detector_model in [ 'yunet' ]:
				face_detectors['yunet'] = cv2.FaceDetectorYN.create(MODELS.get('face_detector_yunet').get('path'), '', (0, 0))
			if deepfuze.globals.face_recognizer_model == 'arcface_blendswap':
				face_recognizer = onnxruntime.InferenceSession(MODELS.get('face_recognizer_arcface_blendswap').get('path'), providers = apply_execution_provider_options(deepfuze.globals.execution_device_id, deepfuze.globals.execution_providers))
			if deepfuze.globals.face_recognizer_model == 'arcface_inswapper':
				face_recognizer = onnxruntime.InferenceSession(MODELS.get('face_recognizer_arcface_inswapper').get('path'), providers = apply_execution_provider_options(deepfuze.globals.execution_device_id, deepfuze.globals.execution_providers))
			if deepfuze.globals.face_recognizer_model == 'arcface_simswap':
				face_recognizer = onnxruntime.InferenceSession(MODELS.get('face_recognizer_arcface_simswap').get('path'), providers = apply_execution_provider_options(deepfuze.globals.execution_device_id, deepfuze.globals.execution_providers))
			if deepfuze.globals.face_recognizer_model == 'arcface_uniface':
				face_recognizer = onnxruntime.InferenceSession(MODELS.get('face_recognizer_arcface_uniface').get('path'), providers = apply_execution_provider_options(deepfuze.globals.execution_device_id, deepfuze.globals.execution_providers))
			face_landmarkers['68'] = onnxruntime.InferenceSession(MODELS.get('face_landmarker_68').get('path'), providers = apply_execution_provider_options(deepfuze.globals.execution_device_id, deepfuze.globals.execution_providers))
			face_landmarkers['68_5'] = onnxruntime.InferenceSession(MODELS.get('face_landmarker_68_5').get('path'), providers = apply_execution_provider_options(deepfuze.globals.execution_device_id, deepfuze.globals.execution_providers))
			gender_age = onnxruntime.InferenceSession(MODELS.get('gender_age').get('path'), providers = apply_execution_provider_options(deepfuze.globals.execution_device_id, deepfuze.globals.execution_providers))
			FACE_ANALYSER =\
			{
				'face_detectors': face_detectors,
				'face_recognizer': face_recognizer,
				'face_landmarkers': face_landmarkers,
				'gender_age': gender_age
			}
	return FACE_ANALYSER


def clear_face_analyser() -> Any:
	global FACE_ANALYSER

	FACE_ANALYSER = None


def pre_check() -> bool:
	download_directory_path = resolve_relative_path('../../../models/deepfuze')
	model_urls =\
	[
		MODELS.get('face_landmarker_68').get('url'),
		MODELS.get('face_landmarker_68_5').get('url'),
		MODELS.get('gender_age').get('url')
	]
	model_paths =\
	[
		MODELS.get('face_landmarker_68').get('path'),
		MODELS.get('face_landmarker_68_5').get('path'),
		MODELS.get('gender_age').get('path')
	]

	if deepfuze.globals.face_detector_model in [ 'many', 'retinaface' ]:
		model_urls.append(MODELS.get('face_detector_retinaface').get('url'))
		model_paths.append(MODELS.get('face_detector_retinaface').get('path'))
	if deepfuze.globals.face_detector_model in [ 'many', 'scrfd' ]:
		model_urls.append(MODELS.get('face_detector_scrfd').get('url'))
		model_paths.append(MODELS.get('face_detector_scrfd').get('path'))
	if deepfuze.globals.face_detector_model in [ 'many', 'yoloface' ]:
		model_urls.append(MODELS.get('face_detector_yoloface').get('url'))
		model_paths.append(MODELS.get('face_detector_yoloface').get('path'))
	if deepfuze.globals.face_detector_model in [ 'yunet' ]:
		model_urls.append(MODELS.get('face_detector_yunet').get('url'))
		model_paths.append(MODELS.get('face_detector_yunet').get('path'))
	if deepfuze.globals.face_recognizer_model == 'arcface_blendswap':
		model_urls.append(MODELS.get('face_recognizer_arcface_blendswap').get('url'))
		model_paths.append(MODELS.get('face_recognizer_arcface_blendswap').get('path'))
	if deepfuze.globals.face_recognizer_model == 'arcface_inswapper':
		model_urls.append(MODELS.get('face_recognizer_arcface_inswapper').get('url'))
		model_paths.append(MODELS.get('face_recognizer_arcface_inswapper').get('path'))
	if deepfuze.globals.face_recognizer_model == 'arcface_simswap':
		model_urls.append(MODELS.get('face_recognizer_arcface_simswap').get('url'))
		model_paths.append(MODELS.get('face_recognizer_arcface_simswap').get('path'))
	if deepfuze.globals.face_recognizer_model == 'arcface_uniface':
		model_urls.append(MODELS.get('face_recognizer_arcface_uniface').get('url'))
		model_paths.append(MODELS.get('face_recognizer_arcface_uniface').get('path'))

	if not deepfuze.globals.skip_download:
		process_manager.check()
		conditional_download(download_directory_path, model_urls)
		process_manager.end()
	return all(is_file(model_path) for model_path in model_paths)


def detect_with_retinaface(vision_frame : VisionFrame, face_detector_size : str) -> Tuple[List[BoundingBox], List[FaceLandmark5], List[Score]]:
	face_detector = get_face_analyser().get('face_detectors').get('retinaface')
	face_detector_width, face_detector_height = unpack_resolution(face_detector_size)
	temp_vision_frame = resize_frame_resolution(vision_frame, (face_detector_width, face_detector_height))
	ratio_height = vision_frame.shape[0] / temp_vision_frame.shape[0]
	ratio_width = vision_frame.shape[1] / temp_vision_frame.shape[1]
	feature_strides = [ 8, 16, 32 ]
	feature_map_channel = 3
	anchor_total = 2
	bounding_box_list = []
	face_landmark_5_list = []
	score_list = []

	detect_vision_frame = prepare_detect_frame(temp_vision_frame, face_detector_size)
	with thread_semaphore():
		detections = face_detector.run(None,
		{
			face_detector.get_inputs()[0].name: detect_vision_frame
		})
	for index, feature_stride in enumerate(feature_strides):
		keep_indices = numpy.where(detections[index] >= deepfuze.globals.face_detector_score)[0]
		if keep_indices.any():
			stride_height = face_detector_height // feature_stride
			stride_width = face_detector_width // feature_stride
			anchors = create_static_anchors(feature_stride, anchor_total, stride_height, stride_width)
			bounding_box_raw = detections[index + feature_map_channel] * feature_stride
			face_landmark_5_raw = detections[index + feature_map_channel * 2] * feature_stride
			for bounding_box in distance_to_bounding_box(anchors, bounding_box_raw)[keep_indices]:
				bounding_box_list.append(numpy.array(
				[
					bounding_box[0] * ratio_width,
					bounding_box[1] * ratio_height,
					bounding_box[2] * ratio_width,
					bounding_box[3] * ratio_height
				]))
			for face_landmark_5 in distance_to_face_landmark_5(anchors, face_landmark_5_raw)[keep_indices]:
				face_landmark_5_list.append(face_landmark_5 * [ ratio_width, ratio_height ])
			for score in detections[index][keep_indices]:
				score_list.append(score[0])
	return bounding_box_list, face_landmark_5_list, score_list


def detect_with_scrfd(vision_frame : VisionFrame, face_detector_size : str) -> Tuple[List[BoundingBox], List[FaceLandmark5], List[Score]]:
	face_detector = get_face_analyser().get('face_detectors').get('scrfd')
	face_detector_width, face_detector_height = unpack_resolution(face_detector_size)
	temp_vision_frame = resize_frame_resolution(vision_frame, (face_detector_width, face_detector_height))
	ratio_height = vision_frame.shape[0] / temp_vision_frame.shape[0]
	ratio_width = vision_frame.shape[1] / temp_vision_frame.shape[1]
	feature_strides = [ 8, 16, 32 ]
	feature_map_channel = 3
	anchor_total = 2
	bounding_box_list = []
	face_landmark_5_list = []
	score_list = []

	detect_vision_frame = prepare_detect_frame(temp_vision_frame, face_detector_size)
	with thread_semaphore():
		detections = face_detector.run(None,
		{
			face_detector.get_inputs()[0].name: detect_vision_frame
		})
	for index, feature_stride in enumerate(feature_strides):
		keep_indices = numpy.where(detections[index] >= deepfuze.globals.face_detector_score)[0]
		if keep_indices.any():
			stride_height = face_detector_height // feature_stride
			stride_width = face_detector_width // feature_stride
			anchors = create_static_anchors(feature_stride, anchor_total, stride_height, stride_width)
			bounding_box_raw = detections[index + feature_map_channel] * feature_stride
			face_landmark_5_raw = detections[index + feature_map_channel * 2] * feature_stride
			for bounding_box in distance_to_bounding_box(anchors, bounding_box_raw)[keep_indices]:
				bounding_box_list.append(numpy.array(
				[
					bounding_box[0] * ratio_width,
					bounding_box[1] * ratio_height,
					bounding_box[2] * ratio_width,
					bounding_box[3] * ratio_height
				]))
			for face_landmark_5 in distance_to_face_landmark_5(anchors, face_landmark_5_raw)[keep_indices]:
				face_landmark_5_list.append(face_landmark_5 * [ ratio_width, ratio_height ])
			for score in detections[index][keep_indices]:
				score_list.append(score[0])
	return bounding_box_list, face_landmark_5_list, score_list


def detect_with_yoloface(vision_frame : VisionFrame, face_detector_size : str) -> Tuple[List[BoundingBox], List[FaceLandmark5], List[Score]]:
	face_detector = get_face_analyser().get('face_detectors').get('yoloface')
	face_detector_width, face_detector_height = unpack_resolution(face_detector_size)
	temp_vision_frame = resize_frame_resolution(vision_frame, (face_detector_width, face_detector_height))
	ratio_height = vision_frame.shape[0] / temp_vision_frame.shape[0]
	ratio_width = vision_frame.shape[1] / temp_vision_frame.shape[1]
	bounding_box_list = []
	face_landmark_5_list = []
	score_list = []

	detect_vision_frame = prepare_detect_frame(temp_vision_frame, face_detector_size)
	with thread_semaphore():
		detections = face_detector.run(None,
		{
			face_detector.get_inputs()[0].name: detect_vision_frame
		})
	detections = numpy.squeeze(detections).T
	bounding_box_raw, score_raw, face_landmark_5_raw = numpy.split(detections, [ 4, 5 ], axis = 1)
	keep_indices = numpy.where(score_raw > deepfuze.globals.face_detector_score)[0]
	if keep_indices.any():
		bounding_box_raw, face_landmark_5_raw, score_raw = bounding_box_raw[keep_indices], face_landmark_5_raw[keep_indices], score_raw[keep_indices]
		for bounding_box in bounding_box_raw:
			bounding_box_list.append(numpy.array(
			[
				(bounding_box[0] - bounding_box[2] / 2) * ratio_width,
				(bounding_box[1] - bounding_box[3] / 2) * ratio_height,
				(bounding_box[0] + bounding_box[2] / 2) * ratio_width,
				(bounding_box[1] + bounding_box[3] / 2) * ratio_height
			]))
		face_landmark_5_raw[:, 0::3] = (face_landmark_5_raw[:, 0::3]) * ratio_width
		face_landmark_5_raw[:, 1::3] = (face_landmark_5_raw[:, 1::3]) * ratio_height
		for face_landmark_5 in face_landmark_5_raw:
			face_landmark_5_list.append(numpy.array(face_landmark_5.reshape(-1, 3)[:, :2]))
		score_list = score_raw.ravel().tolist()
	return bounding_box_list, face_landmark_5_list, score_list


def detect_with_yunet(vision_frame : VisionFrame, face_detector_size : str) -> Tuple[List[BoundingBox], List[FaceLandmark5], List[Score]]:
	face_detector = get_face_analyser().get('face_detectors').get('yunet')
	face_detector_width, face_detector_height = unpack_resolution(face_detector_size)
	temp_vision_frame = resize_frame_resolution(vision_frame, (face_detector_width, face_detector_height))
	ratio_height = vision_frame.shape[0] / temp_vision_frame.shape[0]
	ratio_width = vision_frame.shape[1] / temp_vision_frame.shape[1]
	bounding_box_list = []
	face_landmark_5_list = []
	score_list = []

	face_detector.setInputSize((temp_vision_frame.shape[1], temp_vision_frame.shape[0]))
	face_detector.setScoreThreshold(deepfuze.globals.face_detector_score)
	with thread_semaphore():
		_, detections = face_detector.detect(temp_vision_frame)
	if numpy.any(detections):
		for detection in detections:
			bounding_box_list.append(numpy.array(
			[
				detection[0] * ratio_width,
				detection[1] * ratio_height,
				(detection[0] + detection[2]) * ratio_width,
				(detection[1] + detection[3]) * ratio_height
			]))
			face_landmark_5_list.append(detection[4:14].reshape((5, 2)) * [ ratio_width, ratio_height ])
			score_list.append(detection[14])
	return bounding_box_list, face_landmark_5_list, score_list


def prepare_detect_frame(temp_vision_frame : VisionFrame, face_detector_size : str) -> VisionFrame:
	face_detector_width, face_detector_height = unpack_resolution(face_detector_size)
	detect_vision_frame = numpy.zeros((face_detector_height, face_detector_width, 3))
	detect_vision_frame[:temp_vision_frame.shape[0], :temp_vision_frame.shape[1], :] = temp_vision_frame
	detect_vision_frame = (detect_vision_frame - 127.5) / 128.0
	detect_vision_frame = numpy.expand_dims(detect_vision_frame.transpose(2, 0, 1), axis = 0).astype(numpy.float32)
	return detect_vision_frame


def create_faces(vision_frame : VisionFrame, bounding_box_list : List[BoundingBox], face_landmark_5_list : List[FaceLandmark5], score_list : List[Score]) -> List[Face]:
	faces = []
	if deepfuze.globals.face_detector_score > 0:
		sort_indices = numpy.argsort(-numpy.array(score_list))
		bounding_box_list = [ bounding_box_list[index] for index in sort_indices ]
		face_landmark_5_list = [face_landmark_5_list[index] for index in sort_indices]
		score_list = [ score_list[index] for index in sort_indices ]
		iou_threshold = 0.1 if deepfuze.globals.face_detector_model == 'many' else 0.4
		keep_indices = apply_nms(bounding_box_list, iou_threshold)
		for index in keep_indices:
			bounding_box = bounding_box_list[index]
			face_landmark_5_68 = face_landmark_5_list[index]
			face_landmark_68_5 = expand_face_landmark_68_from_5(face_landmark_5_68)
			face_landmark_68 = face_landmark_68_5
			face_landmark_68_score = 0.0
			if deepfuze.globals.face_landmarker_score > 0:
				face_landmark_68, face_landmark_68_score = detect_face_landmark_68(vision_frame, bounding_box)
				if face_landmark_68_score > deepfuze.globals.face_landmarker_score:
					face_landmark_5_68 = convert_face_landmark_68_to_5(face_landmark_68)
			landmarks : FaceLandmarkSet =\
			{
				'5': face_landmark_5_list[index],
				'5/68': face_landmark_5_68,
				'68': face_landmark_68,
				'68/5': face_landmark_68_5
			}
			scores : FaceScoreSet = \
			{
				'detector': score_list[index],
				'landmarker': face_landmark_68_score
			}
			embedding, normed_embedding = calc_embedding(vision_frame, landmarks.get('5/68'))
			gender, age = detect_gender_age(vision_frame, bounding_box)
			faces.append(Face(
				bounding_box = bounding_box,
				landmarks = landmarks,
				scores = scores,
				embedding = embedding,
				normed_embedding = normed_embedding,
				gender = gender,
				age = age
			))
	return faces


def calc_embedding(temp_vision_frame : VisionFrame, face_landmark_5 : FaceLandmark5) -> Tuple[Embedding, Embedding]:
	face_recognizer = get_face_analyser().get('face_recognizer')
	crop_vision_frame, matrix = warp_face_by_face_landmark_5(temp_vision_frame, face_landmark_5, 'arcface_112_v2', (112, 112))
	crop_vision_frame = crop_vision_frame / 127.5 - 1
	crop_vision_frame = crop_vision_frame[:, :, ::-1].transpose(2, 0, 1).astype(numpy.float32)
	crop_vision_frame = numpy.expand_dims(crop_vision_frame, axis = 0)
	with conditional_thread_semaphore(deepfuze.globals.execution_providers):
		embedding = face_recognizer.run(None,
		{
			face_recognizer.get_inputs()[0].name: crop_vision_frame
		})[0]
	embedding = embedding.ravel()
	normed_embedding = embedding / numpy.linalg.norm(embedding)
	return embedding, normed_embedding


def detect_face_landmark_68(temp_vision_frame : VisionFrame, bounding_box : BoundingBox) -> Tuple[FaceLandmark68, Score]:
	face_landmarker = get_face_analyser().get('face_landmarkers').get('68')
	scale = 195 / numpy.subtract(bounding_box[2:], bounding_box[:2]).max()
	translation = (256 - numpy.add(bounding_box[2:], bounding_box[:2]) * scale) * 0.5
	crop_vision_frame, affine_matrix = warp_face_by_translation(temp_vision_frame, translation, scale, (256, 256))
	crop_vision_frame = cv2.cvtColor(crop_vision_frame, cv2.COLOR_RGB2Lab)
	if numpy.mean(crop_vision_frame[:, :, 0]) < 30:
		crop_vision_frame[:, :, 0] = cv2.createCLAHE(clipLimit = 2).apply(crop_vision_frame[:, :, 0])
	crop_vision_frame = cv2.cvtColor(crop_vision_frame, cv2.COLOR_Lab2RGB)
	crop_vision_frame = crop_vision_frame.transpose(2, 0, 1).astype(numpy.float32) / 255.0
	with conditional_thread_semaphore(deepfuze.globals.execution_providers):
		face_landmark_68, face_heatmap = face_landmarker.run(None,
		{
			face_landmarker.get_inputs()[0].name: [ crop_vision_frame ]
		})
	face_landmark_68 = face_landmark_68[:, :, :2][0] / 64
	face_landmark_68 = face_landmark_68.reshape(1, -1, 2) * 256
	face_landmark_68 = cv2.transform(face_landmark_68, cv2.invertAffineTransform(affine_matrix))
	face_landmark_68 = face_landmark_68.reshape(-1, 2)
	face_landmark_68_score = numpy.amax(face_heatmap, axis = (2, 3))
	face_landmark_68_score = numpy.mean(face_landmark_68_score)
	return face_landmark_68, face_landmark_68_score


def expand_face_landmark_68_from_5(face_landmark_5 : FaceLandmark5) -> FaceLandmark68:
	face_landmarker = get_face_analyser().get('face_landmarkers').get('68_5')
	affine_matrix = estimate_matrix_by_face_landmark_5(face_landmark_5, 'ffhq_512', (1, 1))
	face_landmark_5 = cv2.transform(face_landmark_5.reshape(1, -1, 2), affine_matrix).reshape(-1, 2)
	with conditional_thread_semaphore(deepfuze.globals.execution_providers):
		face_landmark_68_5 = face_landmarker.run(None,
		{
			face_landmarker.get_inputs()[0].name: [ face_landmark_5 ]
		})[0][0]
	face_landmark_68_5 = cv2.transform(face_landmark_68_5.reshape(1, -1, 2), cv2.invertAffineTransform(affine_matrix)).reshape(-1, 2)
	return face_landmark_68_5


def detect_gender_age(temp_vision_frame : VisionFrame, bounding_box : BoundingBox) -> Tuple[int, int]:
	gender_age = get_face_analyser().get('gender_age')
	bounding_box = bounding_box.reshape(2, -1)
	scale = 64 / numpy.subtract(*bounding_box[::-1]).max()
	translation = 48 - bounding_box.sum(axis = 0) * scale * 0.5
	crop_vision_frame, affine_matrix = warp_face_by_translation(temp_vision_frame, translation, scale, (96, 96))
	crop_vision_frame = crop_vision_frame[:, :, ::-1].transpose(2, 0, 1).astype(numpy.float32)
	crop_vision_frame = numpy.expand_dims(crop_vision_frame, axis = 0)
	with conditional_thread_semaphore(deepfuze.globals.execution_providers):
		prediction = gender_age.run(None,
		{
			gender_age.get_inputs()[0].name: crop_vision_frame
		})[0][0]
	gender = int(numpy.argmax(prediction[:2]))
	age = int(numpy.round(prediction[2] * 100))
	return gender, age


def get_one_face(vision_frame : VisionFrame, position : int = 0) -> Optional[Face]:
	many_faces = get_many_faces(vision_frame)
	if many_faces:
		try:
			return many_faces[position]
		except IndexError:
			return many_faces[-1]
	return None


def get_average_face(vision_frames : List[VisionFrame], position : int = 0) -> Optional[Face]:
	average_face = None
	faces = []
	embedding_list = []
	normed_embedding_list = []

	for vision_frame in vision_frames:
		face = get_one_face(vision_frame, position)
		if face:
			faces.append(face)
			embedding_list.append(face.embedding)
			normed_embedding_list.append(face.normed_embedding)
	if faces:
		first_face = get_first(faces)
		average_face = Face(
			bounding_box = first_face.bounding_box,
			landmarks = first_face.landmarks,
			scores = first_face.scores,
			embedding = numpy.mean(embedding_list, axis = 0),
			normed_embedding = numpy.mean(normed_embedding_list, axis = 0),
			gender = first_face.gender,
			age = first_face.age
		)
	return average_face


def get_many_faces(vision_frame : VisionFrame) -> List[Face]:
	faces = []
	try:
		faces_cache = get_static_faces(vision_frame)
		if faces_cache:
			faces = faces_cache
		else:
			bounding_box_list = []
			face_landmark_5_list = []
			score_list = []

			if deepfuze.globals.face_detector_model in [ 'many', 'retinaface']:
				bounding_box_list_retinaface, face_landmark_5_list_retinaface, score_list_retinaface = detect_with_retinaface(vision_frame, deepfuze.globals.face_detector_size)
				bounding_box_list.extend(bounding_box_list_retinaface)
				face_landmark_5_list.extend(face_landmark_5_list_retinaface)
				score_list.extend(score_list_retinaface)
			if deepfuze.globals.face_detector_model in [ 'many', 'scrfd' ]:
				bounding_box_list_scrfd, face_landmark_5_list_scrfd, score_list_scrfd = detect_with_scrfd(vision_frame, deepfuze.globals.face_detector_size)
				bounding_box_list.extend(bounding_box_list_scrfd)
				face_landmark_5_list.extend(face_landmark_5_list_scrfd)
				score_list.extend(score_list_scrfd)
			if deepfuze.globals.face_detector_model in [ 'many', 'yoloface' ]:
				bounding_box_list_yoloface, face_landmark_5_list_yoloface, score_list_yoloface = detect_with_yoloface(vision_frame, deepfuze.globals.face_detector_size)
				bounding_box_list.extend(bounding_box_list_yoloface)
				face_landmark_5_list.extend(face_landmark_5_list_yoloface)
				score_list.extend(score_list_yoloface)
			if deepfuze.globals.face_detector_model in [ 'yunet' ]:
				bounding_box_list_yunet, face_landmark_5_list_yunet, score_list_yunet = detect_with_yunet(vision_frame, deepfuze.globals.face_detector_size)
				bounding_box_list.extend(bounding_box_list_yunet)
				face_landmark_5_list.extend(face_landmark_5_list_yunet)
				score_list.extend(score_list_yunet)
			if bounding_box_list and face_landmark_5_list and score_list:
				faces = create_faces(vision_frame, bounding_box_list, face_landmark_5_list, score_list)
			if faces:
				set_static_faces(vision_frame, faces)
		if deepfuze.globals.face_analyser_order:
			faces = sort_by_order(faces, deepfuze.globals.face_analyser_order)
		if deepfuze.globals.face_analyser_age:
			faces = filter_by_age(faces, deepfuze.globals.face_analyser_age)
		if deepfuze.globals.face_analyser_gender:
			faces = filter_by_gender(faces, deepfuze.globals.face_analyser_gender)
	except (AttributeError, ValueError):
		pass
	return faces


def find_similar_faces(reference_faces : FaceSet, vision_frame : VisionFrame, face_distance : float) -> List[Face]:
	similar_faces : List[Face] = []
	many_faces = get_many_faces(vision_frame)

	if reference_faces:
		for reference_set in reference_faces:
			if not similar_faces:
				for reference_face in reference_faces[reference_set]:
					for face in many_faces:
						if compare_faces(face, reference_face, face_distance):
							similar_faces.append(face)
	return similar_faces


def compare_faces(face : Face, reference_face : Face, face_distance : float) -> bool:
	current_face_distance = calc_face_distance(face, reference_face)
	return current_face_distance < face_distance


def calc_face_distance(face : Face, reference_face : Face) -> float:
	if hasattr(face, 'normed_embedding') and hasattr(reference_face, 'normed_embedding'):
		return 1 - numpy.dot(face.normed_embedding, reference_face.normed_embedding)
	return 0


def sort_by_order(faces : List[Face], order : FaceAnalyserOrder) -> List[Face]:
	if order == 'left-right':
		return sorted(faces, key = lambda face: face.bounding_box[0])
	if order == 'right-left':
		return sorted(faces, key = lambda face: face.bounding_box[0], reverse = True)
	if order == 'top-bottom':
		return sorted(faces, key = lambda face: face.bounding_box[1])
	if order == 'bottom-top':
		return sorted(faces, key = lambda face: face.bounding_box[1], reverse = True)
	if order == 'small-large':
		return sorted(faces, key = lambda face: (face.bounding_box[2] - face.bounding_box[0]) * (face.bounding_box[3] - face.bounding_box[1]))
	if order == 'large-small':
		return sorted(faces, key = lambda face: (face.bounding_box[2] - face.bounding_box[0]) * (face.bounding_box[3] - face.bounding_box[1]), reverse = True)
	if order == 'best-worst':
		return sorted(faces, key = lambda face: face.scores.get('detector'), reverse = True)
	if order == 'worst-best':
		return sorted(faces, key = lambda face: face.scores.get('detector'))
	return faces


def filter_by_age(faces : List[Face], age : FaceAnalyserAge) -> List[Face]:
	filter_faces = []
	for face in faces:
		if categorize_age(face.age) == age:
			filter_faces.append(face)
	return filter_faces


def filter_by_gender(faces : List[Face], gender : FaceAnalyserGender) -> List[Face]:
	filter_faces = []
	for face in faces:
		if categorize_gender(face.gender) == gender:
			filter_faces.append(face)
	return filter_faces
