-
Notifications
You must be signed in to change notification settings - Fork 101
Expand file tree
/
Copy pathgeom3d.py
More file actions
executable file
·1268 lines (959 loc) · 40.2 KB
/
geom3d.py
File metadata and controls
executable file
·1268 lines (959 loc) · 40.2 KB
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
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Part of Spatial Math Toolbox for Python
# Copyright (c) 2000 Peter Corke
# MIT Licence, see details in top-level file: LICENCE
import numpy as np
import math
from collections import namedtuple
import matplotlib.pyplot as plt
import spatialmath.base as base
from spatialmath import SE3
from spatialmath.baseposelist import BasePoseList
_eps = np.finfo(np.float64).eps
# ======================================================================== #
class Plane3:
r"""
Create a plane object from linear coefficients
:param c: Plane coefficients
:type c: 4-element array_like
:return: a Plane object
:rtype: Plane
Planes are represented by the 4-vector :math:`[a, b, c, d]` which describes
the plane :math:`\pi: ax + by + cz + d=0`.
"""
def __init__(self, c):
self.plane = base.getvector(c, 4)
# point and normal
@classmethod
def PointNormal(cls, p, n):
"""
Create a plane object from point and normal
:param p: Point in the plane
:type p: array_like(3)
:param n: Normal vector to the plane
:type n: array_like(3)
:return: a Plane object
:rtype: Plane
:seealso: :meth:`ThreePoints` :meth:`LinePoint`
"""
n = base.getvector(n, 3) # normal to the plane
p = base.getvector(p, 3) # point on the plane
return cls(np.r_[n, -np.dot(n, p)])
# point and normal
@classmethod
def ThreePoints(cls, p):
"""
Create a plane object from three points
:param p: Three points in the plane
:type p: ndarray(3,3)
:return: a Plane object
:rtype: Plane
The points in ``p`` are arranged as columns.
:seealso: :meth:`PointNormal` :meth:`LinePoint`
"""
p = base.ismatrix(p, (3,3))
v1 = p[:,0]
v2 = p[:,1]
v3 = p[:,2]
# compute a normal
n = np.cross(v2-v1, v3-v1)
return cls(n, v1)
@classmethod
def LinePoint(cls, l, p):
"""
Create a plane object from a line and point
:param l: 3D line
:type l: Line3
:param p: Points in the plane
:type p: ndarray(3)
:return: a Plane object
:rtype: Plane
:seealso: :meth:`PointNormal` :meth:`ThreePoints`
"""
n = np.cross(l.w, p)
d = np.dot(l.v, p)
return cls(n, d)
@property
def n(self):
r"""
Normal to the plane
:return: Normal to the plane
:rtype: ndarray(3)
For a plane :math:`\pi: ax + by + cz + d=0` this is the vector
:math:`[a,b,c]`.
:seealso: :meth:`d`
"""
# normal
return self.plane[:3]
@property
def d(self):
r"""
Plane offset
:return: Offset of the plane
:rtype: float
For a plane :math:`\pi: ax + by + cz + d=0` this is the scalar
:math:`d`.
:seealso: :meth:`n`
"""
return self.plane[3]
def contains(self, p, tol=10*_eps):
"""
Test if point in plane
:param p: A 3D point
:type p: array_like(3)
:param tol: Tolerance, defaults to 10*_eps
:type tol: float, optional
:return: if the point is in the plane
:rtype: bool
"""
return abs(np.dot(self.n, p) - self.d) < tol
def plot(self, bounds=None, ax=None, **kwargs):
"""
Plot plane
:param bounds: bounds of plot volume, defaults to None
:type bounds: array_like(2|4|6), optional
:param ax: 3D axes to plot into, defaults to None
:type ax: Axes, optional
:param kwargs: optional arguments passed to ``plot_surface``
The ``bounds`` of the 3D plot volume is [xmin, xmax, ymin, ymax, zmin, zmax]
and a 3D plot is created if not already existing. If ``bounds`` is not
provided it is taken from current 3D axes.
The plane is drawn using ``plot_surface``.
:seealso: :func:`axes_logic`
"""
ax = base.axes_logic(ax, 3)
if bounds is None:
bounds = np.r_[ax.get_xlim(), ax.get_ylim(), ax.get_zlim()]
# X, Y = np.meshgrid(bounds[0: 2], bounds[2: 4])
# Z = -(X * self.plane[0] + Y * self.plane[1] + self.plane[3]) / self.plane[2]
X, Y = np.meshgrid(np.linspace(bounds[0], bounds[1], 50),
np.linspace(bounds[2], bounds[3], 50))
Z = -(X * self.plane[0] + Y * self.plane[1] + self.plane[3]) / self.plane[2]
Z[Z < bounds[4]] = np.nan
Z[Z > bounds[5]] = np.nan
ax.plot_surface(X, Y, Z, **kwargs)
def __str__(self):
"""
Convert plane to string representation
:return: Compact string representation of plane
:rtype: str
"""
return str(self.plane)
def __repr__(self):
"""
Display parameters of plane
:return: Compact string representation of plane
:rtype: str
"""
return str(self)
# ======================================================================== #
class Line3(BasePoseList):
__array_ufunc__ = None # allow pose matrices operators with NumPy values
def __init__(self, v=None, w=None):
"""
Create a Line3 object
:param v: Plucker coordinate vector, or Plucker moment vector
:type v: array_like(6) or array_like(3)
:param w: Plucker direction vector, optional
:type w: array_like(3), optional
:raises ValueError: bad arguments
:return: 3D line
:rtype: ``Line3`` instance
A representation of a 3D line using Plucker coordinates.
- ``Line3(P)`` creates a 3D line from a Plucker coordinate vector ``[v, w]``
where ``v`` (3,) is the moment and ``w`` (3,) is the line direction.
- ``Line3(v, w)`` as above but the components ``v`` and ``w`` are
provided separately.
- ``Line3(L)`` creates a copy of the ``Line3`` object ``L``.
.. note::
- The ``Line3`` object inherits from ``collections.UserList`` and has list-like
behaviours.
- A single ``Line3`` object contains a 1D array of Plucker coordinates.
- The elements of the array are guaranteed to be Plucker coordinates.
- The number of elements is given by ``len(L)``
- The elements can be accessed using index and slice notation, eg. ``L[1]`` or
``L[2:3]``
- The ``Line3`` instance can be used as an iterator in a for loop or list comprehension.
- Some methods support operations on the internal list.
:seealso: :meth:`TwoPoints` :meth:`Planes` :meth:`PointDir`
"""
super().__init__() # enable list powers
if w is None:
# zero or one arguments passed
if super().arghandler(v, convertfrom=(SE3,)):
return
else:
# additional arguments
assert base.isvector(v, 3) and base.isvector(w, 3), 'expecting two 3-vectors'
self.data = [np.r_[v, w]]
# needed to allow __rmul__ to work if left multiplied by ndarray
#self.__array_priority__ = 100
@property
def shape(self):
return (6,)
@staticmethod
def _identity():
return np.zeros((6,))
@staticmethod
def isvalid(x, check=False):
return x.shape == (6,)
@classmethod
def Join(cls, P=None, Q=None):
"""
Create 3D line from two 3D points
:param P: First 3D point
:type P: array_like(3)
:param Q: Second 3D point
:type Q: array_like(3)
:return: 3D line
:rtype: ``Line3`` instance
``Line3.Join(P, Q)`` create a ``Line3`` object that represents
the line joining the 3D points ``P`` (3,) and ``Q`` (3,). The direction
is from ``Q`` to ``P``.
:seealso: :meth:`IntersectingPlanes` :meth:`PointDir`
"""
P = base.getvector(P, 3)
Q = base.getvector(Q, 3)
# compute direction and moment
w = P - Q
v = np.cross(w, P)
return cls(np.r_[v, w])
@classmethod
def IntersectingPlanes(cls, pi1, pi2):
r"""
Create 3D line from intersection of two planes
:param pi1: First plane
:type pi1: array_like(4), or ``Plane``
:param pi2: Second plane
:type pi2: array_like(4), or ``Plane``
:return: 3D line
:rtype: ``Line3`` instance
``L = Plucker.IntersectingPlanes(π1, π2)`` is a Plucker object that represents
the line formed by the intersection of two planes ``π1`` and ``π3``.
Planes are represented by the 4-vector :math:`[a, b, c, d]` which describes
the plane :math:`\pi: ax + by + cz + d=0`.
:seealso: :meth:`Join` :meth:`PointDir`
"""
# TODO inefficient to create 2 temporary planes
if not isinstance(pi1, Plane3):
pi1 = Plane3(base.getvector(pi1, 4))
if not isinstance(pi2, Plane3):
pi2 = Plane3(base.getvector(pi2, 4))
w = np.cross(pi1.n, pi2.n)
v = pi2.d * pi1.n - pi1.d * pi2.n
return cls(np.r_[v, w])
@classmethod
def PointDir(cls, point, dir):
"""
Create 3D line from a point and direction
:param point: A 3D point
:type point: array_like(3)
:param dir: Direction vector
:type dir: array_like(3)
:return: 3D line
:rtype: ``Line3`` instance
``Line3.pointdir(P, W)`` is a Plucker object that represents the
line containing the point ``P`` and parallel to the direction vector ``W``.
:seealso: :meth:`Join` :meth:`IntersectingPlanes`
"""
p = base.getvector(point, 3)
w = base.getvector(dir, 3)
v = np.cross(w, p)
return cls(np.r_[v, w])
def append(self, x):
"""
:param x: Plucker object
:type x: Plucker
:raises ValueError: Attempt to append a non Plucker object
:return: Plucker object with new Plucker line appended
:rtype: Line3 instance
"""
#print('in append method')
if not type(self) == type(x):
raise ValueError("can pnly append Plucker object")
if len(x) > 1:
raise ValueError("cant append a Plucker sequence - use extend")
super().append(x.A)
@property
def A(self):
# get the underlying numpy array
if len(self.data) == 1:
return self.data[0]
else:
return self.data
def __getitem__(self, i):
# print('getitem', i, 'class', self.__class__)
return self.__class__(self.data[i])
@property
def v(self):
r"""
Moment vector
:return: the moment vector
:rtype: ndarray(3)
The line is represented by a vector :math:`(\vec{v}, \vec{w}) \in \mathbb{R}^6`.
:seealso: :meth:`w`
"""
return self.data[0][0:3]
@property
def w(self):
r"""
Direction vector
:return: the direction vector
:rtype: ndarray(3)
The line is represented by a vector :math:`(\vec{v}, \vec{w}) \in \mathbb{R}^6`.
:seealso: :meth:`v` :meth:`uw`
"""
return self.data[0][3:6]
@property
def uw(self):
r"""
Line direction as a unit vector
:return: Line direction as a unit vector
:rtype: ndarray(3,)
``line.uw`` is a unit-vector parallel to the line.
The line is represented by a vector :math:`(\vec{v}, \vec{w}) \in \mathbb{R}^6`.
:seealso: :meth:`w`
"""
return base.unitvec(self.w)
@property
def vec(self):
r"""
Line as a Plucker coordinate vector
:return: Plucker coordinate vector
:rtype: ndarray(6,)
``line.vec`` is the Plucker coordinate vector :math:`(\vec{v}, \vec{w}) \in \mathbb{R}^6`.
"""
return np.r_[self.v, self.w]
def skew(self):
r"""
Line as a Plucker skew-symmetric matrix
:return: Skew-symmetric matrix form of Plucker coordinates
:rtype: ndarray(4,4)
``line.skew()`` is the Plucker matrix, a 4x4 skew-symmetric matrix
representation of the line whose six unique elements are the
Plucker coordinates of the line.
.. math::
\sk{L} = \begin{bmatrix} 0 & v_z & -v_y & \omega_x \\
-v_z & 0 & v_x & \omega_y \\
v_y & -v_x & 0 & \omega_z \\
-\omega_x & -\omega_y & -\omega_z & 0 \end{bmatrix}
.. note::
- For two homogeneous points P and Q on the line, :math:`PQ^T-QP^T` is
also skew symmetric.
- The projection of Plucker line by a perspective camera is a
homogeneous line (3x1) given by :math:`\vee C M C^T` where :math:`C
\in \mathbf{R}^{3 \times 4}` is the camera matrix.
"""
v = self.v
w = self.w
# the following matrix is at odds with H&Z pg. 72
return np.array([
[ 0, v[2], -v[1], w[0]],
[-v[2], 0 , v[0], w[1]],
[ v[1], -v[0], 0, w[2]],
[-w[0], -w[1], -w[2], 0 ]
])
@property
def pp(self):
"""
Principal point of the 3D line
:return: Principal point of the line
:rtype: ndarray(3)
``line.pp`` is the point on the line that is closest to the origin.
Notes:
- Same as Plucker.point(0)
:seealso: :meth:`ppd` :meth`point`
"""
return np.cross(self.v, self.w) / np.dot(self.w, self.w)
@property
def ppd(self):
"""
Distance from principal point to the origin
:return: Distance from principal point to the origin
:rtype: float
``line.ppd`` is the distance from the principal point to the origin.
This is the smallest distance of any point on the line
to the origin.
:seealso: :meth:`pp`
"""
return math.sqrt(np.dot(self.v, self.v) / np.dot(self.w, self.w) )
def point(self, lam):
r"""
Generate point on line
:param lam: Scalar distance from principal point
:type lam: float
:return: Distance from principal point to the origin
:rtype: float
``line.point(λ)`` is a point on the line, where ``λ`` is the parametric
distance along the line from the principal point of the line such
that :math:`P = P_p + \lambda \hat{d}` and :math:`\hat{d}` is the line
direction given by ``line.uw``.
:seealso: :meth:`pp` :meth:`closest` :meth:`uw` :meth:`lam`
"""
lam = base.getvector(lam, out='row')
return self.pp.reshape((3,1)) + self.uw.reshape((3,1)) * lam
def lam(self, point):
r"""
Parametric distance from principal point
:param point: 3D point
:type point: array_like(3)
:return: parametric distance λ
:rtype: float
``line.lam(P)`` is the value of :math:`\lambda` such that
:math:`Q = P_p + \lambda \hat{d}` is closest to ``P``.
:seealso: :meth:`point`
"""
return np.dot( point.flatten() - self.pp, self.uw)
# ------------------------------------------------------------------------- #
# TESTS ON PLUCKER OBJECTS
# ------------------------------------------------------------------------- #
def contains(self, x, tol=50*_eps):
"""
Test if points are on the line
:param x: 3D point
:type x: 3-element array_like, or numpy.ndarray, shape=(3,N)
:param tol: Tolerance, defaults to 50*_eps
:type tol: float, optional
:raises ValueError: Bad argument
:return: Whether point is on the line
:rtype: bool or numpy.ndarray(N) of bool
``line.contains(X)`` is true if the point ``X`` lies on the line defined by
the Plucker object self.
If ``X`` is an array with 3 rows, the test is performed on every column and
an array of booleans is returned.
"""
if base.isvector(x, 3):
x = base.getvector(x)
return np.linalg.norm( np.cross(x - self.pp, self.w) ) < tol
elif base.ismatrix(x, (3,None)):
return [np.linalg.norm(np.cross(_ - self.pp, self.w)) < tol for _ in x.T]
else:
raise ValueError('bad argument')
def __eq__(l1, l2): # lgtm[py/not-named-self] pylint: disable=no-self-argument
"""
Test if two lines are equivalent
:param l2: Second line
:type l2: ``Line3``
:return: lines are equivalent
:rtype: bool
``L1 == L2`` is True if the ``Line3`` objects describe the same line in
space. Note that because of the over parameterization, lines can be
equivalent even if their coordinate vectors are different.
:seealso: :meth:`__ne__`
"""
return abs( 1 - np.dot(base.unitvec(l1.vec), base.unitvec(l2.vec))) < 10*_eps
def __ne__(l1, l2): # lgtm[py/not-named-self] pylint: disable=no-self-argument
"""
Test if two lines are not equivalent
:param l2: Second line
:type l2: ``Line3``
:return: lines are not equivalent
:rtype: bool
``L1 != L2`` is True if the Plucker objects describe different lines in
space. Note that because of the over parameterization, lines can be
equivalent even if their coordinate vectors are different.
:seealso: :meth:`__ne__`
"""
return not l1.__eq__(l2)
def isparallel(l1, l2, tol=10*_eps): # lgtm[py/not-named-self] pylint: disable=no-self-argument
"""
Test if lines are parallel
:param l2: Second line
:type l2: ``Line3``
:return: lines are parallel
:rtype: bool
``l1.isparallel(l2)`` is true if the two lines are parallel.
``l1 | l2`` as above but in binary operator form
:seealso: :meth:`__or__` :meth:`intersects`
"""
return np.linalg.norm(np.cross(l1.w, l2.w) ) < tol
def __or__(l1, l2): # lgtm[py/not-named-self] pylint: disable=no-self-argument
"""
Overloaded ``|`` operator tests for parallelism
:param l2: Second line
:type l2: ``Line3``
:return: lines are parallel
:rtype: bool
``l1 | l2`` is an operator which is true if the two lines are parallel.
.. note:: The ``|`` operator has low precendence.
:seealso: :meth:`isparallel` :meth:`__xor__`
"""
return l1.isparallel(l2)
def __xor__(l1, l2): # lgtm[py/not-named-self] pylint: disable=no-self-argument
"""
Overloaded ``^`` operator tests for intersection
:param l2: Second line
:type l2: Plucker
:return: lines intersect
:rtype: bool
``l1 ^ l2`` is an operator which is true if the two lines intersect.
.. note::
- The ``^`` operator has low precendence.
- Is ``False`` if the lines are equivalent since they would intersect at
an infinite number of points.
:seealso: :meth:`intersects` :meth:`parallel`
"""
return not l1.isparallel(l2) and (abs(l1 * l2) < 10*_eps )
# ------------------------------------------------------------------------- #
# PLUCKER LINE DISTANCE AND INTERSECTION
# ------------------------------------------------------------------------- #
def intersects(l1, l2): # lgtm[py/not-named-self] pylint: disable=no-self-argument
"""
Intersection point of two lines
:param l2: Second line
:type l2: ``Line3``
:return: 3D intersection point
:rtype: ndarray(3) or None
``l1.intersects(l2)`` is the point of intersection of the two lines, or
``None`` if the lines do not intersect or are equivalent.
:seealso: :meth:`commonperp :meth:`eq` :meth:`__xor__`
"""
if l1^l2:
# lines do intersect
return -(np.dot(l1.v, l2.w) * np.eye(3, 3) + \
l1.w.reshape((3,1)) @ l2.v.reshape((1,3)) - \
l2.w.reshape((3,1)) @ l1.v.reshape((1,3))) * base.unitvec(np.cross(l1.w, l2.w))
else:
# lines don't intersect
return None
def distance(l1, l2): # lgtm[py/not-named-self] pylint: disable=no-self-argument
"""
Minimum distance between lines
:param l2: Second line
:type l2: ``Line3``
:return: Closest distance between lines
:rtype: float
``l1.distance(l2) is the minimum distance between two lines.
.. notes:: Works for parallel, skew and intersecting lines.
:seealso: :meth:`closest_to_line`
"""
if l1 | l2:
# lines are parallel
l = np.cross(l1.w, l1.v - l2.v * np.dot(l1.w, l2.w) / dot(l2.w, l2.w)) / np.linalg.norm(l1.w)
else:
# lines are not parallel
if abs(l1 * l2) < 10*_eps:
# lines intersect at a point
l = 0
else:
# lines don't intersect, find closest distance
l = abs(l1 * l2) / np.linalg.norm(np.cross(l1.w, l2.w))**2
return l
def closest_to_line(self, other):
"""
Closest point between lines
:param other: second line
:type other: Line3
:return: nearest points and distance between lines at those points
:rtype: ndarray(3,N), ndarray(N)
There are four cases:
* ``len(self) == len(other) == 1`` find the point on the first line closest to the second line, as well
as the minimum distance between the lines.
* ``len(self) == 1, len(other) == N`` find the point of intersection between the first
line and the ``N`` other lines, returning ``N`` intersection points and distances.
* ``len(self) == N, len(other) == 1`` find the point of intersection between the ``N`` first
lines and the other line, returning ``N`` intersection points and distances.
* ``len(self) == N, len(other) == M`` for each of the ``N`` first
lines find the closest intersection with each of the ``M`` other lines, returning ``N``
intersection points and distances.
** this last one should be an option, default behavior would be to
test self[i] against line[i]
** maybe different function
For two sets of lines, of equal size, return an array of closest points
and distances.
Example::
.. runblock:: pycon
>>> from spatialmath import Plucker
>>> line1 = Plucker.TwoPoints([1, 1, 0], [1, 1, 1])
>>> line2 = Plucker.TwoPoints([0, 0, 0], [2, 3, 5])
>>> line1.closest_to_line(line2)
:reference: `Plucker coordinates <https://web.cs.iastate.edu/~cs577/handouts/plucker-coordinates.pdf>`_
:seealso: :meth:`distance`
"""
# point on line closest to another line
# https://web.cs.iastate.edu/~cs577/handouts/plucker-coordinates.pdf
# but (20) (21) is the negative of correct answer
points = []
dists = []
def intersection(line1, line2):
with np.errstate(divide='ignore', invalid='ignore'):
# compute the distance between all pairs of lines
v1 = line1.v
w1 = line1.w
v2 = line2.v
w2 = line2.w
p1 = (np.cross(v1, np.cross(w2, np.cross(w1, w2))) - np.dot(v2, np.cross(w1, w2)) * w1) \
/ np.sum(np.cross(w1, w2) ** 2)
p2 = (np.cross(-v2, np.cross(w1, np.cross(w1, w2))) + np.dot(v1, np.cross(w1, w2)) * w2) \
/ np.sum(np.cross(w1, w2) ** 2)
return p1, np.linalg.norm(p1 - p2)
if len(self) == len(other):
# two sets of lines of equal length
for line1, line2 in zip(self, other):
point, dist = intersection(line1, line2)
points.append(point)
dists.append(dist)
elif len(self) == 1 and len(other) > 1:
for line in other:
point, dist = intersection(self, line)
points.append(point)
dists.append(dist)
elif len(self) > 1 and len(other) == 1:
for line in self:
point, dist = intersection(line, other)
points.append(point)
dists.append(dist)
if len(points) == 1:
# 1D case for self or line
return points[0], dists[0]
else:
return np.array(points).T, np.array(dists)
def closest_to_point(self, x):
"""
Point on line closest to given point
:param x: An arbitrary 3D point
:type x: array_like(3)
:return: Point on the line and distance to line
:rtype: ndarray(3), float
Find the point on the line closest to ``x`` as well as the distance
at that closest point.
Example:
.. runblock:: pycon
>>> from spatialmath import Plucker
>>> line1 = Plucker.TwoPoints([0, 0, 0], [2, 2, 3])
>>> line1.closest_to_point([1, 1, 1])
:seealso: meth:`point`
"""
# http://www.ahinson.com/algorithms_general/Sections/Geometry/PluckerLine.pdf
# has different equation for moment, the negative
x = base.getvector(x, 3)
lam = np.dot(x - self.pp, self.uw)
p = self.point(lam).flatten() # is the closest point on the line
d = np.linalg.norm( x - p)
return p, d
def commonperp(l1, l2): # lgtm[py/not-named-self] pylint: disable=no-self-argument
"""
Common perpendicular to two lines
:param l2: Second line
:type l2: Line3
:return: Perpendicular line
:rtype: Line3 instance or None
``l1.commonperp(l2)`` is the common perpendicular line between the two lines.
Returns ``None`` if the lines are parallel.
:seealso: :meth:`intersect`
"""
if l1 | l2:
# no common perpendicular if lines are parallel
return None
else:
# lines are skew or intersecting
w = np.cross(l1.w, l2.w)
v = np.cross(l1.v, l2.w) - np.cross(l2.v, l1.w) + \
(l1 * l2) * np.dot(l1.w, l2.w) * base.unitvec(np.cross(l1.w, l2.w))
return l1.__class__(v, w)
def __mul__(left, right): # lgtm[py/not-named-self] pylint: disable=no-self-argument
r"""
Reciprocal product
:param left: Left operand
:type left: Line3
:param right: Right operand
:type right: Line3
:return: reciprocal product
:rtype: float
``left * right`` is the scalar reciprocal product :math:`\hat{w}_L \dot m_R + \hat{w}_R \dot m_R`.
.. note::
- Multiplication or composition of Plucker lines is not defined.
- Pre-multiplication by an SE3 object is supported, see ``__rmul__``.
:seealso: :meth:`__rmul__`
"""
if isinstance(right, Line3):
# reciprocal product
return np.dot(left.uw, right.v) + np.dot(right.uw, left.v)
else:
raise ValueError('bad arguments')
def __rmul__(right, left): # lgtm[py/not-named-self] pylint: disable=no-self-argument
"""
Rigid-body transformation of 3D line
:param left: Rigid-body transform
:type left: SE3
:param right: 3D line
:type right: Line
:return: transformed 3D line
:rtype: Line3 instance
``T * line`` is the line transformed by the rigid body transformation ``T``.
:seealso: :meth:`__mul__`
"""
if isinstance(left, SE3):
A = left.inv().Ad()
return right.__class__( A @ right.vec) # premultiply by SE3.Ad
else:
raise ValueError('can only premultiply Line3 by SE3')
# ------------------------------------------------------------------------- #
# PLUCKER LINE DISTANCE AND INTERSECTION
# ------------------------------------------------------------------------- #
def intersect_plane(self, plane): # lgtm[py/not-named-self] pylint: disable=no-self-argument
r"""
Line intersection with a plane
:param plane: A plane
:type plane: array_like(4) or Plane
:return: Intersection point, λ
:rtype: ndarray(3), float
- ``P, λ = line.intersect_plane(plane)`` is the point where the line
intersects the plane, and the corresponding λ value.
Return None, None if no intersection.
The plane can be specified as:
- a 4-vector :math:`[a, b, c, d]` which describes the plane :math:`\pi: ax + by + cz + d=0`.
- a ``Plane`` object
The return value is a named tuple with elements:
- ``.p`` for the point on the line as a numpy.ndarray, shape=(3,)
- ``.lam`` the `lambda` value for the point on the line.
:sealso: :meth:`point` :class:`Plane`
"""
# Line U, V
# Plane N n
# (VxN-nU:U.N)
# Note that this is in homogeneous coordinates.
# intersection of plane (n,p) with the line (v,p)
# returns point and line parameter
if not isinstance(plane, Plane3):
plane = Plane3(base.getvector(plane, 4))
den = np.dot(self.w, plane.n)
if abs(den) > (100*_eps):
# P = -(np.cross(line.v, plane.n) + plane.d * line.w) / den
p = (np.cross(self.v, plane.n) - plane.d * self.w) / den
t = self.lam(p)
return namedtuple('intersect_plane', 'p lam')(p, t)
else:
return None
def intersect_volume(self, bounds):
"""
Line intersection with a volume
:param bounds: Bounds of an axis-aligned rectangular cuboid
:type plane: array_like(6)
:return: Intersection point, λ value
:rtype: ndarray(3,N), ndarray(N)
``P, λ = line.intersect_volume(bounds)`` is a matrix (3xN) with columns
that indicate where the line intersects the faces of the volume and
the corresponding λ values.
The volume is specified by ``bounds`` = [xmin xmax ymin ymax zmin zmax].
The number of
columns N is either:
- 0, when the line is outside the plot volume or,
- 2 when the line pierces the bounding volume.
See also :meth:`plot` :meth:`point`
"""
intersections = []
# reshape, top row is minimum, bottom row is maximum
bounds23 = bounds.reshape((3, 2))
for face in range(0, 6):
# for each face of the bounding volume
# x=xmin, x=xmax, y=ymin, y=ymax, z=zmin, z=zmax
# planes are:
# 0 normal in x direction, xmin
# 1 normal in x direction, xmax
# 2 normal in y direction, ymin
# 3 normal in y direction, ymax
# 4 normal in z direction, zmin