280 lines
9.3 KiB
Python
280 lines
9.3 KiB
Python
import math
|
|
from typing import Union, Tuple
|
|
import numpy as np
|
|
from PIL import Image
|
|
|
|
|
|
# pylint: disable=unused-argument
|
|
|
|
|
|
def find_euclidean_distance(
|
|
source_representation: Union[np.ndarray, list], test_representation: Union[np.ndarray, list]
|
|
) -> float:
|
|
"""
|
|
Find euclidean distance between 2 vectors
|
|
Args:
|
|
source_representation (numpy array or list)
|
|
test_representation (numpy array or list)
|
|
Returns
|
|
distance
|
|
"""
|
|
if isinstance(source_representation, list):
|
|
source_representation = np.array(source_representation)
|
|
|
|
if isinstance(test_representation, list):
|
|
test_representation = np.array(test_representation)
|
|
|
|
euclidean_distance = source_representation - test_representation
|
|
euclidean_distance = np.sum(np.multiply(euclidean_distance, euclidean_distance))
|
|
euclidean_distance = np.sqrt(euclidean_distance)
|
|
return euclidean_distance
|
|
|
|
|
|
def alignment_procedure(
|
|
img: np.ndarray, left_eye: tuple, right_eye: tuple, nose: tuple
|
|
) -> Tuple[np.ndarray, float, int]:
|
|
"""
|
|
Alignma given face with respect to the left and right eye coordinates.
|
|
Left eye is the eye appearing on the left (right eye of the person). Left top point is (0, 0)
|
|
Args:
|
|
img (numpy array): given image
|
|
left_eye (tuple): left eye coordinates.
|
|
Left eye is appearing on the left of image (right eye of the person)
|
|
right_eye (tuple): right eye coordinates.
|
|
Right eye is appearing on the right of image (left eye of the person)
|
|
nose (tuple): coordinates of nose
|
|
"""
|
|
|
|
left_eye_x, left_eye_y = left_eye
|
|
right_eye_x, right_eye_y = right_eye
|
|
|
|
# -----------------------
|
|
# find rotation direction
|
|
if left_eye_y > right_eye_y:
|
|
point_3rd = (right_eye_x, left_eye_y)
|
|
direction = -1 # rotate same direction to clock
|
|
else:
|
|
point_3rd = (left_eye_x, right_eye_y)
|
|
direction = 1 # rotate inverse direction of clock
|
|
|
|
# -----------------------
|
|
# find length of triangle edges
|
|
|
|
a = find_euclidean_distance(np.array(left_eye), np.array(point_3rd))
|
|
b = find_euclidean_distance(np.array(right_eye), np.array(point_3rd))
|
|
c = find_euclidean_distance(np.array(right_eye), np.array(left_eye))
|
|
|
|
# -----------------------
|
|
# apply cosine rule
|
|
if b != 0 and c != 0: # this multiplication causes division by zero in cos_a calculation
|
|
|
|
cos_a = (b * b + c * c - a * a) / (2 * b * c)
|
|
|
|
# PR15: While mathematically cos_a must be within the closed range [-1.0, 1.0],
|
|
# floating point errors would produce cases violating this
|
|
# In fact, we did come across a case where cos_a took the value 1.0000000169176173
|
|
# which lead to a NaN from the following np.arccos step
|
|
cos_a = min(1.0, max(-1.0, cos_a))
|
|
|
|
angle = np.arccos(cos_a) # angle in radian
|
|
angle = (angle * 180) / math.pi # radian to degree
|
|
|
|
# -----------------------
|
|
# rotate base image
|
|
|
|
if direction == -1:
|
|
angle = 90 - angle
|
|
|
|
img = Image.fromarray(img)
|
|
img = np.array(img.rotate(direction * angle))
|
|
else:
|
|
angle = 0.0 # Dummy value for undefined angle
|
|
|
|
# -----------------------
|
|
|
|
return img, angle, direction
|
|
|
|
|
|
def rotate_facial_area(
|
|
facial_area: Tuple[int, int, int, int], angle: float, direction: int, size: Tuple[int, int]
|
|
) -> Tuple[int, int, int, int]:
|
|
"""
|
|
Rotate the facial area around its center.
|
|
|
|
Args:
|
|
facial_area (tuple of int): Representing the (x1, y1, x2, y2) of the facial area.
|
|
angle (float): Angle of rotation in degrees.
|
|
direction (int): Direction of rotation (-1 for clockwise, 1 for counterclockwise).
|
|
size (tuple of int): Tuple representing the size of the image (width, height).
|
|
|
|
Returns:
|
|
rotated_facial_area (tuple of int): Representing the new coordinates
|
|
(x1, y1, x2, y2) of the rotated facial area.
|
|
"""
|
|
# Angle in radians
|
|
angle = angle * np.pi / 180
|
|
|
|
height, weight = size
|
|
|
|
# Translate the facial area to the center of the image
|
|
x = (facial_area[0] + facial_area[2]) / 2 - weight / 2
|
|
y = (facial_area[1] + facial_area[3]) / 2 - height / 2
|
|
|
|
# Rotate the facial area
|
|
x_new = x * np.cos(angle) + y * direction * np.sin(angle)
|
|
y_new = -x * direction * np.sin(angle) + y * np.cos(angle)
|
|
|
|
# Translate the facial area back to the original position
|
|
x_new = x_new + weight / 2
|
|
y_new = y_new + height / 2
|
|
|
|
# Calculate projected coordinates after alignment
|
|
x1 = x_new - (facial_area[2] - facial_area[0]) / 2
|
|
y1 = y_new - (facial_area[3] - facial_area[1]) / 2
|
|
x2 = x_new + (facial_area[2] - facial_area[0]) / 2
|
|
y2 = y_new + (facial_area[3] - facial_area[1]) / 2
|
|
|
|
# validate projected coordinates are in image's boundaries
|
|
x1 = max(int(x1), 0)
|
|
y1 = max(int(y1), 0)
|
|
x2 = min(int(x2), weight)
|
|
y2 = min(int(y2), height)
|
|
|
|
return (x1, y1, x2, y2)
|
|
|
|
|
|
def bbox_pred(boxes, box_deltas):
|
|
"""
|
|
This function is copied from the following code snippet:
|
|
https://github.com/StanislasBertrand/RetinaFace-tf2/blob/master/retinaface.py
|
|
"""
|
|
if boxes.shape[0] == 0:
|
|
return np.zeros((0, box_deltas.shape[1]))
|
|
|
|
boxes = boxes.astype(float, copy=False)
|
|
widths = boxes[:, 2] - boxes[:, 0] + 1.0
|
|
heights = boxes[:, 3] - boxes[:, 1] + 1.0
|
|
ctr_x = boxes[:, 0] + 0.5 * (widths - 1.0)
|
|
ctr_y = boxes[:, 1] + 0.5 * (heights - 1.0)
|
|
|
|
dx = box_deltas[:, 0:1]
|
|
dy = box_deltas[:, 1:2]
|
|
dw = box_deltas[:, 2:3]
|
|
dh = box_deltas[:, 3:4]
|
|
|
|
pred_ctr_x = dx * widths[:, np.newaxis] + ctr_x[:, np.newaxis]
|
|
pred_ctr_y = dy * heights[:, np.newaxis] + ctr_y[:, np.newaxis]
|
|
pred_w = np.exp(dw) * widths[:, np.newaxis]
|
|
pred_h = np.exp(dh) * heights[:, np.newaxis]
|
|
|
|
pred_boxes = np.zeros(box_deltas.shape)
|
|
# x1
|
|
pred_boxes[:, 0:1] = pred_ctr_x - 0.5 * (pred_w - 1.0)
|
|
# y1
|
|
pred_boxes[:, 1:2] = pred_ctr_y - 0.5 * (pred_h - 1.0)
|
|
# x2
|
|
pred_boxes[:, 2:3] = pred_ctr_x + 0.5 * (pred_w - 1.0)
|
|
# y2
|
|
pred_boxes[:, 3:4] = pred_ctr_y + 0.5 * (pred_h - 1.0)
|
|
|
|
if box_deltas.shape[1] > 4:
|
|
pred_boxes[:, 4:] = box_deltas[:, 4:]
|
|
|
|
return pred_boxes
|
|
|
|
|
|
def landmark_pred(boxes, landmark_deltas):
|
|
"""
|
|
This function copied from the following code snippet
|
|
https://github.com/StanislasBertrand/RetinaFace-tf2/blob/master/retinaface.py
|
|
"""
|
|
if boxes.shape[0] == 0:
|
|
return np.zeros((0, landmark_deltas.shape[1]))
|
|
boxes = boxes.astype(float, copy=False)
|
|
widths = boxes[:, 2] - boxes[:, 0] + 1.0
|
|
heights = boxes[:, 3] - boxes[:, 1] + 1.0
|
|
ctr_x = boxes[:, 0] + 0.5 * (widths - 1.0)
|
|
ctr_y = boxes[:, 1] + 0.5 * (heights - 1.0)
|
|
pred = landmark_deltas.copy()
|
|
for i in range(5):
|
|
pred[:, i, 0] = landmark_deltas[:, i, 0] * widths + ctr_x
|
|
pred[:, i, 1] = landmark_deltas[:, i, 1] * heights + ctr_y
|
|
return pred
|
|
|
|
|
|
def clip_boxes(boxes, im_shape):
|
|
"""
|
|
This function copied from rcnn module of retinaface-tf2 project
|
|
https://github.com/StanislasBertrand/RetinaFace-tf2/blob/master/rcnn/processing/bbox_transform.py
|
|
"""
|
|
# x1 >= 0
|
|
boxes[:, 0::4] = np.maximum(np.minimum(boxes[:, 0::4], im_shape[1] - 1), 0)
|
|
# y1 >= 0
|
|
boxes[:, 1::4] = np.maximum(np.minimum(boxes[:, 1::4], im_shape[0] - 1), 0)
|
|
# x2 < im_shape[1]
|
|
boxes[:, 2::4] = np.maximum(np.minimum(boxes[:, 2::4], im_shape[1] - 1), 0)
|
|
# y2 < im_shape[0]
|
|
boxes[:, 3::4] = np.maximum(np.minimum(boxes[:, 3::4], im_shape[0] - 1), 0)
|
|
return boxes
|
|
|
|
|
|
def anchors_plane(height, width, stride, base_anchors):
|
|
"""
|
|
This function is mainly based on the following code snippet
|
|
https://github.com/StanislasBertrand/RetinaFace-tf2/blob/master/rcnn/cython/anchors.pyx
|
|
"""
|
|
A = base_anchors.shape[0]
|
|
c_0_2 = np.tile(np.arange(0, width)[np.newaxis, :, np.newaxis, np.newaxis], (height, 1, A, 1))
|
|
c_1_3 = np.tile(np.arange(0, height)[:, np.newaxis, np.newaxis, np.newaxis], (1, width, A, 1))
|
|
all_anchors = np.concatenate([c_0_2, c_1_3, c_0_2, c_1_3], axis=-1) * stride + np.tile(
|
|
base_anchors[np.newaxis, np.newaxis, :, :], (height, width, 1, 1)
|
|
)
|
|
return all_anchors
|
|
|
|
|
|
def cpu_nms(dets, threshold):
|
|
"""
|
|
This function is mainly based on the following code snippet
|
|
https://github.com/StanislasBertrand/RetinaFace-tf2/blob/master/rcnn/cython/cpu_nms.pyx
|
|
Fast R-CNN by Ross Girshick
|
|
"""
|
|
x1 = dets[:, 0]
|
|
y1 = dets[:, 1]
|
|
x2 = dets[:, 2]
|
|
y2 = dets[:, 3]
|
|
scores = dets[:, 4]
|
|
|
|
areas = (x2 - x1 + 1) * (y2 - y1 + 1)
|
|
order = scores.argsort()[::-1]
|
|
|
|
ndets = dets.shape[0]
|
|
suppressed = np.zeros((ndets), dtype=int)
|
|
|
|
keep = []
|
|
for _i in range(ndets):
|
|
i = order[_i]
|
|
if suppressed[i] == 1:
|
|
continue
|
|
keep.append(i)
|
|
ix1 = x1[i]
|
|
iy1 = y1[i]
|
|
ix2 = x2[i]
|
|
iy2 = y2[i]
|
|
iarea = areas[i]
|
|
for _j in range(_i + 1, ndets):
|
|
j = order[_j]
|
|
if suppressed[j] == 1:
|
|
continue
|
|
xx1 = max(ix1, x1[j])
|
|
yy1 = max(iy1, y1[j])
|
|
xx2 = min(ix2, x2[j])
|
|
yy2 = min(iy2, y2[j])
|
|
w = max(0.0, xx2 - xx1 + 1)
|
|
h = max(0.0, yy2 - yy1 + 1)
|
|
inter = w * h
|
|
ovr = inter / (iarea + areas[j] - inter)
|
|
if ovr >= threshold:
|
|
suppressed[j] = 1
|
|
|
|
return keep
|