alignfaces 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- alignfaces/__init__.py +15 -0
- alignfaces/aperture_tools.py +213 -0
- alignfaces/contrast_tools.py +106 -0
- alignfaces/contrast_tools_.py +106 -0
- alignfaces/data/shape_predictor_68_face_landmarks.dat +0 -0
- alignfaces/face_landmarks.py +233 -0
- alignfaces/make_aligned_faces.py +1217 -0
- alignfaces/make_aligned_faces_.py +1209 -0
- alignfaces/make_files.py +42 -0
- alignfaces/make_files_.py +42 -0
- alignfaces/make_files_OLD.py +86 -0
- alignfaces/phase_cong_3.py +524 -0
- alignfaces/plot_tools.py +170 -0
- alignfaces/procrustes_tools.py +217 -0
- alignfaces/tests/R/align_reference.csv +1 -0
- alignfaces/tests/R/align_shapes.csv +40 -0
- alignfaces/tests/R/input_shapes.csv +40 -0
- alignfaces/tests/__init__.py +0 -0
- alignfaces/tests/_test_pawarp.py +267 -0
- alignfaces/tests/test_procrustes_tools.py +569 -0
- alignfaces/tests/test_warp_tools.py +316 -0
- alignfaces/warp_tools.py +279 -0
- alignfaces-1.0.1.dist-info/METADATA +135 -0
- alignfaces-1.0.1.dist-info/RECORD +27 -0
- alignfaces-1.0.1.dist-info/WHEEL +5 -0
- alignfaces-1.0.1.dist-info/licenses/LICENSE.txt +13 -0
- alignfaces-1.0.1.dist-info/top_level.txt +1 -0
alignfaces/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
*******************************************************
|
|
3
|
+
*
|
|
4
|
+
* AlignFaces - INIT FILE
|
|
5
|
+
*
|
|
6
|
+
* Version: Version 1.0
|
|
7
|
+
* License: Apache 2.0
|
|
8
|
+
* Written by: Carl Michael Gaspar
|
|
9
|
+
* Created on: March 14, 2019
|
|
10
|
+
* Last updated: March 14, 2019
|
|
11
|
+
*
|
|
12
|
+
*******************************************************
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .make_aligned_faces import *
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
# Might need to import below into make_aligned_faces:
|
|
3
|
+
# from cv2 import imread, cvtColor, COLOR_BGR2GRAY, imwrite
|
|
4
|
+
from skimage.filters import gaussian
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Function to fit an ellipse using a very simple method.
|
|
8
|
+
# Semi-major axis (vertical) length is fixed as argument.
|
|
9
|
+
# Increase length of semi-minor until widest distance among landmarks fits.
|
|
10
|
+
# Immediate code above is redundant with this function.
|
|
11
|
+
def fit_ellipse_semi_minor(semi_major, landmarks, center):
|
|
12
|
+
X, Y = landmarks[0], landmarks[1]
|
|
13
|
+
CX, CY = center[0], center[1]
|
|
14
|
+
Xc = X - CX
|
|
15
|
+
Yc = Y - CY
|
|
16
|
+
a_min = np.floor((Xc.max() - Xc.min()) * 3 / 10)
|
|
17
|
+
a = a_min
|
|
18
|
+
all_in = (((Xc**2/a**2) + (Yc**2/semi_major**2)) <= 1).all()
|
|
19
|
+
while (not all_in):
|
|
20
|
+
a += 1
|
|
21
|
+
all_in = (((Xc**2/a**2) + (Yc**2/semi_major**2)) <= 1).all()
|
|
22
|
+
return a
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def make_ellipse_map(semi_minor, semi_major, center, size, soften=True):
|
|
26
|
+
CX, CY = center[0], center[1]
|
|
27
|
+
x = np.array([i-CX for i in range(size[1])])
|
|
28
|
+
y = np.array([i-CY for i in range(size[0])])
|
|
29
|
+
xv, yv = np.meshgrid(x, y)
|
|
30
|
+
R = (xv**2) / semi_minor**2 + (yv**2) / semi_major**2
|
|
31
|
+
if soften:
|
|
32
|
+
# Soften edges using Butterworth as a function of radius from (CX, CY)
|
|
33
|
+
filter_n = 10
|
|
34
|
+
aperture = 1 / np.sqrt(1 + R**(2*filter_n))
|
|
35
|
+
else:
|
|
36
|
+
aperture = R <= 1
|
|
37
|
+
return aperture
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Function to make a binary map of a circle within image of size = size.
|
|
41
|
+
def make_circle_map(cxy, radius, size):
|
|
42
|
+
size = (size[1], size[0])
|
|
43
|
+
xx = np.array([[x - cxy[0] for x in range(1, size[0]+1)]
|
|
44
|
+
for y in range(size[1])])
|
|
45
|
+
yy = np.array([[y - cxy[1] for y in range(1, size[1]+1)]
|
|
46
|
+
for x in range(size[0])]).T
|
|
47
|
+
rr = np.sqrt(xx**2 + yy**2)
|
|
48
|
+
return rr <= radius
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Function to make binary map selecting for entire image area below below_y
|
|
52
|
+
def make_map_below_y(below_y, size):
|
|
53
|
+
size = (size[1], size[0])
|
|
54
|
+
yy = np.array([[y for y in range(1, size[1]+1)] for x in range(size[0])]).T
|
|
55
|
+
return yy > below_y
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Function to make a binary aperture in shape of Moss's Egg.
|
|
59
|
+
#
|
|
60
|
+
# 1. ABC isosceles with point B facing down
|
|
61
|
+
# a. define upc, midpoint between A and C
|
|
62
|
+
# b. upc fraction along vector from mean of inter-eye midpoints
|
|
63
|
+
# to center of all landmarks
|
|
64
|
+
# i. fraction default is 1/4 but set as argument
|
|
65
|
+
# c. radius_upper is fraction of ellipse_width
|
|
66
|
+
# i. defined in ellipse-fitting functions
|
|
67
|
+
# ii. fraction default is 47/100 but set as argument
|
|
68
|
+
# d. A is upc shifted left by radius_upper
|
|
69
|
+
# e. C is upc shifted right by radius_upper
|
|
70
|
+
# f. B[x] is upc[x] and B[y] is mean of all nose-tips
|
|
71
|
+
# 2. Rest of procedure follows basic construction of Moss's egg
|
|
72
|
+
def make_moss_egg(landmark_features, center, size,
|
|
73
|
+
fraction_width=47/100, soften=True):
|
|
74
|
+
CX, CY = center[0], center[1]
|
|
75
|
+
|
|
76
|
+
# Set radius_upper using same method used when fitting an elliptical
|
|
77
|
+
# aperture.
|
|
78
|
+
shapes = np.array(landmark_features['AllSubjectsLandmarksDict'])
|
|
79
|
+
X = shapes[:, 0::2].reshape(-1,)
|
|
80
|
+
Y = shapes[:, 1::2].reshape(-1,)
|
|
81
|
+
# Longest vertical length of ellipse that fits within image.
|
|
82
|
+
if (size[0] / 2) < CY:
|
|
83
|
+
ellipse_height = (size[0] - CY) * 2
|
|
84
|
+
elif (size[0] / 2) > CY:
|
|
85
|
+
ellipse_height = CY * 2
|
|
86
|
+
else:
|
|
87
|
+
ellipse_height = size[0]
|
|
88
|
+
semi_major = ellipse_height / 2
|
|
89
|
+
semi_minor = fit_ellipse_semi_minor(semi_major=semi_major,
|
|
90
|
+
landmarks=(X, Y),
|
|
91
|
+
center=(CX, CY))
|
|
92
|
+
ellipse_width = semi_minor * 2
|
|
93
|
+
radius_upper = ellipse_width * fraction_width
|
|
94
|
+
|
|
95
|
+
# Upper circle, centered on upc (midpoint of AC in ABC).
|
|
96
|
+
# Top half defines top of Moss Egg.
|
|
97
|
+
to_center = 1 / 4
|
|
98
|
+
eye_midpoints = landmark_features['eye_midpoints']
|
|
99
|
+
eye_midpoint = np.array(eye_midpoints).mean(axis=0)
|
|
100
|
+
upc = ((CX, CY) - eye_midpoint) * to_center + (eye_midpoint)
|
|
101
|
+
horizontal_alignment = upc[0]
|
|
102
|
+
|
|
103
|
+
# Now make two large circles whose intersection defines middle part.
|
|
104
|
+
|
|
105
|
+
# Large circle on left, centered on cac
|
|
106
|
+
radius_large = radius_upper * 2
|
|
107
|
+
cac = (horizontal_alignment - radius_upper, upc[1])
|
|
108
|
+
|
|
109
|
+
# Large circle on right, centered on cbc
|
|
110
|
+
cbc = (horizontal_alignment + radius_upper, upc[1])
|
|
111
|
+
|
|
112
|
+
# Now make small circle at bottom, centered on lm.
|
|
113
|
+
nosey = np.array(landmark_features['nose_tips']).mean(axis=0)[1]
|
|
114
|
+
lm = (horizontal_alignment, nosey)
|
|
115
|
+
|
|
116
|
+
# Isosceles triangle cac -- lm -- cbc (ABC) with apex at lm.
|
|
117
|
+
# Ensure that angle at lm is greater than 60 degrees.
|
|
118
|
+
v1 = np.asarray(cac) - np.asarray(lm)
|
|
119
|
+
v2 = np.asarray(cbc) - np.asarray(lm)
|
|
120
|
+
acos = np.sum(v1 * v2) / (np.sqrt(np.sum(v1**2)) * np.sqrt(np.sum(v2**2)))
|
|
121
|
+
DegABC = np.arccos(acos) * 180 / np.pi
|
|
122
|
+
assert DegABC > 60
|
|
123
|
+
|
|
124
|
+
# Line defined by A (center of large circle) to lm.
|
|
125
|
+
# m * x + y_intercept
|
|
126
|
+
# m * x + c
|
|
127
|
+
delta = np.array(lm) - cac
|
|
128
|
+
m = delta[1] / delta[0]
|
|
129
|
+
t_intercept = -cac[0] / delta[0]
|
|
130
|
+
y_intercept = t_intercept * delta[1] + cac[1]
|
|
131
|
+
|
|
132
|
+
# Intersection of Ca with above line.
|
|
133
|
+
#
|
|
134
|
+
# (x - cac[0])**2 + (m * x + y_intercept - cac[1])**2 = radius_large**2
|
|
135
|
+
# (x - p)**2 + (m * x + c - q)**2 = r**2
|
|
136
|
+
A = m**2 + 1
|
|
137
|
+
B = 2 * (m * y_intercept - m*cac[1] - cac[0])
|
|
138
|
+
C = (cac[1]**2 - radius_large**2 + cac[0]**2 -
|
|
139
|
+
2*y_intercept*cac[1] + y_intercept**2)
|
|
140
|
+
|
|
141
|
+
assert B**2 - 4*A*C > 0
|
|
142
|
+
|
|
143
|
+
# Radius defined by distance from lm to above intersection.
|
|
144
|
+
# x_m = (-B - np.sqrt(B**2 - 4*A*C)) / (2*A)
|
|
145
|
+
x_p = (-B + np.sqrt(B**2 - 4*A*C)) / (2*A)
|
|
146
|
+
Ex = x_p
|
|
147
|
+
Ey = m * Ex + y_intercept
|
|
148
|
+
lower_radius = np.sqrt((((Ex, Ey) - np.array(lm))**2).sum())
|
|
149
|
+
|
|
150
|
+
Ca = make_circle_map(cxy=cac, radius=radius_large, size=size)
|
|
151
|
+
Cb = make_circle_map(cxy=cbc, radius=radius_large, size=size)
|
|
152
|
+
Cu = make_circle_map(cxy=upc, radius=radius_upper, size=size)
|
|
153
|
+
Cc = make_circle_map(cxy=lm, radius=lower_radius, size=size)
|
|
154
|
+
|
|
155
|
+
# LM1 = make_map_below_y(below_y=horizontal_alignment, size=size)
|
|
156
|
+
LM1 = make_map_below_y(below_y=upc[1], size=size)
|
|
157
|
+
LM2 = make_map_below_y(below_y=Ey, size=size)
|
|
158
|
+
|
|
159
|
+
EggA = Cu
|
|
160
|
+
EggB = Ca & Cb & LM1 & (LM2 == False)
|
|
161
|
+
EggC = Cc & LM2
|
|
162
|
+
# plt.imshow(np.c_[EggA, EggB, EggC])
|
|
163
|
+
|
|
164
|
+
MossEgg = EggA | EggB | EggC
|
|
165
|
+
|
|
166
|
+
if soften:
|
|
167
|
+
ME = MossEgg.astype(float)
|
|
168
|
+
IP = landmark_features['IrisPoints']
|
|
169
|
+
IPD = [np.sqrt(sum((I[1] - I[0])**2)) for I in IP]
|
|
170
|
+
sigma = round(np.asarray(IPD).mean() * 0.05)
|
|
171
|
+
MossEgg = gaussian(ME, sigma=(sigma, sigma),
|
|
172
|
+
truncate=3.5 * sigma)
|
|
173
|
+
# MossEgg = gaussian(ME, sigma=(sigma, sigma),
|
|
174
|
+
# truncate=3.5 * sigma, multichannel=False)
|
|
175
|
+
|
|
176
|
+
# package critical variables for visualizing moss's egg construction
|
|
177
|
+
egg_params = {}
|
|
178
|
+
egg_params['A'] = cac
|
|
179
|
+
egg_params['B'] = lm
|
|
180
|
+
egg_params['C'] = cbc
|
|
181
|
+
egg_params['upc'] = upc
|
|
182
|
+
egg_params['radius_large'] = radius_large
|
|
183
|
+
egg_params['radius_upper'] = radius_upper
|
|
184
|
+
egg_params['radius_lower'] = lower_radius
|
|
185
|
+
|
|
186
|
+
return MossEgg, egg_params
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# Pack all into a four-channel image of unsigned 8-bit integers.
|
|
190
|
+
def make_four_channel_image(img, aperture):
|
|
191
|
+
# assert aperture.min() >= 0 and aperture.max() <= 1
|
|
192
|
+
if not (aperture.min() >= 0 and aperture.max() <= 1):
|
|
193
|
+
aperture = aperture - aperture.min()
|
|
194
|
+
aperture = aperture / aperture.max()
|
|
195
|
+
alpha = (aperture * 255).astype(np.uint8)
|
|
196
|
+
if img.ndim == 2:
|
|
197
|
+
assert type(img[0, 0]) is np.uint8
|
|
198
|
+
size = img.shape
|
|
199
|
+
BGRA = np.zeros((size[0], size[1], 4), np.uint8)
|
|
200
|
+
for i in range(3):
|
|
201
|
+
BGRA[:, :, i] = img
|
|
202
|
+
BGRA[:, :, 3] = alpha
|
|
203
|
+
elif img.ndim == 3:
|
|
204
|
+
assert type(img[0, 0, 0]) is np.uint8
|
|
205
|
+
size = img.shape
|
|
206
|
+
BGRA = np.zeros((size[0], size[1], 4), np.uint8)
|
|
207
|
+
for i in range(3):
|
|
208
|
+
BGRA[:, :, i] = img[:, :, i]
|
|
209
|
+
BGRA[:, :, 3] = alpha
|
|
210
|
+
else:
|
|
211
|
+
BGRA = []
|
|
212
|
+
print("Warning: Image is neither grayscale nor RGB.")
|
|
213
|
+
return BGRA
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from skimage import exposure
|
|
3
|
+
|
|
4
|
+
# Ensures that values are centered on 127.5 and either reach until 0 or 255.
|
|
5
|
+
# full_image, numpy image array (any range of values)
|
|
6
|
+
# inner_locs, optional numpy binary map of inner face
|
|
7
|
+
# if supplied, all values are normalized but only properties
|
|
8
|
+
# within map are centered on 127.5 and reach until 0 or 255.
|
|
9
|
+
# no clipping occurs within map, but can occur outside.
|
|
10
|
+
#
|
|
11
|
+
# output image is numpy array, unsigned 8-bit integers.
|
|
12
|
+
def max_stretch_around_127(full_image, inner_locs=None):
|
|
13
|
+
if inner_locs is None:
|
|
14
|
+
inner_locs = np.ones(full_image.shape) == 1
|
|
15
|
+
else:
|
|
16
|
+
print("\nWarning: application of max_stretch_around_127 to subregion" +
|
|
17
|
+
"can result in clipping outside that subregion.")
|
|
18
|
+
|
|
19
|
+
inner_values = full_image[inner_locs]
|
|
20
|
+
om = inner_values.mean() # original mean value within binary map
|
|
21
|
+
inner_values = inner_values - om
|
|
22
|
+
if abs(inner_values.max()) > abs(inner_values.min()):
|
|
23
|
+
S = 127.5 / abs(inner_values.max())
|
|
24
|
+
elif abs(inner_values.max()) < abs(inner_values.min()):
|
|
25
|
+
S = 127.5 / abs(inner_values.min())
|
|
26
|
+
|
|
27
|
+
full_image = (full_image - om) * S + 127.5
|
|
28
|
+
return full_image.astype(np.uint8)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Ensures that values are centered on original mean and either reach until 0 or 255.
|
|
32
|
+
# full_image, numpy image array either [0-1] or [0-255]
|
|
33
|
+
# if [0-1] then multiplied by 255 to get original mean
|
|
34
|
+
# inner_locs, optional numpy binary map of inner face
|
|
35
|
+
# if supplied, all values are normalized but only properties
|
|
36
|
+
# within map are centered on 127.5 and reach until 0 or 255.
|
|
37
|
+
# no clipping occurs within map, but can occur outside.
|
|
38
|
+
#
|
|
39
|
+
# output image is numpy array, unsigned 8-bit integers.
|
|
40
|
+
def max_stretch_around_original_mean(full_image, inner_locs=None):
|
|
41
|
+
if (full_image.min() >=0) and (full_image.max()<=1):
|
|
42
|
+
full_image = full_image * 255
|
|
43
|
+
if inner_locs is None:
|
|
44
|
+
inner_locs = np.ones(full_image.shape) == 1
|
|
45
|
+
else:
|
|
46
|
+
print("\nWarning: application of max_stretch_around_original_mean" +
|
|
47
|
+
"to subregion can result in clipping outside that subregion.")
|
|
48
|
+
|
|
49
|
+
inner_values = full_image[inner_locs]
|
|
50
|
+
om = inner_values.mean() # original mean value within binary map
|
|
51
|
+
inner_values = inner_values - om
|
|
52
|
+
if abs(inner_values.max()) > abs(inner_values.min()):
|
|
53
|
+
# S = 127.5 / abs(inner_values.max())
|
|
54
|
+
# [om to 255]
|
|
55
|
+
# so maximum should now be equal to 255-om
|
|
56
|
+
# so multiply all by S where:
|
|
57
|
+
# MX * S = 255 - om
|
|
58
|
+
# S = (255 - om) / MX
|
|
59
|
+
S = (255 - om) / inner_values.max()
|
|
60
|
+
elif abs(inner_values.max()) < abs(inner_values.min()):
|
|
61
|
+
# S = 127.5 / abs(inner_values.min())
|
|
62
|
+
# [0 to om]
|
|
63
|
+
# so minimum should now be equal to -om
|
|
64
|
+
# so multiply all by:
|
|
65
|
+
# MN * S = -om
|
|
66
|
+
# S = -om / MN
|
|
67
|
+
S = -om / inner_values.min()
|
|
68
|
+
full_image = (full_image - om) * S + om
|
|
69
|
+
return full_image.astype(np.uint8)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def max_stretch(full_image, inner_locs=None):
|
|
73
|
+
if inner_locs is None:
|
|
74
|
+
inner_locs = np.ones(full_image.shape) == 1
|
|
75
|
+
else:
|
|
76
|
+
print("\nWarning: application of max_stretch to subregion" +
|
|
77
|
+
"can result in clipping outside that subregion.")
|
|
78
|
+
|
|
79
|
+
inner_values = full_image[inner_locs]
|
|
80
|
+
omin = inner_values.min() # original mean value within binary map
|
|
81
|
+
inner_values = inner_values - omin
|
|
82
|
+
omax = inner_values.max()
|
|
83
|
+
|
|
84
|
+
full_image = (full_image - omin) / omax
|
|
85
|
+
full_image = full_image * 255
|
|
86
|
+
return full_image.astype(np.uint8)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def contrast_stretch(full_image, inner_locs=None, type="max"):
|
|
90
|
+
if full_image.ndim == 3:
|
|
91
|
+
assert (type=="max") or (type==None)
|
|
92
|
+
if type == "max":
|
|
93
|
+
out_image = exposure.rescale_intensity(full_image)
|
|
94
|
+
return out_image
|
|
95
|
+
if type == "max":
|
|
96
|
+
out_image = max_stretch(full_image, inner_locs)
|
|
97
|
+
elif type == "mean_127":
|
|
98
|
+
out_image = max_stretch_around_127(full_image, inner_locs)
|
|
99
|
+
elif type == "mean_keep":
|
|
100
|
+
out_image = max_stretch_around_original_mean(full_image, inner_locs)
|
|
101
|
+
elif type == None:
|
|
102
|
+
out_image = full_image
|
|
103
|
+
else:
|
|
104
|
+
out_image = full_image
|
|
105
|
+
print("Warning: Invalid argument (type) to constrast_stretch.")
|
|
106
|
+
return out_image
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from skimage import exposure
|
|
3
|
+
|
|
4
|
+
# Ensures that values are centered on 127.5 and either reach until 0 or 255.
|
|
5
|
+
# full_image, numpy image array (any range of values)
|
|
6
|
+
# inner_locs, optional numpy binary map of inner face
|
|
7
|
+
# if supplied, all values are normalized but only properties
|
|
8
|
+
# within map are centered on 127.5 and reach until 0 or 255.
|
|
9
|
+
# no clipping occurs within map, but can occur outside.
|
|
10
|
+
#
|
|
11
|
+
# output image is numpy array, unsigned 8-bit integers.
|
|
12
|
+
def max_stretch_around_127(full_image, inner_locs=[]):
|
|
13
|
+
if inner_locs==[]:
|
|
14
|
+
inner_locs = np.ones(full_image.shape) == 1
|
|
15
|
+
else:
|
|
16
|
+
print("\nWarning: application of max_stretch_around_127 to subregion" +
|
|
17
|
+
"can result in clipping outside that subregion.")
|
|
18
|
+
|
|
19
|
+
inner_values = full_image[inner_locs]
|
|
20
|
+
om = inner_values.mean() # original mean value within binary map
|
|
21
|
+
inner_values = inner_values - om
|
|
22
|
+
if abs(inner_values.max()) > abs(inner_values.min()):
|
|
23
|
+
S = 127.5 / abs(inner_values.max())
|
|
24
|
+
elif abs(inner_values.max()) < abs(inner_values.min()):
|
|
25
|
+
S = 127.5 / abs(inner_values.min())
|
|
26
|
+
|
|
27
|
+
full_image = (full_image - om) * S + 127.5
|
|
28
|
+
return full_image.astype(np.uint8)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Ensures that values are centered on original mean and either reach until 0 or 255.
|
|
32
|
+
# full_image, numpy image array either [0-1] or [0-255]
|
|
33
|
+
# if [0-1] then multiplied by 255 to get original mean
|
|
34
|
+
# inner_locs, optional numpy binary map of inner face
|
|
35
|
+
# if supplied, all values are normalized but only properties
|
|
36
|
+
# within map are centered on 127.5 and reach until 0 or 255.
|
|
37
|
+
# no clipping occurs within map, but can occur outside.
|
|
38
|
+
#
|
|
39
|
+
# output image is numpy array, unsigned 8-bit integers.
|
|
40
|
+
def max_stretch_around_original_mean(full_image, inner_locs=[]):
|
|
41
|
+
if (full_image.min() >=0) and (full_image.max()<=1):
|
|
42
|
+
full_image = full_image * 255
|
|
43
|
+
if inner_locs==[]:
|
|
44
|
+
inner_locs = np.ones(full_image.shape) == 1
|
|
45
|
+
else:
|
|
46
|
+
print("\nWarning: application of max_stretch_around_original_mean" +
|
|
47
|
+
"to subregion can result in clipping outside that subregion.")
|
|
48
|
+
|
|
49
|
+
inner_values = full_image[inner_locs]
|
|
50
|
+
om = inner_values.mean() # original mean value within binary map
|
|
51
|
+
inner_values = inner_values - om
|
|
52
|
+
if abs(inner_values.max()) > abs(inner_values.min()):
|
|
53
|
+
# S = 127.5 / abs(inner_values.max())
|
|
54
|
+
# [om to 255]
|
|
55
|
+
# so maximum should now be equal to 255-om
|
|
56
|
+
# so multiply all by S where:
|
|
57
|
+
# MX * S = 255 - om
|
|
58
|
+
# S = (255 - om) / MX
|
|
59
|
+
S = (255 - om) / inner_values.max()
|
|
60
|
+
elif abs(inner_values.max()) < abs(inner_values.min()):
|
|
61
|
+
# S = 127.5 / abs(inner_values.min())
|
|
62
|
+
# [0 to om]
|
|
63
|
+
# so minimum should now be equal to -om
|
|
64
|
+
# so multiply all by:
|
|
65
|
+
# MN * S = -om
|
|
66
|
+
# S = -om / MN
|
|
67
|
+
S = -om / inner_values.min()
|
|
68
|
+
full_image = (full_image - om) * S + om
|
|
69
|
+
return full_image.astype(np.uint8)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def max_stretch(full_image, inner_locs=[]):
|
|
73
|
+
if inner_locs==[]:
|
|
74
|
+
inner_locs = np.ones(full_image.shape) == 1
|
|
75
|
+
else:
|
|
76
|
+
print("\nWarning: application of max_stretch to subregion" +
|
|
77
|
+
"can result in clipping outside that subregion.")
|
|
78
|
+
|
|
79
|
+
inner_values = full_image[inner_locs]
|
|
80
|
+
omin = inner_values.min() # original mean value within binary map
|
|
81
|
+
inner_values = inner_values - omin
|
|
82
|
+
omax = inner_values.max()
|
|
83
|
+
|
|
84
|
+
full_image = (full_image - omin) / omax
|
|
85
|
+
full_image = full_image * 255
|
|
86
|
+
return full_image.astype(np.uint8)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def contrast_stretch(full_image, inner_locs=[], type="max"):
|
|
90
|
+
if full_image.ndim == 3:
|
|
91
|
+
assert (type=="max") or (type==None)
|
|
92
|
+
if type == "max":
|
|
93
|
+
out_image = exposure.rescale_intensity(full_image)
|
|
94
|
+
return out_image
|
|
95
|
+
if type == "max":
|
|
96
|
+
out_image = max_stretch(full_image, inner_locs)
|
|
97
|
+
elif type == "mean_127":
|
|
98
|
+
out_image = max_stretch_around_127(full_image, inner_locs)
|
|
99
|
+
elif type == "mean_keep":
|
|
100
|
+
out_image = max_stretch_around_original_mean(full_image, inner_locs)
|
|
101
|
+
elif type == None:
|
|
102
|
+
out_image = full_image
|
|
103
|
+
else:
|
|
104
|
+
out_image = full_image
|
|
105
|
+
print("Warning: Invalid argument (type) to constrast_stretch.")
|
|
106
|
+
return out_image
|
|
Binary file
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
# Facial landmark detection
|
|
4
|
+
import dlib
|
|
5
|
+
|
|
6
|
+
# Iris localisation
|
|
7
|
+
# from .phase_cong_3 import phase_congruency
|
|
8
|
+
from skimage.morphology import dilation
|
|
9
|
+
from skimage.morphology import disk
|
|
10
|
+
from skimage import feature
|
|
11
|
+
from skimage.transform import hough_circle, hough_circle_peaks
|
|
12
|
+
from skimage.draw import circle_perimeter
|
|
13
|
+
from skimage.morphology import convex_hull_image
|
|
14
|
+
import skimage
|
|
15
|
+
|
|
16
|
+
# Formatting landmark data
|
|
17
|
+
import itertools
|
|
18
|
+
|
|
19
|
+
# Active contour for pulling upper subset of jawline
|
|
20
|
+
# landmarks closer to hairline.
|
|
21
|
+
from skimage.segmentation import active_contour
|
|
22
|
+
|
|
23
|
+
# Import data file
|
|
24
|
+
# from pkg_resources import resource_filename
|
|
25
|
+
from importlib.resources import files
|
|
26
|
+
|
|
27
|
+
import os
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def facial_landmarks(InputImage):
|
|
31
|
+
|
|
32
|
+
assert InputImage.ndim == 2 or InputImage.ndim == 3, \
|
|
33
|
+
'InputImage should be 2 or 3 dimensions.'
|
|
34
|
+
|
|
35
|
+
detector = dlib.get_frontal_face_detector()
|
|
36
|
+
|
|
37
|
+
# detect faces in the grayscale image
|
|
38
|
+
rects = detector(InputImage, 0)
|
|
39
|
+
if len(rects)==0:
|
|
40
|
+
rects = detector(InputImage, 1)
|
|
41
|
+
if len(rects)==0:
|
|
42
|
+
return
|
|
43
|
+
# assert len(rects)==1, 'Exactly one face must be detected in the image.'
|
|
44
|
+
rect = rects[0]
|
|
45
|
+
|
|
46
|
+
# determine the facial landmarks for the face region, then
|
|
47
|
+
# convert the facial landmark (x, y)-coordinates to a NumPy
|
|
48
|
+
# array
|
|
49
|
+
# predictor = dlib.shape_predictor('/Users/carl/Python Explore/models/shape_predictor_68_face_landmarks.dat')
|
|
50
|
+
# predictor = dlib.shape_predictor(resource_filename('alignfaces', 'data' + os.path.sep + 'shape_predictor_68_face_landmarks.dat'))
|
|
51
|
+
resource_file = files('alignfaces.data').joinpath('shape_predictor_68_face_landmarks.dat')
|
|
52
|
+
predictor = dlib.shape_predictor(str(resource_file))
|
|
53
|
+
shape = predictor(InputImage, rect) # type: dlib.full_object_detection
|
|
54
|
+
shape = np.array([[tp.x, tp.y] for tp in shape.parts()]) # np (68 x XY)
|
|
55
|
+
|
|
56
|
+
assert len(shape)==68, 'Number of points returned by predictor must be 68.'
|
|
57
|
+
|
|
58
|
+
JAWLINE_POINTS = list(range(0, 17))
|
|
59
|
+
RIGHT_EYEBROW_POINTS = list(range(17, 22))
|
|
60
|
+
LEFT_EYEBROW_POINTS = list(range(22, 27))
|
|
61
|
+
NOSE_POINTS = list(range(27, 36))
|
|
62
|
+
RIGHT_EYE_POINTS = list(range(36, 42))
|
|
63
|
+
LEFT_EYE_POINTS = list(range(42, 48))
|
|
64
|
+
MOUTH_OUTLINE_POINTS = list(range(48, 61))
|
|
65
|
+
MOUTH_INNER_POINTS = list(range(61, 68))
|
|
66
|
+
Landmarks = {'JAWLINE_POINTS':shape[JAWLINE_POINTS,:], 'RIGHT_EYEBROW_POINTS':shape[RIGHT_EYEBROW_POINTS,:], \
|
|
67
|
+
'LEFT_EYEBROW_POINTS':shape[LEFT_EYEBROW_POINTS,:], 'NOSE_POINTS':shape[NOSE_POINTS,:], \
|
|
68
|
+
'RIGHT_EYE_POINTS':shape[RIGHT_EYE_POINTS,:], 'LEFT_EYE_POINTS':shape[LEFT_EYE_POINTS,:], \
|
|
69
|
+
'MOUTH_OUTLINE_POINTS':shape[MOUTH_OUTLINE_POINTS,:], 'MOUTH_INNER_POINTS':shape[MOUTH_INNER_POINTS,:]}
|
|
70
|
+
return Landmarks
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# def Iris(InputImage,Landmarks):
|
|
75
|
+
# assert InputImage.ndim==2, 'InputImage should be 2 dimensions.'
|
|
76
|
+
# nrows, ncols = InputImage.shape
|
|
77
|
+
# selem = disk(6) # use for dilation of binary maps
|
|
78
|
+
# M, m, EO = phase_congruency(InputImage, 4, 6)
|
|
79
|
+
# edges2 = feature.canny(M)
|
|
80
|
+
#
|
|
81
|
+
# # Relevant labels for Landmarks
|
|
82
|
+
# labels = dict()
|
|
83
|
+
# labels[0] = 'LEFT_EYE_POINTS'
|
|
84
|
+
# labels[1] = 'RIGHT_EYE_POINTS'
|
|
85
|
+
# IrisPoints = dict()
|
|
86
|
+
#
|
|
87
|
+
# hough_radii = np.arange(6, 9, 1) # range of radii to search
|
|
88
|
+
#
|
|
89
|
+
# for fi in range(2):
|
|
90
|
+
# x = Landmarks[labels[fi]][:,0]
|
|
91
|
+
# y = Landmarks[labels[fi]][:,1]
|
|
92
|
+
# tempbw = np.zeros((nrows,ncols))
|
|
93
|
+
# tempbw[y, x] = 1
|
|
94
|
+
# ch = convex_hull_image(tempbw)
|
|
95
|
+
# dilated = dilation(ch, selem)
|
|
96
|
+
#
|
|
97
|
+
# hough_res = hough_circle(dilated * edges2, hough_radii)
|
|
98
|
+
# accums, cx, cy, radii = hough_circle_peaks(hough_res, hough_radii,total_num_peaks=8)
|
|
99
|
+
#
|
|
100
|
+
# # Choose minimum of average pixel value within intersection of iris-disc and eye
|
|
101
|
+
# total_gray = [0] * max(accums.shape)
|
|
102
|
+
# i = -1
|
|
103
|
+
# for center_y, center_x, radius in zip(cy, cx, radii):
|
|
104
|
+
# i = i + 1
|
|
105
|
+
# circy, circx = circle_perimeter(center_y, center_x, radius)
|
|
106
|
+
# tempbw = np.zeros((nrows,ncols))
|
|
107
|
+
# tempbw[circy, circx] = 1
|
|
108
|
+
# ThisBw = convex_hull_image(tempbw)
|
|
109
|
+
# total_gray[i] = np.sum(ThisBw * ch * InputImage) / np.sum(ThisBw * ch)
|
|
110
|
+
#
|
|
111
|
+
# best_of_hough = total_gray.index(min(total_gray))
|
|
112
|
+
# IrisPoints[fi] = (cy[best_of_hough], cx[best_of_hough], radii[best_of_hough])
|
|
113
|
+
#
|
|
114
|
+
# return IrisPoints
|
|
115
|
+
|
|
116
|
+
def unpack_dict_to_vector_xy(Landmarks):
|
|
117
|
+
LabelToCoordinateIndex = dict()
|
|
118
|
+
CoordinateVector = []
|
|
119
|
+
LabelToCoordinateIndex = dict()
|
|
120
|
+
starti = 0
|
|
121
|
+
for key in Landmarks.keys():
|
|
122
|
+
if isinstance(Landmarks[key], np.ndarray):
|
|
123
|
+
vform = Landmarks[key].flatten()
|
|
124
|
+
else:
|
|
125
|
+
vform = Landmarks[key]
|
|
126
|
+
CoordinateVector.append(vform)
|
|
127
|
+
endi = starti + len(vform)
|
|
128
|
+
LabelToCoordinateIndex[key] = list(range(starti, endi))
|
|
129
|
+
starti = endi
|
|
130
|
+
CoordinateVector = list(itertools.chain.from_iterable(CoordinateVector))
|
|
131
|
+
return CoordinateVector, LabelToCoordinateIndex
|
|
132
|
+
|
|
133
|
+
def pack_vector_xy_as_dict(CoordinateVector, LabelToCoordinateIndex):
|
|
134
|
+
Landmarks = dict()
|
|
135
|
+
for key in LabelToCoordinateIndex.keys():
|
|
136
|
+
starti = LabelToCoordinateIndex[key][0]
|
|
137
|
+
endi = LabelToCoordinateIndex[key][-1]
|
|
138
|
+
clist = CoordinateVector[ starti : endi + 1 ]
|
|
139
|
+
ArrayForThisKey = (np.asarray(clist)).reshape((int((endi-starti+1)/2), 2))
|
|
140
|
+
Landmarks[key] = ArrayForThisKey
|
|
141
|
+
return Landmarks
|
|
142
|
+
|
|
143
|
+
def pull_jawline_to_inside_of_hairline(Landmarks, Face):
|
|
144
|
+
|
|
145
|
+
# left side
|
|
146
|
+
cstart = Landmarks['JAWLINE_POINTS'][0,0] + Landmarks['JAWLINE_POINTS'][0,1]*1j
|
|
147
|
+
cend = Landmarks['JAWLINE_POINTS'][1,0] + Landmarks['JAWLINE_POINTS'][1,1]*1j
|
|
148
|
+
xy = np.linspace(cstart, cend, 5)
|
|
149
|
+
chunkA = np.array([np.real(xy), np.imag(xy)]).T
|
|
150
|
+
|
|
151
|
+
cstart = Landmarks['JAWLINE_POINTS'][1,0] + Landmarks['JAWLINE_POINTS'][1,1]*1j
|
|
152
|
+
cend = Landmarks['JAWLINE_POINTS'][2,0] + Landmarks['JAWLINE_POINTS'][2,1]*1j
|
|
153
|
+
xy = np.linspace(cstart, cend, 5)
|
|
154
|
+
chunkB = np.array([np.real(xy), np.imag(xy)]).T
|
|
155
|
+
|
|
156
|
+
cstart = Landmarks['JAWLINE_POINTS'][2,0] + Landmarks['JAWLINE_POINTS'][2,1]*1j
|
|
157
|
+
cend = Landmarks['JAWLINE_POINTS'][3,0] + Landmarks['JAWLINE_POINTS'][3,1]*1j
|
|
158
|
+
xy = np.linspace(cstart, cend, 5)
|
|
159
|
+
chunkC = np.array([np.real(xy), np.imag(xy)]).T
|
|
160
|
+
|
|
161
|
+
upsampled_points = np.append(chunkA, chunkB[1:,:], axis=0)
|
|
162
|
+
upsampled_points = np.append(upsampled_points, chunkC[1:,:], axis=0)
|
|
163
|
+
|
|
164
|
+
init = upsampled_points
|
|
165
|
+
|
|
166
|
+
if (int((skimage.__version__).split(".")[1]) > 19):
|
|
167
|
+
# switch columns of init
|
|
168
|
+
init = np.roll(init, 1, axis=1)
|
|
169
|
+
snake = active_contour(Face, init, boundary_condition='free-fixed',
|
|
170
|
+
alpha=0.1, beta=1.0, w_line=0, w_edge=5,
|
|
171
|
+
gamma=0.1, convergence=0.01)
|
|
172
|
+
# switch columns of snake
|
|
173
|
+
snake = np.roll(snake, 1, axis=1)
|
|
174
|
+
elif (int((skimage.__version__).split(".")[1]) > 15):
|
|
175
|
+
# switch columns of init
|
|
176
|
+
init = np.roll(init, 1, axis=1)
|
|
177
|
+
snake = active_contour(Face, init, boundary_condition='free-fixed',
|
|
178
|
+
alpha=0.1, beta=1.0, w_line=0, w_edge=5,
|
|
179
|
+
gamma=0.1, convergence=0.01, coordinates='rc')
|
|
180
|
+
# switch columns of snake
|
|
181
|
+
snake = np.roll(snake, 1, axis=1)
|
|
182
|
+
else:
|
|
183
|
+
snake = active_contour(Face, init, bc='free-fixed',
|
|
184
|
+
alpha=0.1, beta=1.0, w_line=0, w_edge=5,
|
|
185
|
+
gamma=0.1, convergence=0.01)
|
|
186
|
+
|
|
187
|
+
Landmarks['JAWLINE_POINTS'][0:3,:] = snake[0:12:4,:]
|
|
188
|
+
|
|
189
|
+
# right side
|
|
190
|
+
cstart = Landmarks['JAWLINE_POINTS'][-1,0] + Landmarks['JAWLINE_POINTS'][-1,1]*1j
|
|
191
|
+
cend = Landmarks['JAWLINE_POINTS'][-2,0] + Landmarks['JAWLINE_POINTS'][-2,1]*1j
|
|
192
|
+
xy = np.linspace(cstart, cend, 5)
|
|
193
|
+
chunkA = np.array([np.real(xy), np.imag(xy)]).T
|
|
194
|
+
|
|
195
|
+
cstart = Landmarks['JAWLINE_POINTS'][-2,0] + Landmarks['JAWLINE_POINTS'][-2,1]*1j
|
|
196
|
+
cend = Landmarks['JAWLINE_POINTS'][-3,0] + Landmarks['JAWLINE_POINTS'][-3,1]*1j
|
|
197
|
+
xy = np.linspace(cstart, cend, 5)
|
|
198
|
+
chunkB = np.array([np.real(xy), np.imag(xy)]).T
|
|
199
|
+
|
|
200
|
+
cstart = Landmarks['JAWLINE_POINTS'][-3,0] + Landmarks['JAWLINE_POINTS'][-3,1]*1j
|
|
201
|
+
cend = Landmarks['JAWLINE_POINTS'][-4,0] + Landmarks['JAWLINE_POINTS'][-4,1]*1j
|
|
202
|
+
xy = np.linspace(cstart, cend, 5)
|
|
203
|
+
chunkC = np.array([np.real(xy), np.imag(xy)]).T
|
|
204
|
+
|
|
205
|
+
upsampled_points = np.append(chunkA, chunkB[1:, :], axis=0)
|
|
206
|
+
upsampled_points = np.append(upsampled_points, chunkC[1:, :], axis=0)
|
|
207
|
+
|
|
208
|
+
init = upsampled_points
|
|
209
|
+
|
|
210
|
+
if (int((skimage.__version__).split(".")[1]) > 19):
|
|
211
|
+
# switch columns of init
|
|
212
|
+
init = np.roll(init, 1, axis=1)
|
|
213
|
+
snake = active_contour(Face, init, boundary_condition='free-fixed',
|
|
214
|
+
alpha=0.1, beta=1.0, w_line=0, w_edge=5,
|
|
215
|
+
gamma=0.1, convergence=0.01)
|
|
216
|
+
# switch columns of snake
|
|
217
|
+
snake = np.roll(snake, 1, axis=1)
|
|
218
|
+
elif (int((skimage.__version__).split(".")[1]) > 15):
|
|
219
|
+
# switch columns of init
|
|
220
|
+
init = np.roll(init, 1, axis=1)
|
|
221
|
+
snake = active_contour(Face, init, boundary_condition='free-fixed',
|
|
222
|
+
alpha=0.1, beta=1.0, w_line=0, w_edge=5,
|
|
223
|
+
gamma=0.1, convergence=0.01, coordinates='rc')
|
|
224
|
+
# switch columns of snake
|
|
225
|
+
snake = np.roll(snake, 1, axis=1)
|
|
226
|
+
else:
|
|
227
|
+
snake = active_contour(Face, init, bc='free-fixed', alpha=0.1,
|
|
228
|
+
beta=1.0, w_line=0, w_edge=5, gamma=0.1,
|
|
229
|
+
convergence=0.01)
|
|
230
|
+
|
|
231
|
+
Landmarks['JAWLINE_POINTS'][-1:-4:-1, :] = snake[0:12:4, :]
|
|
232
|
+
return Landmarks
|
|
233
|
+
###############################################################################
|