forked from menpo/menpo
/
pointcloud.py
715 lines (621 loc) · 27.5 KB
/
pointcloud.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
import numpy as np
from warnings import warn
from scipy.spatial.distance import cdist
from menpo.shape.base import Shape
class PointCloud(Shape):
r"""
An N-dimensional point cloud. This is internally represented as an `ndarray`
of shape ``(n_points, n_dims)``. This class is important for dealing
with complex functionality such as viewing and representing metadata such
as landmarks.
Currently only 2D and 3D pointclouds are viewable.
Parameters
----------
points : ``(n_points, n_dims)`` `ndarray`
The array representing the points.
copy : `bool`, optional
If ``False``, the points will not be copied on assignment. Note that
this will miss out on additional checks. Further note that we still
demand that the array is C-contiguous - if it isn't, a copy will be
generated anyway.
In general this should only be used if you know what you are doing.
"""
def __init__(self, points, copy=True):
super(PointCloud, self).__init__()
if not copy:
if not points.flags.c_contiguous:
warn('The copy flag was NOT honoured. A copy HAS been made. '
'Please ensure the data you pass is C-contiguous.')
points = np.array(points, copy=True, order='C')
else:
points = np.array(points, copy=True, order='C')
self.points = points
@property
def n_points(self):
r"""
The number of points in the pointcloud.
:type: `int`
"""
return self.points.shape[0]
@property
def n_dims(self):
r"""
The number of dimensions in the pointcloud.
:type: `int`
"""
return self.points.shape[1]
def h_points(self):
r"""
Convert poincloud to a homogeneous array: ``(n_dims + 1, n_points)``
:type: ``type(self)``
"""
return np.concatenate((self.points.T, np.ones(self.n_points)[None, :]))
def centre(self):
r"""
The mean of all the points in this PointCloud (centre of mass).
Returns
-------
centre : ``(n_dims)`` `ndarray`
The mean of this PointCloud's points.
"""
return np.mean(self.points, axis=0)
def centre_of_bounds(self):
r"""
The centre of the absolute bounds of this PointCloud. Contrast with
:meth:`centre`, which is the mean point position.
Returns
-------
centre : ``n_dims`` `ndarray`
The centre of the bounds of this PointCloud.
"""
min_b, max_b = self.bounds()
return (min_b + max_b) / 2
def _as_vector(self):
r"""
Returns a flattened representation of the pointcloud.
Note that the flattened representation is of the form
``[x0, y0, x1, y1, ....., xn, yn]`` for 2D.
Returns
-------
flattened : ``(n_points,)`` `ndarray`
The flattened points.
"""
return self.points.ravel()
def tojson(self):
r"""
Convert this :map:`PointCloud` to a dictionary representation suitable
for inclusion in the LJSON landmark format.
Returns
-------
json : `dict`
Dictionary with ``points`` keys.
"""
return {'points': self.points.tolist()}
def from_vector_inplace(self, vector):
r"""
Updates the points of this PointCloud in-place with the reshaped points
from the provided vector. Note that the vector should have the form
``[x0, y0, x1, y1, ....., xn, yn]`` for 2D.
Parameters
----------
vector : ``(n_points,)`` `ndarray`
The vector from which to create the points' array.
"""
self.points = vector.reshape([-1, self.n_dims])
def __str__(self):
return '{}: n_points: {}, n_dims: {}'.format(type(self).__name__,
self.n_points,
self.n_dims)
def bounds(self, boundary=0):
r"""
The minimum to maximum extent of the PointCloud. An optional boundary
argument can be provided to expand the bounds by a constant margin.
Parameters
----------
boundary : `float`
A optional padding distance that is added to the bounds. Default
is ``0``, meaning the max/min of tightest possible containing
square/cube/hypercube is returned.
Returns
-------
min_b : ``(n_dims,)`` `ndarray`
The minimum extent of the :map:`PointCloud` and boundary along
each dimension
max_b : ``(n_dims,)`` `ndarray`
The maximum extent of the :map:`PointCloud` and boundary along
each dimension
"""
min_b = np.min(self.points, axis=0) - boundary
max_b = np.max(self.points, axis=0) + boundary
return min_b, max_b
def range(self, boundary=0):
r"""
The range of the extent of the PointCloud.
Parameters
----------
boundary : `float`
A optional padding distance that is used to extend the bounds
from which the range is computed. Default is ``0``, no extension
is performed.
Returns
-------
range : ``(n_dims,)`` `ndarray`
The range of the :map:`PointCloud` extent in each dimension.
"""
min_b, max_b = self.bounds(boundary)
return max_b - min_b
def bounding_box(self):
r"""
Return the bounding box of this PointCloud as a directed graph.
The the first point (0) will be nearest the origin for an axis aligned
Pointcloud.
In the case of an image, this ordering would appear as:
::
0<--3
| ^
| |
v |
1-->2
Returns
-------
bounding_box : :map:`PointDirectedGraph`
The axis aligned bounding box of the PointCloud.
"""
from .graph import PointDirectedGraph
min_p, max_p = self.bounds()
return PointDirectedGraph(np.array([min_p, [max_p[0], min_p[1]],
max_p, [min_p[0], max_p[1]]]),
np.array([[0, 1], [1, 2], [2, 3], [3, 0]]),
copy=False)
def _view_2d(self, figure_id=None, new_figure=False, image_view=True,
render_markers=True, marker_style='o', marker_size=20,
marker_face_colour='r', marker_edge_colour='k',
marker_edge_width=1., render_axes=True,
axes_font_name='sans-serif', axes_font_size=10,
axes_font_style='normal', axes_font_weight='normal',
axes_x_limits=None, axes_y_limits=None, figure_size=(10, 8),
label=None, **kwargs):
r"""
Visualization of the PointCloud in 2D.
Returns
-------
figure_id : `object`, optional
The id of the figure to be used.
new_figure : `bool`, optional
If ``True``, a new figure is created.
image_view : `bool`, optional
If ``True`` the PointCloud will be viewed as if it is in the image
coordinate system.
render_lines : `bool`, optional
If ``True``, the edges will be rendered.
line_colour : See Below, optional
The colour of the lines.
Example options::
{r, g, b, c, m, k, w}
or
(3, ) ndarray
line_style : ``{-, --, -., :}``, optional
The style of the lines.
line_width : `float`, optional
The width of the lines.
render_markers : `bool`, optional
If ``True``, the markers will be rendered.
marker_style : See Below, optional
The style of the markers. Example options ::
{., ,, o, v, ^, <, >, +, x, D, d, s, p, *, h, H, 1, 2, 3, 4, 8}
marker_size : `int`, optional
The size of the markers in points^2.
marker_face_colour : See Below, optional
The face (filling) colour of the markers.
Example options ::
{r, g, b, c, m, k, w}
or
(3, ) ndarray
marker_edge_colour : See Below, optional
The edge colour of the markers.
Example options ::
{r, g, b, c, m, k, w}
or
(3, ) ndarray
marker_edge_width : `float`, optional
The width of the markers' edge.
render_axes : `bool`, optional
If ``True``, the axes will be rendered.
axes_font_name : See Below, optional
The font of the axes.
Example options ::
{serif, sans-serif, cursive, fantasy, monospace}
axes_font_size : `int`, optional
The font size of the axes.
axes_font_style : {``normal``, ``italic``, ``oblique``}, optional
The font style of the axes.
axes_font_weight : See Below, optional
The font weight of the axes.
Example options ::
{ultralight, light, normal, regular, book, medium, roman,
semibold, demibold, demi, bold, heavy, extra bold, black}
axes_x_limits : (`float`, `float`) `tuple` or ``None``, optional
The limits of the x axis.
axes_y_limits : (`float`, `float`) `tuple` or ``None``, optional
The limits of the y axis.
figure_size : (`float`, `float`) `tuple` or ``None``, optional
The size of the figure in inches.
label : `str`, optional
The name entry in case of a legend.
Returns
-------
viewer : :map:`PointGraphViewer2d`
The viewer object.
"""
from menpo.visualize.base import PointGraphViewer2d
adjacency_array = np.empty(0)
renderer = PointGraphViewer2d(figure_id, new_figure,
self.points,
adjacency_array)
renderer.render(
image_view=image_view, render_lines=False, line_colour='b',
line_style='-', line_width=1., render_markers=render_markers,
marker_style=marker_style, marker_size=marker_size,
marker_face_colour=marker_face_colour,
marker_edge_colour=marker_edge_colour,
marker_edge_width=marker_edge_width, render_axes=render_axes,
axes_font_name=axes_font_name, axes_font_size=axes_font_size,
axes_font_style=axes_font_style, axes_font_weight=axes_font_weight,
axes_x_limits=axes_x_limits, axes_y_limits=axes_y_limits,
figure_size=figure_size, label=label)
return renderer
def _view_landmarks_2d(self, group=None, with_labels=None,
without_labels=None, figure_id=None,
new_figure=False, image_view=True, render_lines=True,
line_colour=None, line_style='-', line_width=1,
render_markers=True, marker_style='o',
marker_size=20, marker_face_colour=None,
marker_edge_colour=None, marker_edge_width=1.,
render_numbering=False,
numbers_horizontal_align='center',
numbers_vertical_align='bottom',
numbers_font_name='sans-serif', numbers_font_size=10,
numbers_font_style='normal',
numbers_font_weight='normal',
numbers_font_colour='k', render_legend=False,
legend_title='', legend_font_name='sans-serif',
legend_font_style='normal', legend_font_size=10,
legend_font_weight='normal',
legend_marker_scale=None, legend_location=2,
legend_bbox_to_anchor=(1.05, 1.),
legend_border_axes_pad=None, legend_n_columns=1,
legend_horizontal_spacing=None,
legend_vertical_spacing=None, legend_border=True,
legend_border_padding=None, legend_shadow=False,
legend_rounded_corners=False, render_axes=False,
axes_font_name='sans-serif', axes_font_size=10,
axes_font_style='normal', axes_font_weight='normal',
axes_x_limits=None, axes_y_limits=None,
figure_size=(10, 8)):
"""
Visualize the landmarks. This method will appear on the Image as
``view_landmarks`` if the Image is 2D.
Parameters
----------
group : `str` or``None`` optional
The landmark group to be visualized. If ``None`` and there are more
than one landmark groups, an error is raised.
with_labels : ``None`` or `str` or `list` of `str`, optional
If not ``None``, only show the given label(s). Should **not** be
used with the ``without_labels`` kwarg.
without_labels : ``None`` or `str` or `list` of `str`, optional
If not ``None``, show all except the given label(s). Should **not**
be used with the ``with_labels`` kwarg.
figure_id : `object`, optional
The id of the figure to be used.
new_figure : `bool`, optional
If ``True``, a new figure is created.
image_view : `bool`, optional
If ``True`` the PointCloud will be viewed as if it is in the image
coordinate system.
render_lines : `bool`, optional
If ``True``, the edges will be rendered.
line_colour : See Below, optional
The colour of the lines.
Example options::
{r, g, b, c, m, k, w}
or
(3, ) ndarray
line_style : ``{-, --, -., :}``, optional
The style of the lines.
line_width : `float`, optional
The width of the lines.
render_markers : `bool`, optional
If ``True``, the markers will be rendered.
marker_style : See Below, optional
The style of the markers. Example options ::
{., ,, o, v, ^, <, >, +, x, D, d, s, p, *, h, H, 1, 2, 3, 4, 8}
marker_size : `int`, optional
The size of the markers in points^2.
marker_face_colour : See Below, optional
The face (filling) colour of the markers.
Example options ::
{r, g, b, c, m, k, w}
or
(3, ) ndarray
marker_edge_colour : See Below, optional
The edge colour of the markers.
Example options ::
{r, g, b, c, m, k, w}
or
(3, ) ndarray
marker_edge_width : `float`, optional
The width of the markers' edge.
render_numbering : `bool`, optional
If ``True``, the landmarks will be numbered.
numbers_horizontal_align : ``{center, right, left}``, optional
The horizontal alignment of the numbers' texts.
numbers_vertical_align : ``{center, top, bottom, baseline}``, optional
The vertical alignment of the numbers' texts.
numbers_font_name : See Below, optional
The font of the numbers. Example options ::
{serif, sans-serif, cursive, fantasy, monospace}
numbers_font_size : `int`, optional
The font size of the numbers.
numbers_font_style : ``{normal, italic, oblique}``, optional
The font style of the numbers.
numbers_font_weight : See Below, optional
The font weight of the numbers.
Example options ::
{ultralight, light, normal, regular, book, medium, roman,
semibold, demibold, demi, bold, heavy, extra bold, black}
numbers_font_colour : See Below, optional
The font colour of the numbers.
Example options ::
{r, g, b, c, m, k, w}
or
(3, ) ndarray
render_legend : `bool`, optional
If ``True``, the legend will be rendered.
legend_title : `str`, optional
The title of the legend.
legend_font_name : See below, optional
The font of the legend. Example options ::
{serif, sans-serif, cursive, fantasy, monospace}
legend_font_style : ``{normal, italic, oblique}``, optional
The font style of the legend.
legend_font_size : `int`, optional
The font size of the legend.
legend_font_weight : See Below, optional
The font weight of the legend.
Example options ::
{ultralight, light, normal, regular, book, medium, roman,
semibold, demibold, demi, bold, heavy, extra bold, black}
legend_marker_scale : `float`, optional
The relative size of the legend markers with respect to the original
legend_location : `int`, optional
The location of the legend. The predefined values are:
=============== ==
'best' 0
'upper right' 1
'upper left' 2
'lower left' 3
'lower right' 4
'right' 5
'center left' 6
'center right' 7
'lower center' 8
'upper center' 9
'center' 10
=============== ==
legend_bbox_to_anchor : (`float`, `float`) `tuple`, optional
The bbox that the legend will be anchored.
legend_border_axes_pad : `float`, optional
The pad between the axes and legend border.
legend_n_columns : `int`, optional
The number of the legend's columns.
legend_horizontal_spacing : `float`, optional
The spacing between the columns.
legend_vertical_spacing : `float`, optional
The vertical space between the legend entries.
legend_border : `bool`, optional
If ``True``, a frame will be drawn around the legend.
legend_border_padding : `float`, optional
The fractional whitespace inside the legend border.
legend_shadow : `bool`, optional
If ``True``, a shadow will be drawn behind legend.
legend_rounded_corners : `bool`, optional
If ``True``, the frame's corners will be rounded (fancybox).
render_axes : `bool`, optional
If ``True``, the axes will be rendered.
axes_font_name : See Below, optional
The font of the axes. Example options ::
{serif, sans-serif, cursive, fantasy, monospace}
axes_font_size : `int`, optional
The font size of the axes.
axes_font_style : ``{normal, italic, oblique}``, optional
The font style of the axes.
axes_font_weight : See Below, optional
The font weight of the axes.
Example options ::
{ultralight, light, normal, regular, book, medium, roman,
semibold,demibold, demi, bold, heavy, extra bold, black}
axes_x_limits : (`float`, `float`) `tuple` or ``None`` optional
The limits of the x axis.
axes_y_limits : (`float`, `float`) `tuple` or ``None`` optional
The limits of the y axis.
figure_size : (`float`, `float`) `tuple` or ``None`` optional
The size of the figure in inches.
Raises
------
ValueError
If both ``with_labels`` and ``without_labels`` are passed.
ValueError
If the landmark manager doesn't contain the provided group label.
"""
if not self.has_landmarks:
raise ValueError('PointCloud does not have landmarks attached, '
'unable to view landmarks.')
self_view = self.view(figure_id=figure_id, new_figure=new_figure,
image_view=image_view, figure_size=figure_size)
landmark_view = self.landmarks[group].view(
with_labels=with_labels, without_labels=without_labels,
figure_id=self_view.figure_id, new_figure=False,
image_view=image_view, render_lines=render_lines,
line_colour=line_colour, line_style=line_style,
line_width=line_width, render_markers=render_markers,
marker_style=marker_style, marker_size=marker_size,
marker_face_colour=marker_face_colour,
marker_edge_colour=marker_edge_colour,
marker_edge_width=marker_edge_width,
render_numbering=render_numbering,
numbers_horizontal_align=numbers_horizontal_align,
numbers_vertical_align=numbers_vertical_align,
numbers_font_name=numbers_font_name,
numbers_font_size=numbers_font_size,
numbers_font_style=numbers_font_style,
numbers_font_weight=numbers_font_weight,
numbers_font_colour=numbers_font_colour,
render_legend=render_legend, legend_title=legend_title,
legend_font_name=legend_font_name,
legend_font_style=legend_font_style,
legend_font_size=legend_font_size,
legend_font_weight=legend_font_weight,
legend_marker_scale=legend_marker_scale,
legend_location=legend_location,
legend_bbox_to_anchor=legend_bbox_to_anchor,
legend_border_axes_pad=legend_border_axes_pad,
legend_n_columns=legend_n_columns,
legend_horizontal_spacing=legend_horizontal_spacing,
legend_vertical_spacing=legend_vertical_spacing,
legend_border=legend_border,
legend_border_padding=legend_border_padding,
legend_shadow=legend_shadow,
legend_rounded_corners=legend_rounded_corners,
render_axes=render_axes, axes_font_name=axes_font_name,
axes_font_size=axes_font_size, axes_font_style=axes_font_style,
axes_font_weight=axes_font_weight, axes_x_limits=axes_x_limits,
axes_y_limits=axes_y_limits, figure_size=figure_size)
return landmark_view
def _view_3d(self, figure_id=None, new_figure=False):
r"""
Visualization of the PointCloud in 3D.
Parameters
----------
figure_id : `object`, optional
The id of the figure to be used.
new_figure : `bool`, optional
If ``True``, a new figure is created.
Returns
-------
viewer : PointCloudViewer3d
The Menpo3D viewer object.
"""
try:
from menpo3d.visualize import PointCloudViewer3d
return PointCloudViewer3d(figure_id, new_figure,
self.points).render()
except ImportError:
from menpo.visualize import Menpo3dErrorMessage
raise ImportError(Menpo3dErrorMessage)
def _view_landmarks_3d(self, figure_id=None, new_figure=False,
group=None):
r"""
Visualization of the PointCloud landmarks in 3D.
Parameters
----------
figure_id : `object`, optional
The id of the figure to be used.
new_figure : `bool`, optional
If ``True``, a new figure is created.
group : `str`
The landmark group to visualize. If ``None`` is passed, the first
and only landmark group on the object will be visualized.
Returns
-------
viewer : LandmarkViewer3d
The Menpo3D viewer object.
"""
try:
from menpo3d.visualize import LandmarkViewer3d
self_renderer = self.view(figure_id=figure_id,
new_figure=new_figure)
return LandmarkViewer3d(self_renderer.figure, False, self,
self.landmarks[group]).render()
except ImportError:
from menpo.visualize import Menpo3dErrorMessage
raise ImportError(Menpo3dErrorMessage)
def view_widget(self, browser_style='buttons', figure_size=(10, 8)):
r"""
Visualization of the PointCloud using the :map:`visualize_pointclouds`
widget.
Parameters
----------
browser_style : ``{buttons, slider}``, optional
It defines whether the selector of the PointCloud objects will have
the form of plus/minus buttons or a slider.
figure_size : (`int`, `int`), optional
The initial size of the rendered figure.
"""
from menpo.visualize import visualize_pointclouds
visualize_pointclouds(self, figure_size=figure_size,
browser_style=browser_style)
def _transform_self_inplace(self, transform):
self.points = transform(self.points)
return self
def distance_to(self, pointcloud, **kwargs):
r"""
Returns a distance matrix between this PointCloud and another.
By default the Euclidean distance is calculated - see
`scipy.spatial.distance.cdist` for valid kwargs to change the metric
and other properties.
Parameters
----------
pointcloud : :map:`PointCloud`
The second pointcloud to compute distances between. This must be
of the same dimension as this PointCloud.
Returns
-------
distance_matrix: ``(n_points, n_points)`` `ndarray`
The symmetric pairwise distance matrix between the two PointClouds
s.t. ``distance_matrix[i, j]`` is the distance between the i'th
point of this PointCloud and the j'th point of the input
PointCloud.
"""
if self.n_dims != pointcloud.n_dims:
raise ValueError("The two PointClouds must be of the same "
"dimensionality.")
return cdist(self.points, pointcloud.points, **kwargs)
def norm(self, **kwargs):
r"""
Returns the norm of this PointCloud. This is a translation and
rotation invariant measure of the point cloud's intrinsic size - in
other words, it is always taken around the point cloud's centre.
By default, the Frobenius norm is taken, but this can be changed by
setting kwargs - see ``numpy.linalg.norm`` for valid options.
Returns
-------
norm : `float`
The norm of this :map:`PointCloud`
"""
return np.linalg.norm(self.points - self.centre(), **kwargs)
def from_mask(self, mask):
"""
A 1D boolean array with the same number of elements as the number of
points in the PointCloud. This is then broadcast across the dimensions
of the PointCloud and returns a new PointCloud containing only those
points that were ``True`` in the mask.
Parameters
----------
mask : ``(n_points,)`` `ndarray`
1D array of booleans
Returns
-------
pointcloud : :map:`PointCloud`
A new pointcloud that has been masked.
Raises
------
ValueError
Mask must have same number of points as pointcloud.
"""
if mask.shape[0] != self.n_points:
raise ValueError('Mask must be a 1D boolean array of the same '
'number of entries as points in this PointCloud.')
pc = self.copy()
pc.points = pc.points[mask, :]
return pc