I deleted my previous answer because I was glued in some convoluted calculations and the question was answered with a nice on-the-shelf software answer. However since then I continued to look into this problem just for the sport of it! I hope people will have as much fun as I had doing this.
The origin where the camera would be if the world was perfect will be the perfect origin and the position it should be will give us the perfect reference frame.
What I'll call the pointing vectors would be unit vectors pointing from the camera origin (somewhere at the back of the camera) to what I'll call the targets (for instance crosses on the table) of which we'll know the positions in the perfect reference frame.
The $u_i$ vectors are the pointing vectors estimates computed with the camera position estimate ($u_i = \frac{target\_i\_position-camera\_position}{\vert\vert target\_i\_position-camera\_position \vert\vert}$) in the perfect reference frame and the $v_i$ are the measured pointing vectors in the reference frame of the camera.
Once you have a $u_i$ computed with the position, you only have to project (or rotate) it to the camera's frame with a rotation matrix $R$ to get a prediction of what you actually measured. It leads to the following least squares problem which is called Wahba's problem :
$$
J(attitude, position) = \sum_{i=1}^{n}\vert\vert v_i-R(attitude)\times u_i(position) \vert\vert²
$$
Usually this problem is used to determine the attitude (i.e orientation) of a satellite by sensing stars that are so remote that the kind of position variation you might see in an orbit wont affect the pointing vectors. Fortunately for us the targets here are not millions of light years away and their position will affect our loss function.
The main problem now before handing it to your favorite optimizer is to understand a way to enforce the fact that the rotation matrix is a rotation matrix. The way I found to do this is to represent the attitude with quaternions. To ensure that a quaternion $q$ represents an attitude you just have to enforce $\vert\vert q \vert\vert^{2} = 1$. I tried the nonlinear contraint before I figured out that I could let the quaternion roam free in $\mathbb{R}^{4}$ and just normalize $q$ just before performing the rotation, leading to a unconstrained optimization problem.
Here is the code I used if you want to play around with it :
import numpy as np
from scipy.spatial.transform import Rotation
from scipy.optimize import minimize
# takes vectors input of shape 3 x n
def rotate(quat, vectors):
r = Rotation.from_quat(quat.flatten())
return np.atleast_2d(r.apply(vectors.T)).T
def normalize_columns(mat):
return np.divide(mat, np.linalg.norm(mat, axis=0))
def compute_pointing_vectors(quat, pos, target_pos):
return normalize_columns(rotate(quat, pos - target_pos))
def problem_set_up(nb_targets, position_magnitude, noise_magnitude, seed=4242):
np.random.seed(seed)
target_pos = position_magnitude * 2 * (np.random.rand(3, nb_targets) - 0.5)
true_pos = 2 * (position_magnitude / 10) * (np.random.rand(3, 1) - 0.5)
true_quat = normalize_columns(np.random.rand(4, 1) - 0.5)
if true_quat[0][0] < 0:
true_quat = -true_quat
true_pointing_vectors = compute_pointing_vectors(true_quat, true_pos, target_pos)
measured_pointing_vectors = true_pointing_vectors + np.random.normal(
0, noise_magnitude, true_pointing_vectors.shape
)
return target_pos, measured_pointing_vectors, true_quat, true_pos
def soft_wahba_loss(X, target_pos, measured_pointing_vectors):
alpha_1, alpha_2, alpha_3, alpha_4, x, y, z = X
quat = normalize_columns(np.array([[alpha_1], [alpha_2], [alpha_3], [alpha_4]]))
pos = np.array([[x], [y], [z]])
pointing_vectors_estimates = compute_pointing_vectors(quat, pos, target_pos)
return (np.square(pointing_vectors_estimates - measured_pointing_vectors)).mean()
def extract_result(res):
pos = np.atleast_2d(res.x[4:]).T
quat = np.atleast_2d(normalize_columns(res.x[0:4])).T
if quat[0][0] < 0:
quat = -quat
return quat, pos
nb_targets = 5
position_magnitude = 100
noise_magnitude = 0 # 1e-3
seed = np.random.randint(0, 1000000)
target_pos, measured_pointing_vectors, true_quat, true_pos = problem_set_up(
nb_targets, position_magnitude, noise_magnitude, seed=seed
)
X0 = np.array([0, 0, 0, 1, 100, 100, 100])
fun = lambda X: soft_wahba_loss(X, target_pos, measured_pointing_vectors)
options = {"gtol": 1e-15}
res = minimize(fun, X0, method="L-BFGS-B", options=options)
quat_estimate, pos_estimate = extract_result(res)
print("with the seed : " + str(seed))
print("the true quaternion is :")
print(true_quat)
print("the quaternion estimate is :")
print(quat_estimate)
print(
"the l2 distance between the two is :"
+ str(np.linalg.norm(true_quat - quat_estimate))
)
print("the true position is : ")
print(true_pos)
print("the position estimate is :")
print(pos_estimate)
print(
"the l2 distance between the two is : "
+ str(np.linalg.norm(true_pos - pos_estimate))
)
The results are interesting. As expected, you need to have at least three targets to extract position and attitude. The attitude is determined relatively precisely but the position is not so accurately computed. In fact the number of targets (above 4 or 5) and the noise level has very little effect on the position determination, which is quite funny.
I believe it is due to the fact that the error function is relatively insensitive to the position, therefore when you're close enough the gradient vanishes and the error stagnates, leading to algorithm termination. You'll have a similar phenomenon if your initial guess is away from the real camera position (like [1000, 1000, 1000]), the gradient w.r.t the position is barely existing (which is not too crazy here if you think about it), hence the algorithm stops immediately. I guess that would be why Wahba's loss is an attitude determination loss and not a position determination loss.
I hope you enjoyed!