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
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from alignfaces.warp_tools import bilin
|
|
3
|
+
from alignfaces.warp_tools import pawarp
|
|
4
|
+
from numpy.fft import fft2, ifft2
|
|
5
|
+
|
|
6
|
+
#################################################################
|
|
7
|
+
# HELPER FUNCTIONS
|
|
8
|
+
#################################################################
|
|
9
|
+
def _make_pixarr(limits):
|
|
10
|
+
xmn, ymn = limits[0], limits[0]
|
|
11
|
+
xmx, ymx = limits[1], limits[1]
|
|
12
|
+
x = np.arange(xmn, xmx + 1)
|
|
13
|
+
y = np.arange(ymn, ymx + 1)
|
|
14
|
+
xx, yy = np.meshgrid(x, y)
|
|
15
|
+
pixarr = (xx.flatten(), yy.flatten())
|
|
16
|
+
pixarr = np.transpose(np.array(pixarr))
|
|
17
|
+
return pixarr
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _random_locations(im_size, num_loc=4, min_dist=5):
|
|
21
|
+
"""create array of num_loc points within min distance of min_dist pixels"""
|
|
22
|
+
PAD = int(im_size * .15)
|
|
23
|
+
locations = (np.random.rand(1, 2) * (im_size-1-PAD*2)+PAD).astype(int)
|
|
24
|
+
total = 1
|
|
25
|
+
while total < num_loc:
|
|
26
|
+
# Sample new point.
|
|
27
|
+
loc = (np.random.rand(1, 2) * (im_size-1-PAD*2)+PAD).astype(int)
|
|
28
|
+
|
|
29
|
+
SAMPLE_NEW = 0;
|
|
30
|
+
for SETLOC in locations:
|
|
31
|
+
if np.sqrt(((SETLOC-loc)**2).sum()) <= min_dist:
|
|
32
|
+
# Bad sample. Skip to beginning of loop for new sample.
|
|
33
|
+
SAMPLE_NEW = 1;
|
|
34
|
+
if SAMPLE_NEW:
|
|
35
|
+
continue
|
|
36
|
+
|
|
37
|
+
# Good sample. Append to results.
|
|
38
|
+
locations = np.r_[locations, loc]
|
|
39
|
+
total += 1
|
|
40
|
+
IND = _locations_to_indices(im_size, locations)
|
|
41
|
+
locations = _indices_to_locations(im_size, IND)
|
|
42
|
+
return locations, IND
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _perturb_locations(locations, max_value, change_range):
|
|
46
|
+
num_targets = locations.shape[0]
|
|
47
|
+
noise = np.random.rand(num_targets, 2)
|
|
48
|
+
other_locations = ((noise-.5) * change_range + locations).astype(int)
|
|
49
|
+
other_locations[other_locations < 0] = 0
|
|
50
|
+
other_locations[other_locations > max_value] = max_value
|
|
51
|
+
return other_locations
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _image_with_donut_targets(im_size=64, locations=((8, 8),(32, 32))):
|
|
55
|
+
"""make zero image with targets at locations. also return target"""
|
|
56
|
+
# Target
|
|
57
|
+
B = np.zeros((3, 3))
|
|
58
|
+
|
|
59
|
+
B[1, 1] = 9 * 8
|
|
60
|
+
B[0, 1] = -9
|
|
61
|
+
B[2, 1] = -9
|
|
62
|
+
B[1, 0] = -9
|
|
63
|
+
B[1, 2] = -9
|
|
64
|
+
|
|
65
|
+
B[0, 0] = -9
|
|
66
|
+
B[0, 2] = -9
|
|
67
|
+
B[2, 0] = -9
|
|
68
|
+
B[2, 2] = -9
|
|
69
|
+
|
|
70
|
+
# Image with targets
|
|
71
|
+
A = np.zeros((im_size, im_size))
|
|
72
|
+
for l in locations:
|
|
73
|
+
tr, tc = l
|
|
74
|
+
A[1 + tr - 1, 1 + tc - 1] = B[1, 1]
|
|
75
|
+
A[0 + tr - 1, 1 + tc - 1] = B[0, 1]
|
|
76
|
+
A[2 + tr - 1, 1 + tc - 1] = B[2, 1]
|
|
77
|
+
A[1 + tr - 1, 0 + tc - 1] = B[1, 0]
|
|
78
|
+
A[1 + tr - 1, 2 + tc - 1] = B[1, 2]
|
|
79
|
+
|
|
80
|
+
A[0 + tr - 1, 0 + tc - 1] = B[0, 0]
|
|
81
|
+
A[0 + tr - 1, 2 + tc - 1] = B[0, 2]
|
|
82
|
+
A[2 + tr - 1, 0 + tc - 1] = B[2, 0]
|
|
83
|
+
A[2 + tr - 1, 2 + tc - 1] = B[2, 2]
|
|
84
|
+
|
|
85
|
+
img, target = A, B
|
|
86
|
+
return img, target
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _fft_convolve2d(x, y):
|
|
90
|
+
""" 2D convolution, using FFT"""
|
|
91
|
+
pad = np.array(x.shape) - np.array(y.shape)
|
|
92
|
+
if pad[0] % 2 == 0:
|
|
93
|
+
rb, ra = int(pad[0]/2)+1, int(pad[0]/2)-1
|
|
94
|
+
else:
|
|
95
|
+
rb, ra = int(np.ceil(pad[0]/2)), int(np.floor(pad[0]/2))
|
|
96
|
+
if pad[1] % 2 == 0:
|
|
97
|
+
cb, ca = int(pad[1]/2)+1, int(pad[1]/2)-1
|
|
98
|
+
else:
|
|
99
|
+
cb, ca = int(np.ceil(pad[1]/2)), int(np.floor(pad[1]/2))
|
|
100
|
+
pad_width = ((rb, ra), (cb, ca))
|
|
101
|
+
py = np.pad(y, pad_width, mode="constant")
|
|
102
|
+
|
|
103
|
+
fr = fft2(x)
|
|
104
|
+
fr2 = fft2(np.flipud(np.fliplr(py)))
|
|
105
|
+
m,n = fr.shape
|
|
106
|
+
cc = np.real(ifft2(fr*fr2))
|
|
107
|
+
cc = np.roll(cc, int(-m/2+1), axis=0)
|
|
108
|
+
cc = np.roll(cc, int(-n/2+1), axis=1)
|
|
109
|
+
return cc
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _locations_to_indices(img_width, locations):
|
|
113
|
+
num_targets = locations.shape[0]
|
|
114
|
+
IND = np.zeros(num_targets, )
|
|
115
|
+
for i, rc in enumerate(locations):
|
|
116
|
+
r, c = rc
|
|
117
|
+
this_ind = r * img_width + c
|
|
118
|
+
IND[i] = this_ind
|
|
119
|
+
IND.sort()
|
|
120
|
+
return IND.astype(int)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _indices_to_locations(img_width, IND):
|
|
124
|
+
IND.sort()
|
|
125
|
+
num_targets = IND.size
|
|
126
|
+
SUB = np.zeros((num_targets, 2), dtype=int)
|
|
127
|
+
for i, ind in enumerate(IND):
|
|
128
|
+
r = int(np.floor(ind / img_width))
|
|
129
|
+
c = int(ind % img_width)
|
|
130
|
+
SUB[i, :] = [r, c]
|
|
131
|
+
return SUB
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _top_n_locations(C, num_targets):
|
|
135
|
+
cv = np.copy(C).flatten()
|
|
136
|
+
si = cv.argsort()
|
|
137
|
+
IND = si[-num_targets:]
|
|
138
|
+
IND.sort()
|
|
139
|
+
return IND
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _top_n_locations_robust(C, num_targets):
|
|
143
|
+
CC = np.copy(C)
|
|
144
|
+
SUB = np.zeros((num_targets, 2), dtype=int)
|
|
145
|
+
for i in range(num_targets):
|
|
146
|
+
MC = np.where(CC==CC.max())
|
|
147
|
+
r, c = MC[0][0], MC[1][0]
|
|
148
|
+
SUB[i, :] = [r, c]
|
|
149
|
+
# set 5 x 5 area centered on (r, c) to 0
|
|
150
|
+
CC[r-1, c-1] = 0
|
|
151
|
+
CC[r-1, c+0] = 0
|
|
152
|
+
CC[r-1, c+1] = 0
|
|
153
|
+
CC[r+0, c-1] = 0
|
|
154
|
+
CC[r+0, c+0] = 0
|
|
155
|
+
CC[r+0, c+1] = 0
|
|
156
|
+
CC[r+1, c-1] = 0
|
|
157
|
+
CC[r+1, c+0] = 0
|
|
158
|
+
CC[r+1, c+1] = 0
|
|
159
|
+
|
|
160
|
+
CC[r-2, c-2] = 0
|
|
161
|
+
CC[r-2, c-1] = 0
|
|
162
|
+
CC[r-2, c-0] = 0
|
|
163
|
+
CC[r-2, c+1] = 0
|
|
164
|
+
CC[r-2, c+2] = 0
|
|
165
|
+
|
|
166
|
+
CC[r-1, c-2] = 0
|
|
167
|
+
CC[r-1, c+2] = 0
|
|
168
|
+
|
|
169
|
+
CC[r-0, c-2] = 0
|
|
170
|
+
CC[r-0, c+2] = 0
|
|
171
|
+
|
|
172
|
+
CC[r+1, c-2] = 0
|
|
173
|
+
CC[r+1, c+2] = 0
|
|
174
|
+
|
|
175
|
+
CC[r+2, c-2] = 0
|
|
176
|
+
CC[r+2, c-1] = 0
|
|
177
|
+
CC[r+2, c-0] = 0
|
|
178
|
+
CC[r+2, c+1] = 0
|
|
179
|
+
CC[r+2, c+2] = 0
|
|
180
|
+
IND = _locations_to_indices(CC.shape[1], SUB)
|
|
181
|
+
SUB = _indices_to_locations(CC.shape[1], IND)
|
|
182
|
+
return SUB, IND
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _distances_of_best_matched_points(locations, top_locations):
|
|
186
|
+
# every location matched with every estimated location
|
|
187
|
+
num_targets = locations.shape[0]
|
|
188
|
+
O = locations
|
|
189
|
+
N = top_locations
|
|
190
|
+
DELTA = np.kron(O, np.ones((num_targets, 1))) - np.tile(N, (num_targets, 1))
|
|
191
|
+
DIST = np.sqrt((DELTA**2).sum(axis=1))
|
|
192
|
+
D = DIST.reshape((num_targets, num_targets))
|
|
193
|
+
|
|
194
|
+
# distances between points and estimated points giving best-match
|
|
195
|
+
MIN_D = np.zeros((num_targets,))
|
|
196
|
+
for di in range(num_targets):
|
|
197
|
+
MIN_D[di] = D.min();
|
|
198
|
+
locs = np.where(D==D.min())
|
|
199
|
+
R, C = locs[0][0], locs[1][0]
|
|
200
|
+
D[R,:] = np.inf
|
|
201
|
+
D[:,C] = np.inf
|
|
202
|
+
return MIN_D
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
#################################################################
|
|
206
|
+
# DEFINE FUNCTIONS USED FOR UNIT TESTING
|
|
207
|
+
#################################################################
|
|
208
|
+
def test_bilin_with_integer_coordinates():
|
|
209
|
+
"""Simple test where warped image should be same as original."""
|
|
210
|
+
im = np.array([[1, 2], [3, 4]])
|
|
211
|
+
xy = (np.array([0, 1, 0, 1]), np.array([0, 0, 1, 1]))
|
|
212
|
+
nRGB = 1
|
|
213
|
+
RGBsub = np.array([0, 0, 0, 0])
|
|
214
|
+
out = bilin(im, xy, nRGB, RGBsub)
|
|
215
|
+
expected_output = im.flatten()
|
|
216
|
+
assert np.allclose(out, expected_output)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_bilin_with_integer_coordinates_rgb():
|
|
220
|
+
"""Simple test where warped RGB image should be same as original."""
|
|
221
|
+
im = np.array([[1, 2], [3, 4]])
|
|
222
|
+
xy = (np.array([0, 1, 0, 1]), np.array([0, 0, 1, 1]))
|
|
223
|
+
nRGB = 3
|
|
224
|
+
RGB = np.stack((im, ) * 3, axis=-1)
|
|
225
|
+
RGBsub = np.array([0, 0, 0, 0])
|
|
226
|
+
for i in range(1, nRGB):
|
|
227
|
+
append_this = np.ones((4,)) * i
|
|
228
|
+
RGBsub = np.hstack((RGBsub, append_this))
|
|
229
|
+
RGBsub = RGBsub.astype(int)
|
|
230
|
+
out = bilin(RGB, xy, nRGB, RGBsub)
|
|
231
|
+
|
|
232
|
+
pixarr = _make_pixarr([0, 1])
|
|
233
|
+
pixarr_tiled = np.tile(pixarr, (3, 1))
|
|
234
|
+
expected_output = RGB[pixarr_tiled[:, 1], pixarr_tiled[:, 0], RGBsub]
|
|
235
|
+
assert np.allclose(out, expected_output)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def test_bilin_with_real_valued_coordinates():
|
|
239
|
+
"""Expected warped image is easy to calculate by hand."""
|
|
240
|
+
im = np.array([[28, 24], [16, 28]])
|
|
241
|
+
xy = ([0.75], [0.60])
|
|
242
|
+
nRGB = 1
|
|
243
|
+
RGBsub = [0]
|
|
244
|
+
out = bilin(im, xy, nRGB, RGBsub)
|
|
245
|
+
|
|
246
|
+
p = xy[0][0]
|
|
247
|
+
linear1 = (1 - p) * im[0, 0] + p * im[0, 1]
|
|
248
|
+
linear2 = (1 - p) * im[1, 0] + p * im[1, 1]
|
|
249
|
+
p = xy[1][0]
|
|
250
|
+
expected_output = (1 - p) * linear1 + p * linear2
|
|
251
|
+
assert np.allclose(out[0], expected_output)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def test_pawarp():
|
|
255
|
+
# Parameters - input image with targets centered on landmarks (locations)
|
|
256
|
+
im_size = 128 # length of square image, pixels
|
|
257
|
+
num_targets = 3 # number of targets
|
|
258
|
+
min_target_dist = 32 # minimum distance between landmarks, pixels
|
|
259
|
+
|
|
260
|
+
# Parameters - warped version of input image
|
|
261
|
+
change_range = 8 # range of random shift in landmark position, pixels
|
|
262
|
+
max_value=im_size-1 # maximum x and y position. minimum always 0.
|
|
263
|
+
|
|
264
|
+
# Parameters - test
|
|
265
|
+
iterations = 1000
|
|
266
|
+
ALL_D = np.zeros((iterations, num_targets))
|
|
267
|
+
|
|
268
|
+
# Perform multiple tests.
|
|
269
|
+
for this_it in range(iterations):
|
|
270
|
+
# Create image with cross-shaped targets
|
|
271
|
+
locations, target_indices = _random_locations(im_size, num_targets, min_target_dist)
|
|
272
|
+
img, target = _image_with_donut_targets(im_size, locations)
|
|
273
|
+
|
|
274
|
+
# # Ensure that all helper functions are working properly.
|
|
275
|
+
# # Should be able to find exact target positions using convolution.
|
|
276
|
+
# C = fft_convolve2d(img, target)
|
|
277
|
+
# top_indices = top_n_locations(C, num_targets)
|
|
278
|
+
# working_functions = (top_indices==target_indices).all()
|
|
279
|
+
# assert working_functions, "Helper functions do not work: Cannot interpret test."
|
|
280
|
+
|
|
281
|
+
# Warp centers of targets to slightly different locations
|
|
282
|
+
new_locations = _perturb_locations(locations, max_value, change_range)
|
|
283
|
+
wimg, tri, inpix, fwdwarpix = pawarp(img, base=new_locations, target=locations, interp='bilin')
|
|
284
|
+
|
|
285
|
+
# Recover the original image by warping back
|
|
286
|
+
wwimg, tri, inpix, fwdwarpix = pawarp(wimg, base=locations, target=new_locations, interp='bilin')
|
|
287
|
+
|
|
288
|
+
# Look for the targets in the recovered image
|
|
289
|
+
C = _fft_convolve2d(wwimg, target)
|
|
290
|
+
estimated_locations, top_indices = _top_n_locations_robust(C, num_targets)
|
|
291
|
+
|
|
292
|
+
# plt_1 = plt.figure(figsize=(10,10))
|
|
293
|
+
# plt.imshow(wwimg, cmap="gray")
|
|
294
|
+
# plt.plot(locations[:,1], locations[:,0], 'g.', markersize=24, alpha=0.5)
|
|
295
|
+
# plt.plot(estimated_locations[:,1], estimated_locations[:,0], 'r+', markersize=24, alpha=0.5)
|
|
296
|
+
# plt.show()
|
|
297
|
+
|
|
298
|
+
best_distances = _distances_of_best_matched_points(locations, estimated_locations)
|
|
299
|
+
# print("Distances of best-matched points: ", best_distances)
|
|
300
|
+
# print(this_it)
|
|
301
|
+
|
|
302
|
+
ALL_D[this_it, :] = best_distances
|
|
303
|
+
|
|
304
|
+
# Evaluate
|
|
305
|
+
ALL_D = ALL_D.flatten()
|
|
306
|
+
Q95_less_than_1_pixel = np.quantile(ALL_D, 0.95) < 1
|
|
307
|
+
max_offset_less_than_perturbation = ALL_D.max() < change_range
|
|
308
|
+
PASS = Q95_less_than_1_pixel and max_offset_less_than_perturbation
|
|
309
|
+
assert PASS
|
|
310
|
+
#################################################################
|
|
311
|
+
# UNIT TESTS
|
|
312
|
+
#################################################################
|
|
313
|
+
test_bilin_with_integer_coordinates()
|
|
314
|
+
test_bilin_with_integer_coordinates_rgb()
|
|
315
|
+
test_bilin_with_real_valued_coordinates()
|
|
316
|
+
test_pawarp()
|
alignfaces/warp_tools.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy.spatial import Delaunay
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
# bilinear interpolation
|
|
6
|
+
#
|
|
7
|
+
# input:
|
|
8
|
+
# array image, numpy array.
|
|
9
|
+
# xy image coordinates, positive real valued, tuple of arrays.
|
|
10
|
+
# (length is number of pixels in array)
|
|
11
|
+
# nRGB number of color channels (third dimension of array).
|
|
12
|
+
# RGBsub channel index for each pixel in array, numpy array
|
|
13
|
+
# (length is number of pixels in array)
|
|
14
|
+
def bilin(array, xy, nRGB, RGBsub):
|
|
15
|
+
|
|
16
|
+
assert (array.ndim == 2) or (array.ndim == 3)
|
|
17
|
+
if (array.ndim == 3):
|
|
18
|
+
assert (array.shape[2] == nRGB)
|
|
19
|
+
assert (len(RGBsub) == len(xy[0]) * nRGB)
|
|
20
|
+
|
|
21
|
+
# Zero padding.
|
|
22
|
+
# Handles solution to bad cases where subscripts pushed beyond image.
|
|
23
|
+
if nRGB == 1:
|
|
24
|
+
array = np.hstack((array, np.zeros((array.shape[0], 1))))
|
|
25
|
+
array = np.vstack((array, np.zeros((1, array.shape[1]))))
|
|
26
|
+
else:
|
|
27
|
+
#
|
|
28
|
+
# above works for single-channel grayscale.
|
|
29
|
+
# now for RGB
|
|
30
|
+
# print("In bilin:")
|
|
31
|
+
# print(array.shape)
|
|
32
|
+
# print(np.zeros((nRGB, array.shape[0], 1)).shape)
|
|
33
|
+
# array = np.concatenate((array,
|
|
34
|
+
# np.zeros((nRGB, array.shape[0], 1))), axis=2)
|
|
35
|
+
# array = np.concatenate((array,
|
|
36
|
+
# np.zeros((nRGB, 1, array.shape[1]))), axis=1)
|
|
37
|
+
array = np.concatenate((array,
|
|
38
|
+
np.zeros((array.shape[0], 1, nRGB))), axis=1)
|
|
39
|
+
array = np.concatenate((array,
|
|
40
|
+
np.zeros((1, array.shape[1], nRGB))), axis=0)
|
|
41
|
+
|
|
42
|
+
xy = (np.tile(xy[0], (nRGB, 1)).flatten(),
|
|
43
|
+
np.tile(xy[1], (nRGB, 1)).flatten())
|
|
44
|
+
|
|
45
|
+
ur = (np.ceil(xy[0]).astype(int), np.floor(xy[1]).astype(int))
|
|
46
|
+
ul = (np.floor(xy[0]).astype(int), np.floor(xy[1]).astype(int))
|
|
47
|
+
br = (np.ceil(xy[0]).astype(int), np.ceil(xy[1]).astype(int))
|
|
48
|
+
bl = (np.floor(xy[0]).astype(int), np.ceil(xy[1]).astype(int))
|
|
49
|
+
|
|
50
|
+
bad = br[0] == ul[0]
|
|
51
|
+
ur[0][bad] += 1
|
|
52
|
+
br[0][bad] += 1
|
|
53
|
+
|
|
54
|
+
bad = bl[1] == ul[1]
|
|
55
|
+
br[1][bad] += 1
|
|
56
|
+
bl[1][bad] += 1
|
|
57
|
+
|
|
58
|
+
indbl = np.ravel_multi_index((bl[1], bl[0], RGBsub),
|
|
59
|
+
(array.shape[0], array.shape[1], nRGB))
|
|
60
|
+
indbr = np.ravel_multi_index((br[1], br[0], RGBsub),
|
|
61
|
+
(array.shape[0], array.shape[1], nRGB))
|
|
62
|
+
indul = np.ravel_multi_index((ul[1], ul[0], RGBsub),
|
|
63
|
+
(array.shape[0], array.shape[1], nRGB))
|
|
64
|
+
indur = np.ravel_multi_index((ur[1], ur[0], RGBsub),
|
|
65
|
+
(array.shape[0], array.shape[1], nRGB))
|
|
66
|
+
|
|
67
|
+
vecarray = array.flatten()
|
|
68
|
+
|
|
69
|
+
denom = br[0] - bl[0]
|
|
70
|
+
num_a = br[0] - xy[0]
|
|
71
|
+
num_b = xy[0] - bl[0]
|
|
72
|
+
x1out = ((num_a / denom) * vecarray[indbl] +
|
|
73
|
+
(num_b / denom) * vecarray[indbr])
|
|
74
|
+
|
|
75
|
+
x2out = ((num_a / denom) * vecarray[indul] +
|
|
76
|
+
(num_b / denom) * vecarray[indur])
|
|
77
|
+
|
|
78
|
+
denom = ul[1] - bl[1]
|
|
79
|
+
num_a = ul[1] - xy[1]
|
|
80
|
+
num_b = xy[1] - bl[1]
|
|
81
|
+
out = (num_a / denom) * x1out + (num_b / denom) * x2out
|
|
82
|
+
return out
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# piecewise affine warp of target image ''im'' from target to base coords.
|
|
86
|
+
#
|
|
87
|
+
# input:
|
|
88
|
+
# im image, numpy array.
|
|
89
|
+
# base landmarks, rows are [x,y] points, numpy array.
|
|
90
|
+
# target landmarks, rows are [x,y] points, numpy array.
|
|
91
|
+
def pawarp(im, base, target, interp='bilin'):
|
|
92
|
+
# base & target should be numpy arrays, vertices X coordinate [x/y]
|
|
93
|
+
isuint8 = False
|
|
94
|
+
if (im.dtype == 'uint8'):
|
|
95
|
+
isuint8 = True
|
|
96
|
+
assert (im.ndim == 2) or (im.ndim == 3)
|
|
97
|
+
if (im.ndim == 3):
|
|
98
|
+
nRGB = im.shape[2]
|
|
99
|
+
else:
|
|
100
|
+
nRGB = 1
|
|
101
|
+
imdims = (im.shape[0], im.shape[1])
|
|
102
|
+
warpim = np.zeros((im.shape[0], im.shape[1], nRGB))
|
|
103
|
+
|
|
104
|
+
nverts = base.shape[0]
|
|
105
|
+
nverts2 = target.shape[0]
|
|
106
|
+
assert (nverts == nverts2)
|
|
107
|
+
|
|
108
|
+
boxpix = np.array([[1, 1], [1, imdims[0]],
|
|
109
|
+
[imdims[1], 1], [imdims[1], imdims[0]]])
|
|
110
|
+
boxpix -= 1
|
|
111
|
+
|
|
112
|
+
pix1 = base.astype(float)
|
|
113
|
+
pix2 = target.astype(float)
|
|
114
|
+
|
|
115
|
+
pix1 = np.vstack((pix1, boxpix))
|
|
116
|
+
pix2 = np.vstack((pix2, boxpix))
|
|
117
|
+
|
|
118
|
+
# Perform Delaunay triangulation on pixel coordinates of base vertices.
|
|
119
|
+
dt = Delaunay(pix1)
|
|
120
|
+
tri = dt.simplices
|
|
121
|
+
ntri = tri.shape[0]
|
|
122
|
+
|
|
123
|
+
# Get the first, second, and third vertex for each triangle (x--coords).
|
|
124
|
+
xio = pix1[tri[:, 0], 0]
|
|
125
|
+
xi = pix2[tri[:, 0], 0]
|
|
126
|
+
xjo = pix1[tri[:, 1], 0]
|
|
127
|
+
xj = pix2[tri[:, 1], 0]
|
|
128
|
+
xko = pix1[tri[:, 2], 0]
|
|
129
|
+
xk = pix2[tri[:, 2], 0]
|
|
130
|
+
|
|
131
|
+
# Get the first, second, and third vertex for each triangle (y--coords).
|
|
132
|
+
yio = pix1[tri[:, 0], 1]
|
|
133
|
+
yi = pix2[tri[:, 0], 1]
|
|
134
|
+
yjo = pix1[tri[:, 1], 1]
|
|
135
|
+
yj = pix2[tri[:, 1], 1]
|
|
136
|
+
yko = pix1[tri[:, 2], 1]
|
|
137
|
+
yk = pix2[tri[:, 2], 1]
|
|
138
|
+
|
|
139
|
+
# Array for warp parameters (one set of params per triangle).
|
|
140
|
+
# a_i for i in 1 to 6, in equation 28 of Matthews & Baker on page 145.
|
|
141
|
+
wparams = np.zeros((ntri, 6))
|
|
142
|
+
|
|
143
|
+
# Calculate warp parameters for each triangle.
|
|
144
|
+
denom = (xjo - xio) * (yko - yio) - (yjo - yio) * (xko - xio)
|
|
145
|
+
|
|
146
|
+
wparams[:, 0] = ((xio * ((xk - xi) * (yjo - yio) - (xj - xi) * (yko - yio)) +
|
|
147
|
+
yio * ((xj - xi) * (xko - xio) - (xk - xi) * (xjo - xio))) /
|
|
148
|
+
denom + xi)
|
|
149
|
+
|
|
150
|
+
wparams[:, 3] = ((xio * ((yk - yi) * (yjo - yio) - (yj - yi) * (yko - yio)) +
|
|
151
|
+
yio * ((yj - yi) * (xko - xio) - (yk - yi) * (xjo - xio))) /
|
|
152
|
+
denom + yi)
|
|
153
|
+
|
|
154
|
+
wparams[:, 1] = ((xj - xi) * (yko - yio) - (xk - xi) * (yjo - yio)) / denom
|
|
155
|
+
|
|
156
|
+
wparams[:, 4] = ((yj - yi) * (yko - yio) - (yk - yi) * (yjo - yio)) / denom
|
|
157
|
+
|
|
158
|
+
wparams[:, 2] = ((xk - xi) * (xjo - xio) - (xj - xi) * (xko - xio)) / denom
|
|
159
|
+
|
|
160
|
+
wparams[:, 5] = ((yk - yi) * (xjo - xio) - (yj - yi) * (xko - xio)) / denom
|
|
161
|
+
|
|
162
|
+
# Determine square bounds of pixels inside base mesh.
|
|
163
|
+
xmx = int(min(np.ceil(pix1[:, 0].max()), imdims[1]))
|
|
164
|
+
xmn = int(max(np.floor(pix1[:, 0].min()), 0))
|
|
165
|
+
ymx = int(min(np.ceil(pix1[:, 1].max()), imdims[0]))
|
|
166
|
+
ymn = int(max(np.floor(pix1[:, 1].min()), 0))
|
|
167
|
+
|
|
168
|
+
# Array for pixel coordinates inside base mesh.
|
|
169
|
+
npix = im[ymn:ymx + 1, xmn:xmx + 1].size
|
|
170
|
+
pixarr = np.zeros((npix, 2))
|
|
171
|
+
|
|
172
|
+
x = np.arange(xmn, xmx + 1)
|
|
173
|
+
y = np.arange(ymn, ymx + 1)
|
|
174
|
+
xx, yy = np.meshgrid(x, y)
|
|
175
|
+
pixarr = (xx.flatten(), yy.flatten())
|
|
176
|
+
# pixind = np.ravel_multi_index((pixarr[1], pixarr[0]), imdims)
|
|
177
|
+
|
|
178
|
+
pixarr = np.transpose(np.array(pixarr))
|
|
179
|
+
inpix = dt.find_simplex(pixarr)
|
|
180
|
+
|
|
181
|
+
# Get only those pixels that are inside the convex hull.
|
|
182
|
+
isin = np.argwhere(inpix >= 0)[:, 0]
|
|
183
|
+
|
|
184
|
+
# Warp parameters for each pixel inside convex hull.
|
|
185
|
+
wp = wparams[inpix[isin], :]
|
|
186
|
+
|
|
187
|
+
fwdx = wp[:, 0] + wp[:, 1] * pixarr[isin, 0] + wp[:, 2] * pixarr[isin, 1]
|
|
188
|
+
fwdy = wp[:, 3] + wp[:, 4] * pixarr[isin, 0] + wp[:, 5] * pixarr[isin, 1]
|
|
189
|
+
if interp == 'nearest':
|
|
190
|
+
fwdx = fwdx.round().astype(int)
|
|
191
|
+
fwdy = fwdy.round().astype(int)
|
|
192
|
+
|
|
193
|
+
fwdwarpix = np.transpose(np.vstack((fwdx, fwdy)))
|
|
194
|
+
|
|
195
|
+
fwdwarpix[fwdwarpix[:, 0] < 1, 0] = 1
|
|
196
|
+
fwdwarpix[fwdwarpix[:, 1] < 1, 1] = 1
|
|
197
|
+
|
|
198
|
+
fwdwarpix[np.isnan(fwdwarpix[:, 0]), 0] = 1
|
|
199
|
+
fwdwarpix[np.isnan(fwdwarpix[:, 1]), 1] = 1
|
|
200
|
+
|
|
201
|
+
fwdwarpix[fwdwarpix[:, 0] > imdims[1], 0] = imdims[1]
|
|
202
|
+
fwdwarpix[fwdwarpix[:, 1] > imdims[0], 1] = imdims[0]
|
|
203
|
+
|
|
204
|
+
RGBsub = np.empty((1, 1))
|
|
205
|
+
for RGB in range(nRGB):
|
|
206
|
+
this_channel = np.ones((fwdwarpix.shape[0], 1)) * RGB
|
|
207
|
+
RGBsub = np.vstack((RGBsub, this_channel))
|
|
208
|
+
RGBsub = np.delete(RGBsub, (0), axis=0)
|
|
209
|
+
RGBsub = RGBsub.astype(int).flatten()
|
|
210
|
+
|
|
211
|
+
pixx = np.tile(pixarr[isin, 0], (nRGB, 1)).flatten()
|
|
212
|
+
pixy = np.tile(pixarr[isin, 1], (nRGB, 1)).flatten()
|
|
213
|
+
# alldims = (imdims[0], imdims[1], nRGB)
|
|
214
|
+
# pixind = np.ravel_multi_index((pixy, pixx, RGBsub), alldims)
|
|
215
|
+
# print("pixarr should be tiled by channel regardless of method.")
|
|
216
|
+
# print("input fwdwarpix_tup grayscale for bilin.")
|
|
217
|
+
# print("fwdwarpix tiled for nearest neighbor.")
|
|
218
|
+
# print("pixx.shape, pixy.shape: \t")
|
|
219
|
+
# print(pixx.shape)
|
|
220
|
+
# print(pixy.shape)
|
|
221
|
+
# print("pixarr.shape: \t")
|
|
222
|
+
# print(pixarr.shape)
|
|
223
|
+
|
|
224
|
+
# Pixarr is now expanded by channel.
|
|
225
|
+
# Same as before in the case of grayscale image.
|
|
226
|
+
pixarr = np.vstack((pixx, pixy)).transpose()
|
|
227
|
+
|
|
228
|
+
if interp == 'nearest':
|
|
229
|
+
# fwdwarpind = sub2ind([imdims nRGB], repmat(fwdwarpix(:,2),[nRGB 1]),
|
|
230
|
+
# repmat(fwdwarpix(:,1),[nRGB 1]), RGBsub);
|
|
231
|
+
print("Nearest neighbor to do:")
|
|
232
|
+
print("In Matlab, we would derive fwdwarpind.")
|
|
233
|
+
print("In this case, expand pixarr & fwdwarpix across RGB channels.")
|
|
234
|
+
if nRGB > 1:
|
|
235
|
+
warpim = np.zeros((imdims[0], imdims[1], nRGB))
|
|
236
|
+
else:
|
|
237
|
+
warpim = np.zeros(imdims)
|
|
238
|
+
if interp == 'nearest':
|
|
239
|
+
print("fwdwarpix already reduced by isin!")
|
|
240
|
+
print("To do: deal with that!")
|
|
241
|
+
# expand fwdwarpix across channels
|
|
242
|
+
# pixx = np.tile(pixarr[isin, 0], (nRGB, 1)).flatten()
|
|
243
|
+
# pixy = np.tile(pixarr[isin, 1], (nRGB, 1)).flatten()
|
|
244
|
+
# pixarr = np.vstack((pixx, pixy)).transpose()
|
|
245
|
+
fwdx = np.tile(fwdwarpix[:, 0], (nRGB, 1)).flatten()
|
|
246
|
+
fwdy = np.tile(fwdwarpix[:, 1], (nRGB, 1)).flatten()
|
|
247
|
+
fwdwarpix = np.vstack((fwdx, fwdy)).transpose()
|
|
248
|
+
|
|
249
|
+
print("Indexing for 3rd dimension, if there is one!")
|
|
250
|
+
if nRGB > 1:
|
|
251
|
+
warpim[pixarr[:, 1], pixarr[:, 0], RGBsub] = im[fwdwarpix[:, 1],
|
|
252
|
+
fwdwarpix[:, 0],
|
|
253
|
+
RGBsub]
|
|
254
|
+
else:
|
|
255
|
+
warpim[pixarr[:, 1], pixarr[:, 0]] = im[fwdwarpix[:, 1],
|
|
256
|
+
fwdwarpix[:, 0]]
|
|
257
|
+
else:
|
|
258
|
+
# print("now, pixarr is already reduced by isin!")
|
|
259
|
+
# print("To do: deal with that!")
|
|
260
|
+
#
|
|
261
|
+
# print("RGBsub.shape")
|
|
262
|
+
# print(RGBsub.shape)
|
|
263
|
+
|
|
264
|
+
fwdwarpix_tup = (fwdwarpix[:, 0], fwdwarpix[:, 1])
|
|
265
|
+
out = bilin(im, fwdwarpix_tup, nRGB, RGBsub)
|
|
266
|
+
|
|
267
|
+
# print("Indexing for 3rd dimension, if there is one!")
|
|
268
|
+
if nRGB > 1:
|
|
269
|
+
warpim[pixarr[:, 1], pixarr[:, 0], RGBsub] = out
|
|
270
|
+
else:
|
|
271
|
+
warpim[pixarr[:, 1], pixarr[:, 0]] = out
|
|
272
|
+
|
|
273
|
+
if isuint8:
|
|
274
|
+
warpim = warpim.astype(np.uint8)
|
|
275
|
+
return warpim, tri, inpix, fwdwarpix
|
|
276
|
+
# -----------------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# print("All above - final warp code in /warp_tests/")
|