import numpy as np
import matplotlib.pyplot as plt
from scipy.spatial.transform import Rotation
rng = np.random.default_rng(5)
# ---- helper functions ----
def skew(v):
return np.array([[ 0., -v[2], v[1]],
[ v[2], 0., -v[0]],
[-v[1], v[0], 0. ]]) # shape (3, 3)
def triangulate_dlt(P1, P2, x1, x2):
A = np.array([
x1[0]*P1[2] - P1[0],
x1[1]*P1[2] - P1[1],
x2[0]*P2[2] - P2[0],
x2[1]*P2[2] - P2[1],
]) # shape (4, 4)
_, _, Vt = np.linalg.svd(A)
X_h = Vt[-1] # shape (4,)
return X_h[:3] / X_h[3] # shape (3,)
# ---- ground-truth pose ----
R_true = Rotation.from_euler('y', 10., degrees=True).as_matrix() # shape (3, 3)
t_true = np.array([0.5, 0.0, 0.0]) # shape (3,)
K = np.array([[500., 0., 320.],
[ 0., 500., 240.],
[ 0., 0., 1.]]) # shape (3, 3)
E_true = skew(t_true) @ R_true # shape (3, 3)
P1 = K @ np.hstack([np.eye(3), np.zeros((3, 1))]) # shape (3, 4)
P2 = K @ np.hstack([R_true, t_true.reshape(3, 1)]) # shape (3, 4)
# ---- synthetic correspondences with noise ----
n = 30
X_world = rng.uniform(-0.4, 0.4, (n, 3)) + np.array([0., 0., 3.0]) # shape (30, 3)
def project(P, X):
lam_x = P @ np.append(X, 1.)
return lam_x[:2] / lam_x[2] # shape (2,)
x1s = np.array([project(P1, X) for X in X_world]) + rng.normal(0., 0.3, (n, 2)) # shape (30, 2)
x2s = np.array([project(P2, X) for X in X_world]) + rng.normal(0., 0.3, (n, 2)) # shape (30, 2)
# normalized image coords
y1s = np.linalg.solve(K, np.hstack([x1s, np.ones((n, 1))]).T).T # shape (30, 3)
y2s = np.linalg.solve(K, np.hstack([x2s, np.ones((n, 1))]).T).T # shape (30, 3)
# ---- decompose E ----
U, s, Vt = np.linalg.svd(E_true) # U shape (3,3), s shape (3,), Vt shape (3,3)
# Fix determinants
if np.linalg.det(U) < 0: U = -U
if np.linalg.det(Vt) < 0: Vt = -Vt
W = np.array([[0., -1., 0.],
[1., 0., 0.],
[0., 0., 1.]]) # shape (3, 3)
u3 = U[:, 2] # shape (3,)
R1c = U @ W @ Vt; t1c = +u3
R2c = U @ W @ Vt; t2c = -u3
R3c = U @ W.T @ Vt; t3c = +u3
R4c = U @ W.T @ Vt; t4c = -u3
candidates = [(R1c, t1c), (R2c, t2c), (R3c, t3c), (R4c, t4c)]
# ---- cheirality test ----
scores = []
for R_c, t_c in candidates:
P1c = np.hstack([np.eye(3), np.zeros((3, 1))]) # shape (3, 4) -- unnormalized OK
P2c = np.hstack([R_c, t_c.reshape(3, 1)]) # shape (3, 4)
count = 0
for i in range(n):
X_tri = triangulate_dlt(P1c, P2c, y1s[i], y2s[i]) # shape (3,)
in_front_1 = X_tri[2] > 0
X_cam2 = R_c @ X_tri + t_c # shape (3,)
in_front_2 = X_cam2[2] > 0
if in_front_1 and in_front_2:
count += 1
scores.append(count)
best_idx = int(np.argmax(scores))
R_best, t_best = candidates[best_idx]
# ---- visualize camera positions top-down ----
fig, ax = plt.subplots(figsize=(9, 7))
ax.set_aspect('equal')
ax.set_xlabel('x (world)'); ax.set_ylabel('z (world)')
ax.set_title('Recovered camera candidates (top-down view)')
ax.grid(alpha=0.2)
def draw_camera(ax, R, t, color, label, alpha=1.0):
# camera center in world: C = -R^T t
C = -R.T @ t # shape (3,)
# optical axis direction (z column of R in camera coords = R^T[:,2] in world)
zdir = R.T[:, 2] # shape (3,)
ax.scatter(C[0], C[2], color=color, s=80, alpha=alpha, zorder=5)
ax.annotate(label, (C[0]+0.03, C[2]+0.03), fontsize=8, color=color, alpha=alpha)
ax.annotate('', xy=(C[0]+0.25*zdir[0], C[2]+0.25*zdir[2]),
xytext=(C[0], C[2]),
arrowprops=dict(arrowstyle='->', color=color, lw=1.5, alpha=alpha))
# camera 1 at origin
ax.scatter(0, 0, color='black', s=100, zorder=5)
ax.annotate('Cam 1', (0.03, 0.03), fontsize=8)
ax.annotate('', xy=(0, 0.3), xytext=(0, 0),
arrowprops=dict(arrowstyle='->', color='black', lw=1.5))
cand_colors = ['#4a90d9', 'tomato', '#2ecc71', 'goldenrod']
for i, (R_c, t_c) in enumerate(candidates):
is_best = (i == best_idx)
color = cand_colors[i]
draw_camera(ax, R_c, t_c, color,
f'Cand {i+1} ({scores[i]}/{n})',
alpha=1.0 if is_best else 0.35)
# show a few triangulated points (best candidate)
R_b, t_b = candidates[best_idx]
P2b = np.hstack([R_b, t_b.reshape(3, 1)])
for i in range(min(12, n)):
X_tri = triangulate_dlt(np.eye(3, 4), P2b, y1s[i], y2s[i])
ax.scatter(X_tri[0], X_tri[2], color='#4a90d9', s=15, alpha=0.5)
plt.tight_layout()
plt.show()
print(f"Cheirality scores (n={n} pts): {scores}")
print(f"Best candidate: {best_idx+1} (R error: {np.degrees(np.arccos(np.clip((np.trace(R_best @ R_true.T)-1)/2,-1,1))):.2f} deg)")