ProjectiveGeometry23 0.1.0__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.
- ProjectiveGeometry23/__init__.py +1 -0
- ProjectiveGeometry23/central_projection.py +249 -0
- ProjectiveGeometry23/estimation.py +71 -0
- ProjectiveGeometry23/homography.py +136 -0
- ProjectiveGeometry23/pluecker.py +161 -0
- ProjectiveGeometry23/source_detector_geometry.py +103 -0
- ProjectiveGeometry23/svg_utils.py +141 -0
- ProjectiveGeometry23/utils.py +238 -0
- projectivegeometry23-0.1.0.dist-info/METADATA +142 -0
- projectivegeometry23-0.1.0.dist-info/RECORD +13 -0
- projectivegeometry23-0.1.0.dist-info/WHEEL +5 -0
- projectivegeometry23-0.1.0.dist-info/licenses/LICENSE +201 -0
- projectivegeometry23-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Computations on the detector in pixels and 3D millimeters of source-detector systems such as X-ray.
|
|
3
|
+
|
|
4
|
+
Author: André Aichert
|
|
5
|
+
Date: June 22, 2023
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from numpy.linalg import norm
|
|
10
|
+
from .central_projection import ProjectionMatrix
|
|
11
|
+
from .utils import cvec, append, dehomogenize
|
|
12
|
+
|
|
13
|
+
class SourceDetectorGeometry:
|
|
14
|
+
def __init__(self, projection: ProjectionMatrix):
|
|
15
|
+
"""From a 3x4 projection matrix and pixel spacing, compute physical location of detector.
|
|
16
|
+
This allows for a full visualization of a source-detector geometry as a pyramid or frustum.
|
|
17
|
+
In case you have a left-handed coordinate frame, use negative pixel spacing to swap the
|
|
18
|
+
viewing direction of the system (i.e. negate pixel spacing if projection looks backwards."""
|
|
19
|
+
projection.normalize()
|
|
20
|
+
|
|
21
|
+
C = projection.getCenterOfProjection()
|
|
22
|
+
|
|
23
|
+
m1 = projection.P[0, :3]
|
|
24
|
+
m2 = projection.P[1, :3]
|
|
25
|
+
m3 = projection.P[2, :3]
|
|
26
|
+
|
|
27
|
+
U = np.append(np.cross(m3, m2), 0)
|
|
28
|
+
V = np.append(np.cross(m3, m1), 0)
|
|
29
|
+
|
|
30
|
+
U *= projection.pixel_spacing / norm(U)
|
|
31
|
+
V *= projection.pixel_spacing / norm(V)
|
|
32
|
+
|
|
33
|
+
principal_plane = projection.P[2, :4]
|
|
34
|
+
|
|
35
|
+
# Note how this conversion is symmetric for U and V.
|
|
36
|
+
V_dir = V[:3] / projection.pixel_spacing
|
|
37
|
+
f = np.dot(m1, np.cross(V_dir, m3))
|
|
38
|
+
|
|
39
|
+
# However, the results will be the same ONLY in the case of rectangular pixels.
|
|
40
|
+
# Assumption of rectangular pixels for a digital detector is pretty safe though.
|
|
41
|
+
image_plane = principal_plane.copy()
|
|
42
|
+
image_plane[3] -= f * projection.pixel_spacing
|
|
43
|
+
|
|
44
|
+
# negative pixel spacings support flipping detector axes (left handed systems)
|
|
45
|
+
# Note how mulpiplication of a projection matrix with -1 inverts viewing direction.
|
|
46
|
+
# if projection.pixel_spacing < 0:
|
|
47
|
+
# V *= -1
|
|
48
|
+
|
|
49
|
+
central_projection = SourceDetectorGeometry.centralProjectionToPlane(C, image_plane)
|
|
50
|
+
|
|
51
|
+
source_detector_distance = np.dot(image_plane, C)[0]
|
|
52
|
+
|
|
53
|
+
principal_point_3d = cvec(C) - append(m3, 0) * source_detector_distance
|
|
54
|
+
|
|
55
|
+
principal_point = np.dot(projection.P, principal_point_3d)
|
|
56
|
+
pp = principal_point / principal_point[2]
|
|
57
|
+
|
|
58
|
+
# This is the corner of the detector where the pixel origin is located
|
|
59
|
+
detector_origin = principal_point_3d - cvec(U) * pp[0] - cvec(V) * pp[1]
|
|
60
|
+
|
|
61
|
+
# things that fully define the source-detector geometry:
|
|
62
|
+
self.source_position = C
|
|
63
|
+
self.detector_origin = detector_origin
|
|
64
|
+
self.axis_direction_Upx = U
|
|
65
|
+
self.axis_direction_Vpx = V
|
|
66
|
+
# plus some useful extras
|
|
67
|
+
self.image_plane = image_plane
|
|
68
|
+
self.principal_point_3d = principal_point_3d
|
|
69
|
+
self.source_detector_distance = source_detector_distance
|
|
70
|
+
# and a projection matrix in 3D world coordinates (3D points -> 3D points on the detector).
|
|
71
|
+
self.central_projection_3d = central_projection
|
|
72
|
+
|
|
73
|
+
def __repr__(self):
|
|
74
|
+
return "\n ".join([
|
|
75
|
+
"SourceDetectorGeometry:",
|
|
76
|
+
f"Source Position: {self.source_position.flatten()}",
|
|
77
|
+
f"Source Detector Distance: {self.source_detector_distance}",
|
|
78
|
+
f"Detector Origin: {self.detector_origin[:,0].tolist()}",
|
|
79
|
+
f"Principal Point 3D: {self.principal_point_3d[:,0].tolist()}",
|
|
80
|
+
f"Axis Orientation:\n U={self.axis_direction_Upx}\n V={self.axis_direction_Vpx}"
|
|
81
|
+
])
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def centralProjectionToPlane(cls, C, E):
|
|
86
|
+
"""A mapping T from a 3D point C to a plane E via central projection from C.
|
|
87
|
+
Mapping is according to T*X=meet(join(C,X),E) written in matrix form."""
|
|
88
|
+
C = C.flatten()
|
|
89
|
+
E = E.flatten()
|
|
90
|
+
T = [[+ C[1]*E[1] + C[2]*E[2] + C[3]*E[3] , - C[0]*E[1] , - C[0]*E[2] , - C[0]*E[3] ], # noqa
|
|
91
|
+
[- C[1]*E[0] , + C[0]*E[0] + C[2]*E[2] + C[3]*E[3] , - C[1]*E[2] , - C[1]*E[3] ], # noqa
|
|
92
|
+
[- C[2]*E[0] , - C[2]*E[1] , + C[0]*E[0] + C[3]*E[3] + C[1]*E[1] , - C[2]*E[3] ], # noqa
|
|
93
|
+
[- C[3]*E[0] , - C[3]*E[1] , - C[3]*E[2] , + C[0]*E[0] + C[1]*E[1] + C[2]*E[2] ]] # noqa
|
|
94
|
+
return T
|
|
95
|
+
|
|
96
|
+
def detectorPixelIn3Dmm(self, u, v):
|
|
97
|
+
""" Compute the 3D location of a pixel (u, v) in world coordinates (mm) . See also: ProjectionMatrix.sourceDetectorGeometry()
|
|
98
|
+
This function mostly serves as documentation for how to interpret the source detector geometry."""
|
|
99
|
+
return self.detector_origin + self.axis_direction_Upx * u + self.axis_direction_Vpx * v
|
|
100
|
+
|
|
101
|
+
def projectToDetector3Dmm(self, X):
|
|
102
|
+
return self.central_projection_3d @ X
|
|
103
|
+
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interactive drawing of points, lines in 2d and 3D, as well as vidualizing X-ray source-detector geometries.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
!pip install svg_vis
|
|
6
|
+
|
|
7
|
+
from svg.Jupyter import CanvasWithOverlay
|
|
8
|
+
from pg.homography import rotation_x, rotation_z, scale
|
|
9
|
+
from svg.Renderer import RenderSVG
|
|
10
|
+
|
|
11
|
+
target = ProjectionMatrix([...], detector_size_px, spacing)
|
|
12
|
+
|
|
13
|
+
vis = CanvasWithOverlay(target.image_size[0], target.image_size[1])
|
|
14
|
+
|
|
15
|
+
# World transformation
|
|
16
|
+
ax, az = 0.5, 0.5
|
|
17
|
+
s = 0.2
|
|
18
|
+
|
|
19
|
+
def handle_draw(vis):
|
|
20
|
+
global ax
|
|
21
|
+
global az
|
|
22
|
+
x,y = vis.mouse_state.pos()
|
|
23
|
+
if vis.mouse_state.clicked:
|
|
24
|
+
az += vis.mouse_state.dx * 0.01
|
|
25
|
+
ax += vis.mouse_state.dy * 0.01
|
|
26
|
+
|
|
27
|
+
svg = RenderSVG((vis.w, vis.h))
|
|
28
|
+
|
|
29
|
+
svg.add(svg_world_geometry)
|
|
30
|
+
svg.add(svg_source_detector, projection=target,
|
|
31
|
+
draw_on_detector=svg_world_geometry,
|
|
32
|
+
label_source='C0', label_detector='I0(u,v)')
|
|
33
|
+
|
|
34
|
+
T = scale(s) @ rotation_x(ax) @ rotation_z(az)
|
|
35
|
+
raw_svg_code = svg.render(P=target.P@T)
|
|
36
|
+
vis.html_overlay.value = raw_svg_code
|
|
37
|
+
|
|
38
|
+
vis.handle_draw = handle_draw
|
|
39
|
+
|
|
40
|
+
vis.display()
|
|
41
|
+
|
|
42
|
+
Author: André Aichert
|
|
43
|
+
Date: Dec 11th, 2023
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
from svg_snip.Composer import Composer
|
|
48
|
+
import svg_snip.Elements as e2d
|
|
49
|
+
import svg_snip.Elements3D as e3d
|
|
50
|
+
|
|
51
|
+
import ProjectiveGeometry23.utils as pgu
|
|
52
|
+
from ProjectiveGeometry23 import pluecker
|
|
53
|
+
|
|
54
|
+
from ProjectiveGeometry23.central_projection import ProjectionMatrix
|
|
55
|
+
from ProjectiveGeometry23.source_detector_geometry import SourceDetectorGeometry
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def svg_coordinate_frame(P, size=100, **kwargs):
|
|
59
|
+
"""Draw a coordinate system of default size 100."""
|
|
60
|
+
el = [
|
|
61
|
+
'<g>',
|
|
62
|
+
# Coordinate frame
|
|
63
|
+
e3d.line(P=P, X1=[0,0,0,1], X2=[size,0,0,1], stroke='red', **kwargs),
|
|
64
|
+
e3d.line(P=P, X1=[0,0,0,1], X2=[0,size,0,1], stroke='green', **kwargs),
|
|
65
|
+
e3d.line(P=P, X1=[0,0,0,1], X2=[0,0,size,1], stroke='blue', **kwargs)
|
|
66
|
+
]
|
|
67
|
+
return '\n '.join(el) + '\n</g>\n'
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def svg_world_geometry(P, **kwargs):
|
|
71
|
+
"""Draw a coordinate system of size 100 and a wire cube
|
|
72
|
+
of the same size centered in the origin."""
|
|
73
|
+
el = [
|
|
74
|
+
'<g>',
|
|
75
|
+
# Coordinate frame
|
|
76
|
+
e3d.line(P=P, X1=[0,0,0,1], X2=[100,0,0,1], stroke='red', **kwargs),
|
|
77
|
+
e3d.line(P=P, X1=[0,0,0,1], X2=[0,100,0,1], stroke='green', **kwargs),
|
|
78
|
+
e3d.line(P=P, X1=[0,0,0,1], X2=[0,0,100,1], stroke='blue', **kwargs),
|
|
79
|
+
# a cube with size 100
|
|
80
|
+
e3d.wire_cube(P=P, min=[-50,-50,-50], max=[50,50,50], stroke='black', **kwargs)
|
|
81
|
+
]
|
|
82
|
+
return '\n '.join(el) + '\n</g>\n'
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def svg_source_detector(P, projection: ProjectionMatrix, draw_on_detector=None, **kwargs):
|
|
86
|
+
"""Draw X-ray source-detector geometry.
|
|
87
|
+
Define draw_on_detector as any SVG drawing function to project
|
|
88
|
+
additional 3D geometry to the detector plane."""
|
|
89
|
+
sdg = SourceDetectorGeometry(projection)
|
|
90
|
+
C = pgu.cvec(sdg.source_position)
|
|
91
|
+
O = sdg.detector_origin
|
|
92
|
+
U = pgu.cvec(sdg.axis_direction_Upx) * projection.image_size[0]
|
|
93
|
+
V = pgu.cvec(sdg.axis_direction_Vpx) * projection.image_size[1]
|
|
94
|
+
|
|
95
|
+
el = [
|
|
96
|
+
'<g>\n',
|
|
97
|
+
# Source position
|
|
98
|
+
e3d.point(P=P, X=C, r=1, fill="black", **kwargs),
|
|
99
|
+
# Detector frame
|
|
100
|
+
e3d.polygon(P=P, Xs=[O, O+U, O+V+U ,O+V],
|
|
101
|
+
fill="#00000020", stroke="#00000040", **kwargs),
|
|
102
|
+
e3d.line(P=P, X1=O, X2=O + U, stroke="magenta", **kwargs),
|
|
103
|
+
e3d.line(P=P, X1=O, X2=O + V, stroke="cyan", **kwargs),
|
|
104
|
+
# Frustum
|
|
105
|
+
e3d.line(P=P, X1=C, X2=O, stroke="#00000020", **kwargs),
|
|
106
|
+
e3d.line(P=P, X1=C, X2=O+V, stroke="#00000020", **kwargs),
|
|
107
|
+
e3d.line(P=P, X1=C, X2=O+U, stroke="#00000020", **kwargs),
|
|
108
|
+
e3d.line(P=P, X1=C, X2=O+V+U, stroke="#00000020", **kwargs)
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
if 'label_source' in kwargs:
|
|
112
|
+
el += [e3d.text(P=P, X=C, content=kwargs['label_source'], **kwargs)]
|
|
113
|
+
if 'label_detector' in kwargs:
|
|
114
|
+
el += [e3d.text(P=P, X=O, content=kwargs['label_detector'], **kwargs)]
|
|
115
|
+
|
|
116
|
+
if draw_on_detector is not None:
|
|
117
|
+
T_detector = sdg.central_projection_3d
|
|
118
|
+
detector = draw_on_detector(P=P@T_detector, **kwargs)
|
|
119
|
+
else:
|
|
120
|
+
detector = ""
|
|
121
|
+
|
|
122
|
+
return '\n<!-->Source Detector Geometry<-->\n' + detector + '\n '.join(el) + '\n</g>\n'
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def svg_homogeneous_line(l, composer: Composer, stroke="yellow", **kwargs):
|
|
126
|
+
"""Draw a 2D line given in homogeneous coordinates.
|
|
127
|
+
Note: composer is passed in automatically via svg.Renderer.
|
|
128
|
+
"""
|
|
129
|
+
w, h = composer.image_size
|
|
130
|
+
l = pgu.cvec(l)
|
|
131
|
+
x1, y1, x2, y2 = pgu.intersectLineWithRect(l, w, h)
|
|
132
|
+
if not all(isinstance(v, float) for v in [x1, y1, x2, y2]):
|
|
133
|
+
return ""
|
|
134
|
+
return e2d.line(x1=x1, y1=y1, x2=x2, y2=y2, composer=composer, stroke=stroke, **kwargs)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def svg_pluecker_line(P, L, **kwargs):
|
|
138
|
+
"""Draw a 3D line given in plucker coordinates."""
|
|
139
|
+
l = pluecker.project(L, P)
|
|
140
|
+
return svg_homogeneous_line(l, **kwargs)
|
|
141
|
+
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for geometry.
|
|
3
|
+
|
|
4
|
+
Note that these functions are imported directly to projective_geometry.
|
|
5
|
+
|
|
6
|
+
Example (compute line through point [2, 1] pointing right):
|
|
7
|
+
import projective_geometry as pg
|
|
8
|
+
pg.join(RP2Point(1, 2, 1), RP2Point(0, 1, 0))
|
|
9
|
+
|
|
10
|
+
Author: André Aichert
|
|
11
|
+
Date: June 22, 2023
|
|
12
|
+
|
|
13
|
+
FIXME (?) numpy is awkward with keeping track of row versus column vectors.
|
|
14
|
+
Possibly simplify code to just use 1D arrays?
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
from numpy.linalg import pinv
|
|
20
|
+
import shlex
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def dot(a,b):
|
|
24
|
+
"""Computes dot product for two vectors, no matter the actual shape of the
|
|
25
|
+
numpy array (e.g. 2D row or 2D column or 1D) """
|
|
26
|
+
return np.dot(a.ravel(), b.ravel())
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def cvec(vector):
|
|
30
|
+
"""Column vector from 1D array or list of values."""
|
|
31
|
+
vector = np.array(vector)
|
|
32
|
+
if vector.ndim == 1:
|
|
33
|
+
vector = vector.reshape(-1, 1)
|
|
34
|
+
return vector
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def nullspace(M):
|
|
38
|
+
""" Solve M@X == 0 for |X| == 1.
|
|
39
|
+
Returns solution closest to 0 if M is full rank"""
|
|
40
|
+
_, _, V = np.linalg.svd(M)
|
|
41
|
+
return V[-1, :]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def append(vector, last_coordinate):
|
|
45
|
+
""""Takes a vector as a list of values or an np.array, interprets it as a
|
|
46
|
+
column vector and appends the value last_coordinate to the end of it."""
|
|
47
|
+
vector = cvec(vector)
|
|
48
|
+
return np.vstack([vector, [last_coordinate]])
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def homogenize(euclidean):
|
|
52
|
+
""""Takes a vector as a list of values or an np.array, interprets it as a
|
|
53
|
+
column vector and appends a one to the end of it."""
|
|
54
|
+
return append(euclidean, 1.0)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def infinite(direction):
|
|
58
|
+
""""Takes a vector as a list of values or an np.array, interprets it as a
|
|
59
|
+
column vector and appends a zero to the end of it. Normalized to unit."""
|
|
60
|
+
return append(direction, 0.0) / np.linalg.norm(direction)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def dehomogenize(vector):
|
|
64
|
+
""""Divides column vector by last element and returns all but last element.
|
|
65
|
+
"""
|
|
66
|
+
vector = cvec(vector)
|
|
67
|
+
return vector[0:-1] / vector[-1]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def hessianNormalForm(vector):
|
|
71
|
+
"""Compute the Hessian normal form of a 2D line or 3D plane given as
|
|
72
|
+
homogeneous three-, respectively four-vector."""
|
|
73
|
+
return vector / np.linalg.norm(vector[0:-1])
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def RP2Point(x, y, w=1):
|
|
77
|
+
"""Functions to improve code readability"""
|
|
78
|
+
return cvec([x,y,w])
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def RP2Line(l0, l1, l2):
|
|
82
|
+
"""Functions to improve code readability"""
|
|
83
|
+
return cvec([l0, l1, l2])
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def RP3Point(x, y, z, w=1):
|
|
87
|
+
"""Functions to improve code readability"""
|
|
88
|
+
return cvec([x,y,z,w])
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def RP3Plane(p0, p1, p2, p3):
|
|
92
|
+
"""Functions to improve code readability"""
|
|
93
|
+
return cvec([p0, p1, p2, p3])
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def join2(a,b):
|
|
97
|
+
"""Compute joining line from two homogeneous 2D points."""
|
|
98
|
+
return cvec(np.cross(cvec(a)[:,0], cvec(b)[:,0]))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def meet2(l,m):
|
|
102
|
+
"""Compute intersection from homogeneous coordinates of two 2D lines."""
|
|
103
|
+
return cvec(np.cross(cvec(l)[:,0], cvec(m)[:,0]))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def join3(A, B, C):
|
|
107
|
+
"""
|
|
108
|
+
Compute the common plane passing through three points or the point of
|
|
109
|
+
intersection of three planes.
|
|
110
|
+
|
|
111
|
+
Note: for computing the joining line from two 3D points, please use
|
|
112
|
+
L = pluecker.join_points(A,B)
|
|
113
|
+
|
|
114
|
+
Parameters:
|
|
115
|
+
A, B, C: 1D arrays or column vectors representing three points
|
|
116
|
+
in homogeneous coordinates.
|
|
117
|
+
"""
|
|
118
|
+
ABC = np.vstack((A, B, C)) if A.ndim == 1 else np.hstack((A, B, C))
|
|
119
|
+
P = np.array([
|
|
120
|
+
+np.linalg.det(ABC[[1, 2, 3], :]),
|
|
121
|
+
-np.linalg.det(ABC[[0, 2, 3], :]),
|
|
122
|
+
+np.linalg.det(ABC[[0, 1, 3], :]),
|
|
123
|
+
-np.linalg.det(ABC[[0, 1, 2], :]),
|
|
124
|
+
])
|
|
125
|
+
return cvec(P)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def meet3(P, Q, R):
|
|
129
|
+
"""
|
|
130
|
+
Compute the point of intersection for three planes that meet at one point.
|
|
131
|
+
|
|
132
|
+
Note: for computing the line of intersection from two 3D planes, please use
|
|
133
|
+
L = pluecker.meet_planes(P,Q)
|
|
134
|
+
|
|
135
|
+
Parameters:
|
|
136
|
+
P, Q, R: 1D arrays or column vectors representing planes in homogeneous
|
|
137
|
+
coordinates.
|
|
138
|
+
"""
|
|
139
|
+
PQR = np.vstack((P, Q, R)) if P.ndim == 1 else np.hstack((P, Q, R))
|
|
140
|
+
X = np.array([
|
|
141
|
+
+np.linalg.det(PQR[[1, 2, 3], :]),
|
|
142
|
+
-np.linalg.det(PQR[[0, 2, 3], :]),
|
|
143
|
+
+np.linalg.det(PQR[[0, 1, 3], :]),
|
|
144
|
+
-np.linalg.det(PQR[[0, 1, 2], :]),
|
|
145
|
+
])
|
|
146
|
+
return cvec(X)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def KRt(K, R, t):
|
|
150
|
+
"""Compose projection matrix from intrinsic and extrinsic parameters."""
|
|
151
|
+
return K@np.column_stack((R,t))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def line2d_to_angle_intercept(l):
|
|
155
|
+
"""Convert homogeneous coordinates of a 2D line to ange and intercept.
|
|
156
|
+
As is the convention in mathematics, the angle is measured with respect
|
|
157
|
+
to the x-axis. alpha=t=0 corresponds to the line (0,1,0,0)."""
|
|
158
|
+
alpha = np.arctan2(l[1], l[0]) - np.pi * 0.5 # angle
|
|
159
|
+
alpha = alpha + 2.0 * np.pi if alpha < -np.pi else alpha
|
|
160
|
+
t = -l[2] / np.sqrt(l[0] * l[0] + l[1] * l[1]) # intercept
|
|
161
|
+
return np.array([alpha, t])
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def line2d_from_angle_intercept(alpha, t):
|
|
165
|
+
"""Convert ange and intercept to homogeneous coordinates of a 2D line.
|
|
166
|
+
As is the convention in mathematics, the angle is measured with respect
|
|
167
|
+
to the x-axis. alpha=t=0 corresponds to the line (0,1,0,0)."""
|
|
168
|
+
return RP2Line(
|
|
169
|
+
np.cos(alpha + np.pi * 0.5),
|
|
170
|
+
np.sin(alpha + np.pi * 0.5),
|
|
171
|
+
-t)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def intersectLineWithRect(l, n_x: float, n_y: float ):
|
|
175
|
+
"""Intersects line with a rect. Use this for drawing lines.
|
|
176
|
+
Arguments:
|
|
177
|
+
l a 2D line in homogeneous coordinates.
|
|
178
|
+
sequence of numbers or np.array.
|
|
179
|
+
Returns:
|
|
180
|
+
x1 y1 x2 y2 line entry (x1,y1) and exit points (x2,y2)."""
|
|
181
|
+
|
|
182
|
+
eps = 1e-10
|
|
183
|
+
l = cvec(l)
|
|
184
|
+
# Find intersections with image boundaries
|
|
185
|
+
intersection = [
|
|
186
|
+
meet2(l,cvec([1,0,0]))[:,0],
|
|
187
|
+
meet2(l,cvec([-1,0,n_x-1]))[:,0],
|
|
188
|
+
meet2(l,cvec([0,1,0]))[:,0],
|
|
189
|
+
meet2(l,cvec([0,-1,n_y-1]))[:,0]
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
# Find intersections which are in bounds
|
|
193
|
+
pto, pfrom = [-1,-1,-1], [-1,-1,-1]
|
|
194
|
+
for i in range(4):
|
|
195
|
+
if abs(intersection[i][2])>eps:
|
|
196
|
+
intersection[i] = intersection[i] / intersection[i][2]
|
|
197
|
+
if intersection[i][0]<=n_x+eps and intersection[i][1]<=n_y+eps and \
|
|
198
|
+
intersection[i][0]+eps>=0 and intersection[i][1]+eps>=0:
|
|
199
|
+
if pfrom[0]<0:
|
|
200
|
+
pfrom = [intersection[i][0],intersection[i][1],0]
|
|
201
|
+
elif pto[0]<0:
|
|
202
|
+
pto = [intersection[i][0],intersection[i][1],0]
|
|
203
|
+
else:
|
|
204
|
+
# This may happen if a corner coincides with the line.
|
|
205
|
+
# Then, we have to use two intersections, which are far apart to get the line.
|
|
206
|
+
pto2=cvec([intersection[i][0],intersection[i][1],0])
|
|
207
|
+
if np.linalg.norm(np.array(pfrom)-np.array(pto)) < np.linalg.norm(np.array(pfrom)-np.array(pto2)):
|
|
208
|
+
pto=pto2
|
|
209
|
+
|
|
210
|
+
return (pfrom[0],pfrom[1],pto[0],pto[1])
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def parse_ompl(ompl):
|
|
214
|
+
"""Load a text file with one matrix per line (*.ompl).
|
|
215
|
+
Lines that so not start with '#' character (for comments) contain a colon
|
|
216
|
+
seperated list of floating point values, each representing a matrix row.
|
|
217
|
+
Lines starting with '#>' may contain additional meta info, such as
|
|
218
|
+
#> spacing="0.1" detector_size_px="800 600"
|
|
219
|
+
"""
|
|
220
|
+
comments = []
|
|
221
|
+
meta = dict()
|
|
222
|
+
matrices = []
|
|
223
|
+
for index, line in enumerate(ompl.split('\n')):
|
|
224
|
+
if line.startswith('#'):
|
|
225
|
+
if line.startswith('#>'):
|
|
226
|
+
# Expected string: key1:"value1" key2:"value2" ...
|
|
227
|
+
kvps = [assignment.split('=') for assignment in shlex.split(line[2:])]
|
|
228
|
+
meta.update({kvp[0]: kvp[1] for kvp in kvps})
|
|
229
|
+
else:
|
|
230
|
+
comments += (index, line[2:])
|
|
231
|
+
else:
|
|
232
|
+
if len(line) < 2:
|
|
233
|
+
continue
|
|
234
|
+
line = line.replace('[', '')
|
|
235
|
+
line = line.replace(']', '')
|
|
236
|
+
matrices += [[[float(value) for value in row.split()] for row in line.split(';')]]
|
|
237
|
+
return matrices, meta, comments
|
|
238
|
+
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ProjectiveGeometry23
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Projective geometry in 2D and 3D with homogeneous and Plücker coordinates, projection matrices, and visualization.
|
|
5
|
+
Home-page: https://github.com/aaichert/ProjectiveGeometry23
|
|
6
|
+
Author: Andre Aichert
|
|
7
|
+
Author-email: aaichert@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.7
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: numpy
|
|
16
|
+
Requires-Dist: scipy
|
|
17
|
+
Provides-Extra: svg
|
|
18
|
+
Requires-Dist: svg_snip; extra == "svg"
|
|
19
|
+
Dynamic: author
|
|
20
|
+
Dynamic: author-email
|
|
21
|
+
Dynamic: classifier
|
|
22
|
+
Dynamic: description
|
|
23
|
+
Dynamic: description-content-type
|
|
24
|
+
Dynamic: home-page
|
|
25
|
+
Dynamic: license
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
Dynamic: provides-extra
|
|
28
|
+
Dynamic: requires-dist
|
|
29
|
+
Dynamic: requires-python
|
|
30
|
+
Dynamic: summary
|
|
31
|
+
|
|
32
|
+
# ProjectiveGeometry23
|
|
33
|
+
## Projective Geometry of Two- and Three-space
|
|
34
|
+
|
|
35
|
+
`ProjectiveGeometry23` is a collection of numpy-based utilities for projective geometry of real two- and three-space, including homogeneous coordinates of point, lines and planes, Plücker coordinates and projection matrices.
|
|
36
|
+
|
|
37
|
+
The package has been converted from an existing C++ implementation [LibProjectiveGeometry](https://github.com/aaichert/LibProjectiveGeometry), which is well-tested and obviously faster.
|
|
38
|
+
|
|
39
|
+
The main use of this code is for the visualization or X-Ray source-detector geometries but applied generally to geometric computer vision problems.
|
|
40
|
+
|
|
41
|
+
Features:
|
|
42
|
+
- Computing with points, lines and planes
|
|
43
|
+
- Plücker coordinates, Plücker matrices
|
|
44
|
+
- Projection matrices, intrinsic and extrinsic parameters
|
|
45
|
+
- Decomposition of projection matrices, backprojection
|
|
46
|
+
- Visualization of X-Ray source-detector geometries
|
|
47
|
+
- (WIP) estimation based on direct linear transform, X-ray calibraion
|
|
48
|
+
|
|
49
|
+
General recommendation for better readibility of outputs:
|
|
50
|
+
|
|
51
|
+
```py
|
|
52
|
+
from rich import print
|
|
53
|
+
np.set_printoptions(suppress=True)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Example of an interactive visualization, using the optional `svg_snip` package:
|
|
57
|
+
|
|
58
|
+
```py
|
|
59
|
+
from svg_snip.Jupyter import CanvasWithOverlay
|
|
60
|
+
from svg_snip.Composer import Composer
|
|
61
|
+
import svg_snip.Elements as e2d
|
|
62
|
+
import svg_snip.Elements3D as e3d
|
|
63
|
+
|
|
64
|
+
from ProjectiveGeometry23.homography import rotation_x, rotation_z, scale
|
|
65
|
+
from ProjectiveGeometry23.svg_utils import svg_source_detector, svg_world_geometry
|
|
66
|
+
|
|
67
|
+
vis = CanvasWithOverlay(int(target.image_size[0]), int(target.image_size[1]))
|
|
68
|
+
|
|
69
|
+
# World transformation
|
|
70
|
+
ax, az = 0.5, 0.5
|
|
71
|
+
s = 0.2
|
|
72
|
+
|
|
73
|
+
def handle_draw(vis):
|
|
74
|
+
global ax
|
|
75
|
+
global az
|
|
76
|
+
x,y = vis.mouse_state.pos()
|
|
77
|
+
svg = Composer((vis.w, vis.h))
|
|
78
|
+
|
|
79
|
+
svg.add(svg_world_geometry)
|
|
80
|
+
svg.add(svg_source_detector, projection=target,
|
|
81
|
+
draw_on_detector=svg_world_geometry,
|
|
82
|
+
label_source='C0', label_detector='I0(u,v)')
|
|
83
|
+
|
|
84
|
+
svg.add(e2d.star, x=x, y=y, size=8,
|
|
85
|
+
fill="red" if vis.mouse_state.clicked else "blue")
|
|
86
|
+
|
|
87
|
+
svg.add(e2d.text, x=10, y=20, content=f'ax={ax:.3} az={az:.3}')
|
|
88
|
+
|
|
89
|
+
T = scale(s) @ rotation_x(ax) @ rotation_z(az)
|
|
90
|
+
raw_svg_code = svg.render(P=target.P@T)
|
|
91
|
+
vis.html_overlay.value = raw_svg_code
|
|
92
|
+
|
|
93
|
+
if vis.mouse_state.clicked:
|
|
94
|
+
az += vis.mouse_state.dx * 0.01
|
|
95
|
+
ax += vis.mouse_state.dy * 0.01
|
|
96
|
+
|
|
97
|
+
vis.handle_draw = handle_draw
|
|
98
|
+
|
|
99
|
+
vis.display()
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+

|
|
104
|
+
|
|
105
|
+
## Installation
|
|
106
|
+
|
|
107
|
+
### Using pip
|
|
108
|
+
You can install `ProjectiveGeometry23` using pip:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
pip install ProjectiveGeometry23
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
### Using `setup.py`
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
git clone https://github.com/aaichert/ProjectiveGeometry23
|
|
119
|
+
cd ProjectiveGeometry23
|
|
120
|
+
python setup.py install
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Testing and Publication on PyPy
|
|
124
|
+
|
|
125
|
+
Two useful code snippets
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
python -m unittest discover tests
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
python setup.py sdist bdist_wheel
|
|
133
|
+
pip install twine
|
|
134
|
+
twine upload dist/*
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### References
|
|
138
|
+
|
|
139
|
+
1. LibProjectiveGeometry (c++). GitHub https://github.com/aaichert/EpipolarConsistency/tree/master/code/LibProjectiveGeometry
|
|
140
|
+
2. Hartley, Richard, and Andrew Zisserman. Multiple view geometry in computer vision. Cambridge university press, 2003. https://www.robots.ox.ac.uk/~vgg/hzbook/
|
|
141
|
+
3. Coxeter, Harold Scott Macdonald. Projective geometry. Springer Science & Business Media, 2003.
|
|
142
|
+
4. Stolfi, Jorge. "Oriented projective geometry." Proceedings of the third annual symposium on Computational geometry. 1987.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
ProjectiveGeometry23/__init__.py,sha256=nEYkor_6Nf-o7BiMinE-QURrXcIQtJbsyd613uX6Pfs,57
|
|
2
|
+
ProjectiveGeometry23/central_projection.py,sha256=yh12BpCTF9SK2bUH14lP4zWhbr-G_luCOcVY10SGC5M,9718
|
|
3
|
+
ProjectiveGeometry23/estimation.py,sha256=3-xccEYAG4J9ojlxeHx2rEBfFKIwcFhMsqaCmi-D5mw,2286
|
|
4
|
+
ProjectiveGeometry23/homography.py,sha256=P0B96CaeOccoWRDHHShFLsIea0wSdkn-1XHvLmtisEs,2896
|
|
5
|
+
ProjectiveGeometry23/pluecker.py,sha256=ayanNyDbwxwCGdC2bEXAGq3kjTQRXTtgticK7s9zABY,6586
|
|
6
|
+
ProjectiveGeometry23/source_detector_geometry.py,sha256=MBE45j9uVqk8rW-PPABKNJRT_QthfZX2yuvcz6_zIxg,5130
|
|
7
|
+
ProjectiveGeometry23/svg_utils.py,sha256=-spwByvZKlSoTnH1cX5VpZ8AhI0jAVHB8Mg1HUm5GD0,4988
|
|
8
|
+
ProjectiveGeometry23/utils.py,sha256=p6ZowiTDr0yvo_TTCF8mxtt3e-dFoEDw53pLYJ__hiY,8108
|
|
9
|
+
projectivegeometry23-0.1.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
10
|
+
projectivegeometry23-0.1.0.dist-info/METADATA,sha256=KvhGzS8uoNT_Qez2Xxz9rCOiHUu2kqJHDdyvKNTvDBM,4394
|
|
11
|
+
projectivegeometry23-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
12
|
+
projectivegeometry23-0.1.0.dist-info/top_level.txt,sha256=WwClYIS9b_oVIm0Xkr6hB-A8-W8UgFOLW3yQQM1mx2g,21
|
|
13
|
+
projectivegeometry23-0.1.0.dist-info/RECORD,,
|