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.
@@ -0,0 +1,569 @@
1
+ """
2
+
3
+ CAVEATS
4
+ Any transform requiring a rotation > abs(pi / 2) will be poor.
5
+ The further past this threshold, the greater the error.
6
+ Domain knowledge for faces allows us to perform a pre-processing step to
7
+ circumvent this limitation by simply re-orienting any faces with a
8
+ specific landmark configuration suggesting > abs(pi/2) from upright.
9
+
10
+ When I am done here,
11
+ Only change necessary is "import procrustes_tools as pt" to
12
+ "import alignfaces2.procrustes_tools as pt"
13
+
14
+ TO DO
15
+
16
+ 1. make_aligned_faces module or elsewhere -- pre-processing reorient outliers.
17
+ * because of max rotation issue
18
+ """
19
+ #################################################################
20
+ # IMPORT MODULES
21
+ #################################################################
22
+ from importlib.resources import files
23
+
24
+ # import sys
25
+ import numpy as np
26
+ # import matplotlib.pyplot as plt
27
+ # import pdb # TEMPORARY FOR DEBUGGING
28
+ import alignfaces.procrustes_tools as pt
29
+ # import alignfaces2.procrustes_toolsRohlf as pt
30
+
31
+ #################################################################
32
+ # TEMPORARY TO RUN WITHOUT INSTALLATION OF ALIGNFACES2.
33
+ #################################################################
34
+ # try:
35
+ # arg = sys.argv[1]
36
+ # except IndexError:
37
+ # raise SystemExit(f"Usage: {sys.argv[0]} <kravchenko or rohlf>")
38
+ #
39
+ # package_dir = "/Users/Carl/Studies/facepackage/alignfaces2/src/alignfaces2"
40
+ # sys.path.append(package_dir)
41
+ #
42
+ # if arg == "kravchenko":
43
+ # import procrustes_tools as pt
44
+ # else:
45
+ # import procrustes_toolsRohlf as pt
46
+
47
+
48
+ #################################################################
49
+ # DEFINE SUBFUNCTIONS USED FOR UNIT TESTING
50
+ #################################################################
51
+ def matrix_to_list_of_vectors(M):
52
+ """
53
+ Convert npy matrix of shapes (shapes are rows) to a list of shapes.
54
+ Entries in list are one-dimensional npy arrays.
55
+
56
+ M is typical output of csv import.
57
+ Output list is what's required of procrustes_tools module;
58
+ specifically, generalized_procrustes_analysis().
59
+ """
60
+ M = M.tolist()
61
+ L = [np.array(s) for s in M]
62
+ return L
63
+
64
+
65
+ def max_abs_error_relative_to_mean_radius(standard_shape, test_shape):
66
+ """
67
+ Compare test_shape to standard_shape, relative to mean radius of standard.
68
+ Returned value is a percentage.
69
+
70
+ standard_shape npy array
71
+ test_shape npy array
72
+
73
+ Must have the same shape. Either:
74
+ one-dimensional (single shape) or
75
+ two-dimensional (shapes along first dimension)
76
+
77
+ Each shape is [x1 y1 x2 y2 ... xp yp] for p points.
78
+ """
79
+ assert standard_shape.shape == test_shape.shape
80
+ base_mat = standard_shape.reshape(-1, 2)
81
+ base_radii = np.sqrt(np.dot(base_mat, base_mat.T) *
82
+ np.eye(base_mat.shape[0]))
83
+ mean_radius_base = np.trace(base_radii) / base_mat.shape[0]
84
+
85
+ max_abs_error = np.abs(standard_shape - test_shape).max()
86
+ error_percent = max_abs_error * 100 / mean_radius_base
87
+ return error_percent
88
+
89
+
90
+ def _affine_perturbation(base_shape, number_samples=2, kappa=10):
91
+ # Center to 0.
92
+ mean_x, mean_y = np.mean(base_shape[::2]), np.mean(base_shape[1::2])
93
+ # centered = np.copy(base_shape)
94
+ centered = base_shape.copy()
95
+ centered[::2] -= mean_x
96
+ centered[1::2] -= mean_y
97
+ base_points = np.tile(centered, (number_samples, 1))
98
+
99
+ # Random rotation & scale.
100
+ thetas, scales = [], []
101
+ temp_shape = centered.reshape((-1, 2)).T
102
+ for i in range(number_samples):
103
+ # Make affine transformation matrix.
104
+ # theta = np.random.vonmises(mu=0, kappa=kappa)
105
+ theta = np.random.random_sample() * (np.pi / 2) # [0, pi/2)
106
+ # multiply from [-1 or 1]
107
+ # isneg = (np.random.random() > 0.5)
108
+ # theta = (-1 * isneg * theta) + (isneg==False) * theta # (-pi/2, pi/2)
109
+ scale = np.random.lognormal(mean=0.0, sigma=0.25)
110
+ thetas.append(theta)
111
+ scales.append(scale)
112
+ scaled_shape = temp_shape * scale
113
+ rotation_matrix = np.array([[np.cos(theta), -np.sin(theta)],
114
+ [np.sin(theta), np.cos(theta)]])
115
+
116
+ # Apply transform.
117
+ rotated_shape = np.dot(rotation_matrix, scaled_shape)
118
+ base_points[i, :] = rotated_shape.T.reshape(-1)
119
+
120
+ # Random translation.
121
+ shift_shapes = np.random.randint(low=0, high=10, size=(number_samples, 2))
122
+ shift_points = np.tile(shift_shapes, (1, int(centered.size / 2)))
123
+ shapes = base_points + shift_points
124
+
125
+ # Format of shapes in Procrustes Tools is list of numpy arrays.
126
+ shape_list = []
127
+ for i in range(number_samples):
128
+ shape_list.append(shapes[i, :])
129
+ return {"shape_list": shape_list, "thetas": thetas,
130
+ "scales": scales, "shift_points": shift_points,
131
+ "meanxy": [mean_x, mean_y]}
132
+
133
+
134
+ def _affine_recovery(shapes, thetas, scales, shift_points):
135
+ """
136
+ Get recovered_shapes from shapes and parameters for original affine
137
+ transformation.
138
+
139
+ Each should be very close to base_shape
140
+ Can then measure average distance between base_shape and each of shapes.
141
+
142
+ But for a fair comparison with GPA output, we can also compare between the
143
+ recovered shapes themselves.
144
+ """
145
+ # Set up.
146
+ shapes_array = np.array(shapes)
147
+ num_shapes = shapes_array.shape[0]
148
+ num_points = int(shapes_array.shape[1] / 2)
149
+
150
+ # Undo translation.
151
+ shifted_shapes = shapes_array - shift_points
152
+
153
+ # Undo rotation & scale.
154
+ recovered_points = np.zeros(shapes_array.shape)
155
+ for i in range(num_shapes):
156
+ theta = -thetas[i] # HERE
157
+ rotation_matrix = np.array([[np.cos(theta), -np.sin(theta)],
158
+ [np.sin(theta), np.cos(theta)]])
159
+
160
+ temp_shape = shifted_shapes[i, :].reshape((-1, 2)).T
161
+ rotated_shape = np.dot(rotation_matrix, temp_shape)
162
+ scaled_shape = rotated_shape / scales[i]
163
+ recovered_points[i, :] = scaled_shape.T.reshape((1, num_points * 2))
164
+ recovered_points[:, ::2] += mean_x
165
+ recovered_points[:, 1::2] += mean_y
166
+ return recovered_points
167
+
168
+
169
+ def _make_shapes_around_base(base_shape, num_pairs=10):
170
+ """
171
+ Randomly generate pairs of shapes that are perturbations of base_shape.
172
+ Each shape is slightly different from base_shape.
173
+ However, each pair of shapes has a mean equal to base_shape.
174
+ Further, all shapes are centered at origin.
175
+ """
176
+ assert np.isclose(base_shape[::2].mean(), 0)
177
+ assert np.isclose(base_shape[1::2].mean(), 0)
178
+ shapes = []
179
+ for i in range(num_pairs):
180
+ # Sample perturbations to first 2 points.
181
+ # Independence of samples means that many resulting shapes will be
182
+ # 'different' from base_shape; i.e., affine transform insufficient to
183
+ # match base_shape.
184
+ first_four = np.random.normal(loc=0, scale=2, size=(4,))
185
+ # Final point ensures that resulting shape is centered at origin.
186
+ next_one = np.array([-first_four[0] - first_four[2]])
187
+ last_one = np.array([-first_four[1] - first_four[3]])
188
+ # Vector of perturbations.
189
+ delta = np.r_[first_four, next_one, last_one]
190
+ # First shape in this pair is a perturbation of base_shape.
191
+ shape1 = base_shape + delta
192
+ # Opposite perturbation to second shape in this pair.
193
+ # Ensures mean of this pair is almost equivalent to base_shape.
194
+ shape2 = base_shape - delta
195
+ # Checks to ensure what was claimed is true.
196
+ assert np.isclose(shape1[::2].mean(), 0)
197
+ assert np.isclose(shape1[1::2].mean(), 0)
198
+ assert np.isclose(shape2[::2].mean(), 0)
199
+ assert np.isclose(shape2[1::2].mean(), 0)
200
+ assert np.allclose((shape1 + shape2) / 2, base_shape)
201
+ shapes.append(shape1)
202
+ shapes.append(shape2)
203
+ mean_sampled = np.array(shapes).mean(axis=0)
204
+ assert np.allclose(mean_sampled, base_shape)
205
+ return shapes
206
+
207
+
208
+ # def plot_shapes(shapes):
209
+ # assert type(shapes) is list
210
+ # assert type(shapes[0]) is np.ndarray
211
+ # assert len(shapes[0].shape) == 1
212
+ # shapes_npy = np.array(shapes)
213
+ # xx = np.transpose(shapes_npy[:, ::2])
214
+ # yy = np.transpose(shapes_npy[:, 1::2])
215
+ # fig, axs = plt.subplots()
216
+ # axs.plot(xx, yy)
217
+ # axs.axis('equal')
218
+
219
+
220
+ def center_shapes_in_list(shapes):
221
+ return [pt.translate(s) for s in shapes]
222
+
223
+
224
+ def set_unit_energy(shapes):
225
+ if type(shapes) is list:
226
+ new_shapes = [z / np.sqrt(np.dot(z, z)) for z in shapes]
227
+ elif len(shapes.shape) == 2:
228
+ new_shapes = [z / np.sqrt(np.dot(z, z)) for z in shapes]
229
+ else:
230
+ new_shapes = shapes / np.sqrt(np.dot(shapes, shapes))
231
+ return new_shapes
232
+
233
+
234
+ def get_angle_vector_3p(input_m):
235
+ m = pt._shape_array_to_matrix(input_m)
236
+ a = np.sqrt(((m[1] - m[2])**2).sum())
237
+ b = np.sqrt(((m[0] - m[2])**2).sum())
238
+ c = np.sqrt(((m[0] - m[1])**2).sum())
239
+ ra = np.arccos((b**2 + c**2 - a**2) / (2*b*c))
240
+ rb = np.arccos((a**2 + c**2 - b**2) / (2*a*c))
241
+ rc = np.arccos((a**2 + b**2 - c**2) / (2*a*b))
242
+ return np.array([ra, rb, rc]) * (180 / np.pi)
243
+
244
+
245
+ def all_comparison_triangles_isomorphic_with_standard(standard, comparison):
246
+ standard_angles = get_angle_vector_3p(standard)
247
+ A = [get_angle_vector_3p(P) for P in comparison]
248
+ matches_base = [np.allclose(a, standard_angles) for a in A]
249
+ return all(matches_base)
250
+
251
+
252
+ # Does my affine sampler distort shape?
253
+ # This code only works with triangles.
254
+ base_shape = np.array([0, 0, 15, 10, 30, 5]).astype(float)
255
+ base_angles = get_angle_vector_3p(base_shape)
256
+ num_shapes = 1000
257
+ affine = _affine_perturbation(base_shape, number_samples=num_shapes)
258
+ shapes = affine['shape_list']
259
+ assert all_comparison_triangles_isomorphic_with_standard(base_shape, shapes)
260
+
261
+
262
+ #################################################################
263
+ # IMPORT DATA
264
+ #################################################################
265
+ # Import geomorph plethodon data & results of geomorph's gpagen() in R.
266
+ def get_geomorph_plethodon_data():
267
+ in_shapes_file = files('alignfaces.tests.R').joinpath('input_shapes.csv')
268
+ al_shapes_file = files('alignfaces.tests.R').joinpath('align_shapes.csv')
269
+ al_ref_file = files('alignfaces.tests.R').joinpath('align_reference.csv')
270
+ shapes = np.loadtxt(fname=in_shapes_file, delimiter=',')
271
+ shapes = matrix_to_list_of_vectors(shapes)
272
+ aligned_R = np.loadtxt(fname=al_shapes_file, delimiter=',')
273
+ ref_R = np.loadtxt(fname=al_ref_file, delimiter=',')
274
+ cshapes = center_shapes_in_list(shapes)
275
+ return [cshapes, ref_R, aligned_R]
276
+
277
+ #################################################################
278
+ # DEFINE FUNCTIONS USED FOR UNIT TESTING
279
+ #################################################################
280
+ def test_translate_1():
281
+ """Input shape is already centered at origin. No change expected."""
282
+ shape = np.array([-15, -5, 0, 5, 15, 0]).astype(float)
283
+ expected_shape = shape
284
+ new_shape = pt.translate(shape)
285
+ assert np.allclose(new_shape, expected_shape)
286
+
287
+
288
+ def test_translate_2():
289
+ """Result of translation known."""
290
+ expected_shape = np.array([-15, -5, 0, 5, 15, 0]).astype(float)
291
+ shift = np.array([2, -6, 2, -6, 2, -6]).astype(float)
292
+ shape = expected_shape + shift
293
+ new_shape = pt.translate(shape)
294
+ assert np.allclose(new_shape, expected_shape)
295
+
296
+
297
+ def test_rotate_1():
298
+ """Result of rotation known."""
299
+ shape = np.array([0, 0, 0, 1, 1, 0]).astype(float)
300
+ theta = np.pi / 2
301
+ expected_shape = np.array([0, 0, -1, 0, 0, 1]).astype(float)
302
+ new_shape = pt.rotate(shape, theta)
303
+ assert np.allclose(new_shape, expected_shape)
304
+
305
+
306
+ def test_rotate_2():
307
+ """Result of rotation known. Just another theta."""
308
+ v = 1 / np.sqrt(2)
309
+ shape = np.array([0, 0, v, v, v, -v]).astype(float)
310
+ theta = np.pi / 4
311
+ expected_shape = np.array([0, 0, 0, 1, 1, 0]).astype(float)
312
+ new_shape = pt.rotate(shape, theta)
313
+ assert np.allclose(new_shape, expected_shape)
314
+
315
+
316
+ def test_theta_in_get_rotation_scale():
317
+ """Optimal rotation known to be pi / 2."""
318
+ shape = np.array([0, 0, 0, 1, 1, 0]).astype(float)
319
+ reference_shape = np.array([0, 0, -1, 0, 0, 1]).astype(float)
320
+ scale, theta = pt.get_rotation_scale(reference_shape, shape)
321
+ assert np.isclose(theta, np.pi / 2)
322
+
323
+
324
+ def test_scale_in_get_rotation_scale():
325
+ """Scale of shape relative to reference known to be 1 / 2."""
326
+ shape = np.array([0, 0, 0, 1, 1, 0]).astype(float)
327
+ reference_shape = np.array([0, 0, -1, 0, 0, 1]).astype(float) * 2
328
+ scale, theta = pt.get_rotation_scale(reference_shape, shape)
329
+ assert np.isclose(scale, 1/2)
330
+
331
+
332
+ def test_procrustes_analysis():
333
+ """
334
+ Shape to be aligned to reference is an affine transform of reference.
335
+ Test function for classical procrustes analysis.
336
+ """
337
+ reference_shape = np.array([0, 0, 0, 1, 1, 0]).astype(float) * 2
338
+ shape = np.array([0, 0, -1, 0, 0, 1]).astype(float)
339
+ aligned_shape = pt.procrustes_analysis(reference_shape, shape)
340
+ reference_shape = pt.translate(reference_shape)
341
+ aligned_shape = pt.translate(aligned_shape)
342
+ assert np.allclose(aligned_shape, reference_shape)
343
+
344
+
345
+ def test_generalized_procrustes_analysis_distortion():
346
+ # Does my GPA function distort shape?
347
+ base = np.array([0, 0, 15, 10, 30, 5]).astype(float)
348
+ affine = _affine_perturbation(base, number_samples=1000)
349
+ shapes = affine['shape_list']
350
+
351
+ mean_shape, new_shapes, D = pt.generalized_procrustes_analysis(shapes)
352
+ assert all_comparison_triangles_isomorphic_with_standard(base, new_shapes)
353
+
354
+
355
+ def test_generalized_procrustes_analysis_1():
356
+ """
357
+ Shape to be aligned to reference is an affine transform of reference.
358
+ Test function for generalized procrustes analysis.
359
+ """
360
+ shape1 = np.array([-3, -4, 3, -4, 0, 8]).astype(float)
361
+ shape2 = np.array([-4, 3, -4, -3, 8, 0]).astype(float)
362
+ shapes = [shape1, shape2]
363
+ expected_reference = shape1
364
+ reference_shape, new_shapes, D = pt.generalized_procrustes_analysis(shapes)
365
+ reference_shape = pt.procrustes_analysis(expected_reference,
366
+ reference_shape)
367
+ assert np.allclose(reference_shape, expected_reference)
368
+
369
+
370
+ def test_generalized_procrustes_analysis_equivalent_shapes():
371
+ # note: n should be > 3
372
+ """
373
+ All triangles are affine isomorphic.
374
+
375
+ Any transform requiring a rotation > abs(pi / 2) will be poor.
376
+ The further past this threshold, the greater the error.
377
+ Domain knowledge for faces allows us to perform a pre-processing step to
378
+ circumvent this limitation by simply re-orienting any faces with a
379
+ specific landmark configuration suggesting > abs(pi/2) from upright.
380
+
381
+ To do
382
+ 1. [here] clip sampled thetas in _affine_perturbation().
383
+ 2. [overall] pre-processing. detect outliers via features and reorient.
384
+ """
385
+ base_shape = np.array([0, 0, 15, 10, 30, 5]).astype(float)
386
+ # base_shape = pt.translate(base_shape)
387
+
388
+ num_shapes = 100
389
+ affine = _affine_perturbation(base_shape, number_samples=num_shapes)
390
+ shapes = affine['shape_list']
391
+ # shapes = center_shapes_in_list(shapes)
392
+ # pdb.set_trace() # TEMPORARY FOR DEBUGGING
393
+ mean_shape, new_shapes, D = pt.generalized_procrustes_analysis(shapes)
394
+
395
+ mean_distance_among_aligned = []
396
+ for i in range(num_shapes - 1):
397
+ for j in range(i + 1, num_shapes):
398
+ delta = new_shapes[i] - new_shapes[j]
399
+ d = np.sqrt(pt._shape_array_to_matrix(delta**2).sum(axis=1))
400
+ mean_distance_among_aligned.append(d.mean())
401
+ assert len(mean_distance_among_aligned) == (num_shapes**2-num_shapes)/2
402
+
403
+ base_mat = mean_shape.reshape(-1, 2)
404
+ base_radii = np.sqrt(np.dot(base_mat, base_mat.T) *
405
+ np.eye(base_mat.shape[0]))
406
+ mean_radius_base = np.trace(base_radii) / base_mat.shape[0]
407
+ # pdb.set_trace() # TEMPORARY FOR DEBUGGING
408
+ error_percent = max(mean_distance_among_aligned) * 100 / mean_radius_base
409
+ # print("Comparison of all aligned shapes generated from random affine "
410
+ # "transform of a base shape. Average point-wise distance for all"
411
+ # "pairwise comparisons of aligned shapes. Max of these relative"
412
+ # "to mean radius of mean aligned shape.")
413
+ # print(f"\tMax abs error as percent of mean radius of consensus\t: {error_percent}")
414
+ assert error_percent < 1e-10
415
+
416
+
417
+ def test_generalized_procrustes_analysis_2():
418
+ """
419
+ No affine transform to exactly match the 3rd shape to the rest.
420
+ However, each shape is an isosceles triangle.
421
+ Given that the procrustes algorithm initializes the reference shape as
422
+ the first shape in input list of shapes, final alignments should ensure
423
+ that the base of each triangle is horizontal and the 3rd point is centered
424
+ at zero along the x axis.
425
+ """
426
+ shape1 = np.array([-3, -4, 3, -4, 0, 8]).astype(float)
427
+ shape2 = np.array([-4, 3, -4, -3, 8, 0]).astype(float)
428
+ shape3 = np.array([-3, -8, 3, -8, 0, 16]).astype(float)
429
+ shapes = [shape1, shape2, shape3]
430
+ reference_shape, new_shapes, D = pt.generalized_procrustes_analysis(shapes)
431
+
432
+ M = np.array(new_shapes)
433
+ first_2_pnts_horizontal = np.allclose(M[:, 1], M[:, 3])
434
+ third_pnt_at_zero_x = np.allclose(M[:, 4], np.array([0, 0, 0]))
435
+ assert (first_2_pnts_horizontal and third_pnt_at_zero_x)
436
+
437
+
438
+ def test_generalized_procrustes_analysis_geomorph_1():
439
+ geo_data = get_geomorph_plethodon_data()
440
+ shapes = geo_data[0]
441
+ ref_R = geo_data[1]
442
+ # Consensus shape & aligned shapes using this Python package.
443
+ ref_python, aligned_python, D = pt.generalized_procrustes_analysis(shapes)
444
+ # geomorph scales to unit energy, so need to rescale ref_python.
445
+ ref_python = set_unit_energy(ref_python)
446
+ # Compare resulting consensus shapes.
447
+ error_percent = max_abs_error_relative_to_mean_radius(ref_R, ref_python)
448
+ # print("Comparison of consensus shape recovered by geomorph (R) and "
449
+ # "facepackage (Python).")
450
+ # print(f"\tMax abs error as percent of mean radius\t: {error_percent}")
451
+ assert error_percent < 1
452
+
453
+
454
+ def test_generalized_procrustes_analysis_geomorph_2():
455
+ geo_data = get_geomorph_plethodon_data()
456
+ shapes = geo_data[0]
457
+ aligned_R = geo_data[2]
458
+ # Consensus shape & aligned shapes using this Python package.
459
+ ref_python, aligned_python, D = pt.generalized_procrustes_analysis(shapes)
460
+ # print("geomorph outputs unit scale shapes. do same with python output.")
461
+ aligned_python = set_unit_energy(aligned_python)
462
+ aligned_python = np.array(aligned_python)
463
+ # pdb.set_trace() # TEMPORARY FOR DEBUGGING
464
+ # Compare shapes aligned by the two methods.
465
+ error_percent = max_abs_error_relative_to_mean_radius(aligned_R,
466
+ aligned_python)
467
+ # print("Comparison of shapes aligned by geomorph (R) and by"
468
+ # "facepackage (Python).")
469
+ # print(f"\tMax abs error as percent of mean radius\t: {error_percent}")
470
+ assert error_percent < 1
471
+
472
+
473
+ def test_generalized_procrustes_analysis_recover_donut_center():
474
+ """
475
+ Recover a base shape (prototype) that does not exist in analyzed shapes.
476
+ """
477
+ base_shape = np.array([-6, -8, 6, -8, 0, 16]).astype(float)
478
+ shapes = _make_shapes_around_base(base_shape, num_pairs=100)
479
+ reference_shape, new_shapes, D = pt.generalized_procrustes_analysis(shapes)
480
+
481
+ # New reference should be the same 'shape' as the base, or very close.
482
+ # But likely does not have the right scale and orientation.
483
+ aligned_reference = pt.procrustes_analysis(base_shape, reference_shape)
484
+ error_percent = max_abs_error_relative_to_mean_radius(base_shape,
485
+ aligned_reference)
486
+ # print("Recover base shape at center of sampled shapes. "
487
+ # "Exact shape variable.")
488
+ # print(f"\tMax abs error as percent of mean radius\t: {error_percent}")
489
+ assert error_percent < 1
490
+
491
+
492
+ #################################################################
493
+ # UNIT TESTS
494
+ #################################################################
495
+
496
+ # Basic functions.
497
+ test_translate_1()
498
+ test_translate_2()
499
+ test_rotate_1()
500
+ test_rotate_2()
501
+
502
+ # Alignment functions -
503
+ # Shapes are equivalent except for affine transform.
504
+ # Expect near-perfect alignments.
505
+ test_theta_in_get_rotation_scale()
506
+ test_scale_in_get_rotation_scale()
507
+ test_procrustes_analysis()
508
+ test_generalized_procrustes_analysis_1()
509
+
510
+ # *** all below involve more than 2 input shapes.
511
+ test_generalized_procrustes_analysis_distortion()
512
+ test_generalized_procrustes_analysis_equivalent_shapes()
513
+
514
+ # Alignment functions -
515
+ # Shapes cannot be perfectly aligned by affine transform.
516
+ # only 3 input shapes
517
+ # only match 'shape features'
518
+ test_generalized_procrustes_analysis_2()
519
+ test_generalized_procrustes_analysis_geomorph_1()
520
+ test_generalized_procrustes_analysis_geomorph_2()
521
+ test_generalized_procrustes_analysis_recover_donut_center()
522
+
523
+
524
+ ###############################################################################
525
+ ###############################################################################
526
+ # Limits of simple affine.
527
+ # base_shape
528
+ # theta = np.pi / 2
529
+ # r_shape = pt.rotate(base_shape, theta)
530
+ # a_shape = pt.procrustes_analysis(base_shape, r_shape)
531
+ # plot_shapes([base_shape, a_shape])
532
+ # err = max_abs_error_relative_to_mean_radius(base_shape, a_shape)
533
+ #
534
+ # degrees = [-d for d in range(180)]
535
+ # radians = [d * np.pi/180 for d in degrees]
536
+ # E = []
537
+ # for theta in radians:
538
+ # r_shape = pt.rotate(base_shape, theta)
539
+ # a_shape = pt.procrustes_analysis(base_shape, r_shape)
540
+ # err = max_abs_error_relative_to_mean_radius(base_shape, a_shape)
541
+ # E.append(err)
542
+ # plt.plot(degrees, E)
543
+ #
544
+ # delta = np.diff(np.array(E), n=1)
545
+ # plt.plot(delta)
546
+ # delta[0:100].argmax()
547
+ # E[90]
548
+ # E[91]
549
+ # degrees[90:92]
550
+ #
551
+ # # Rotations above 90 degrees lead to jump in error.
552
+ # base_shape = np.random.standard_normal((2 * 1000, ))
553
+ # base_shape = pt.translate(base_shape)
554
+ # E = []
555
+ # for theta in radians:
556
+ # r_shape = pt.rotate(base_shape, theta)
557
+ # a_shape = pt.procrustes_analysis(base_shape, r_shape)
558
+ # err = max_abs_error_relative_to_mean_radius(base_shape, a_shape)
559
+ # E.append(err)
560
+ # plt.plot(degrees, E)
561
+ # E[90], E[91]
562
+ # degrees[90:92]
563
+ #
564
+ # # Avoid any rotation greater than pi/2 or less than -pi/2
565
+
566
+
567
+ # END
568
+ ##############################################################################
569
+ ##############################################################################