Merge pull request #1888 from borglab/feature/wrapF

F wrapper and bugfix
release/4.3a0
Frank Dellaert 2024-10-31 12:43:50 -07:00 committed by GitHub
commit 67495babae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 486 additions and 69 deletions

View File

@ -56,7 +56,7 @@ std::vector<Point3> createPoints() {
/**
* Create a set of ground-truth poses
* Default values give a circular trajectory, radius 30 at pi/4 intervals,
* Default values give a circular trajectory, radius 30 at pi/4 intervals,
* always facing the circle center
*/
std::vector<Pose3> createPoses(

View File

@ -38,7 +38,7 @@ using namespace gtsam;
/* ************************************************************************* */
int main(int argc, char* argv[]) {
// Define the camera calibration parameters
Cal3_S2 K(50.0, 50.0, 0.0, 50.0, 50.0);
Cal3_S2 cal(50.0, 50.0, 0.0, 50.0, 50.0);
// Create the set of 8 ground-truth landmarks
vector<Point3> points = createPoints();
@ -47,13 +47,13 @@ int main(int argc, char* argv[]) {
vector<Pose3> poses = posesOnCircle(4, 30);
// Calculate ground truth fundamental matrices, 1 and 2 poses apart
auto F1 = FundamentalMatrix(K, poses[0].between(poses[1]), K);
auto F2 = FundamentalMatrix(K, poses[0].between(poses[2]), K);
auto F1 = FundamentalMatrix(cal.K(), poses[0].between(poses[1]), cal.K());
auto F2 = FundamentalMatrix(cal.K(), poses[0].between(poses[2]), cal.K());
// Simulate measurements from each camera pose
std::array<std::array<Point2, 8>, 4> p;
for (size_t i = 0; i < 4; ++i) {
PinholeCamera<Cal3_S2> camera(poses[i], K);
PinholeCamera<Cal3_S2> camera(poses[i], cal);
for (size_t j = 0; j < 8; ++j) {
p[i][j] = camera.project(points[j]);
}

View File

@ -2,10 +2,12 @@
* @file FundamentalMatrix.cpp
* @brief FundamentalMatrix classes
* @author Frank Dellaert
* @date Oct 23, 2024
* @date October 2024
*/
#include <gtsam/base/Matrix.h>
#include <gtsam/geometry/FundamentalMatrix.h>
#include <gtsam/geometry/Point2.h>
namespace gtsam {
@ -26,6 +28,11 @@ Point2 EpipolarTransfer(const Matrix3& Fca, const Point2& pa, //
}
//*************************************************************************
FundamentalMatrix::FundamentalMatrix(const Matrix3& U, double s,
const Matrix3& V) {
initialize(U, s, V);
}
FundamentalMatrix::FundamentalMatrix(const Matrix3& F) {
// Perform SVD
Eigen::JacobiSVD<Matrix3> svd(F, Eigen::ComputeFullU | Eigen::ComputeFullV);
@ -47,28 +54,24 @@ FundamentalMatrix::FundamentalMatrix(const Matrix3& F) {
"The input matrix does not represent a valid fundamental matrix.");
}
// Ensure the second singular value is recorded as s
s_ = singularValues(1);
initialize(U, singularValues(1), V);
}
// Check if U is a reflection
if (U.determinant() < 0) {
U = -U;
s_ = -s_; // Change sign of scalar if U is a reflection
}
void FundamentalMatrix::initialize(Matrix3 U, double s, Matrix3 V) {
// Check if U is a reflection and flip third column if so
if (U.determinant() < 0) U.col(2) *= -1;
// Check if V is a reflection
if (V.determinant() < 0) {
V = -V;
s_ = -s_; // Change sign of scalar if U is a reflection
}
// Same check for V
if (V.determinant() < 0) V.col(2) *= -1;
// Assign the rotations
U_ = Rot3(U);
s_ = s;
V_ = Rot3(V);
}
Matrix3 FundamentalMatrix::matrix() const {
return U_.matrix() * Vector3(1, s_, 0).asDiagonal() * V_.transpose().matrix();
return U_.matrix() * Vector3(1.0, s_, 0).asDiagonal() *
V_.transpose().matrix();
}
void FundamentalMatrix::print(const std::string& s) const {
@ -90,9 +93,9 @@ Vector FundamentalMatrix::localCoordinates(const FundamentalMatrix& F) const {
}
FundamentalMatrix FundamentalMatrix::retract(const Vector& delta) const {
Rot3 newU = U_.retract(delta.head<3>());
double newS = s_ + delta(3); // Update scalar
Rot3 newV = V_.retract(delta.tail<3>());
const Rot3 newU = U_.retract(delta.head<3>());
const double newS = s_ + delta(3);
const Rot3 newV = V_.retract(delta.tail<3>());
return FundamentalMatrix(newU, newS, newV);
}

View File

@ -2,7 +2,7 @@
* @file FundamentalMatrix.h
* @brief FundamentalMatrix classes
* @author Frank Dellaert
* @date Oct 23, 2024
* @date October 2024
*/
#pragma once
@ -15,11 +15,15 @@ namespace gtsam {
/**
* @class FundamentalMatrix
* @brief Represents a general fundamental matrix.
* @brief Represents a fundamental matrix in computer vision, which encodes the
* epipolar geometry between two views.
*
* This class represents a general fundamental matrix, which is a 3x3 matrix
* that describes the relationship between two images. It is parameterized by a
* left rotation U, a scalar s, and a right rotation V.
* The FundamentalMatrix class encapsulates the fundamental matrix, which
* relates corresponding points in stereo images. It is parameterized by two
* rotation matrices (U and V) and a scalar parameter (s).
* Using these values, the fundamental matrix is represented as
*
* F = U * diag(1, s, 0) * V^T
*/
class GTSAM_EXPORT FundamentalMatrix {
private:
@ -34,15 +38,10 @@ class GTSAM_EXPORT FundamentalMatrix {
/**
* @brief Construct from U, V, and scalar s
*
* Initializes the FundamentalMatrix with the given left rotation U,
* scalar s, and right rotation V.
*
* @param U Left rotation matrix
* @param s Scalar parameter for the fundamental matrix
* @param V Right rotation matrix
* Initializes the FundamentalMatrix From the SVD representation
* U*diag(1,s,0)*V^T. It will internally convert to using SO(3).
*/
FundamentalMatrix(const Rot3& U, double s, const Rot3& V)
: U_(U), s_(s), V_(V) {}
FundamentalMatrix(const Matrix3& U, double s, const Matrix3& V);
/**
* @brief Construct from a 3x3 matrix using SVD
@ -54,22 +53,35 @@ class GTSAM_EXPORT FundamentalMatrix {
*/
FundamentalMatrix(const Matrix3& F);
/**
* @brief Construct from essential matrix and calibration matrices
*
* Initializes the FundamentalMatrix from the given essential matrix E
* and calibration matrices Ka and Kb, using
* F = Ka^(-T) * E * Kb^(-1)
* and then calls constructor that decomposes F via SVD.
*
* @param E Essential matrix
* @param Ka Calibration matrix for the left camera
* @param Kb Calibration matrix for the right camera
*/
FundamentalMatrix(const Matrix3& Ka, const EssentialMatrix& E,
const Matrix3& Kb)
: FundamentalMatrix(Ka.transpose().inverse() * E.matrix() *
Kb.inverse()) {}
/**
* @brief Construct from calibration matrices Ka, Kb, and pose aPb
*
* Initializes the FundamentalMatrix from the given calibration
* matrices Ka and Kb, and the pose aPb.
*
* @tparam CAL Calibration type, expected to have a matrix() method
* @param Ka Calibration matrix for the left camera
* @param aPb Pose from the left to the right camera
* @param Kb Calibration matrix for the right camera
*/
template <typename CAL>
FundamentalMatrix(const CAL& Ka, const Pose3& aPb, const CAL& Kb)
: FundamentalMatrix(Ka.K().transpose().inverse() *
EssentialMatrix::FromPose3(aPb).matrix() *
Kb.K().inverse()) {}
FundamentalMatrix(const Matrix3& Ka, const Pose3& aPb, const Matrix3& Kb)
: FundamentalMatrix(Ka, EssentialMatrix::FromPose3(aPb), Kb) {}
/// Return the fundamental matrix representation
Matrix3 matrix() const;
@ -96,6 +108,13 @@ class GTSAM_EXPORT FundamentalMatrix {
/// Retract the given vector to get a new FundamentalMatrix
FundamentalMatrix retract(const Vector& delta) const;
/// @}
private:
/// Private constructor for internal use
FundamentalMatrix(const Rot3& U, double s, const Rot3& V)
: U_(U), s_(s), V_(V) {}
/// Initialize SO(3) matrices from general O(3) matrices
void initialize(Matrix3 U, double s, Matrix3 V);
};
/**

View File

@ -578,6 +578,8 @@ class Unit3 {
// Standard Constructors
Unit3();
Unit3(const gtsam::Point3& pose);
Unit3(double x, double y, double z);
Unit3(const gtsam::Point2& p, double f);
// Testable
void print(string s = "") const;
@ -620,10 +622,10 @@ class EssentialMatrix {
EssentialMatrix(const gtsam::Rot3& aRb, const gtsam::Unit3& aTb);
// Constructors from Pose3
gtsam::EssentialMatrix FromPose3(const gtsam::Pose3& _1P2_);
static gtsam::EssentialMatrix FromPose3(const gtsam::Pose3& _1P2_);
gtsam::EssentialMatrix FromPose3(const gtsam::Pose3& _1P2_,
Eigen::Ref<Eigen::MatrixXd> H);
static gtsam::EssentialMatrix FromPose3(const gtsam::Pose3& _1P2_,
Eigen::Ref<Eigen::MatrixXd> H);
// Testable
void print(string s = "") const;
@ -903,6 +905,59 @@ class Cal3Bundler {
void serialize() const;
};
#include <gtsam/geometry/FundamentalMatrix.h>
// FundamentalMatrix class
class FundamentalMatrix {
// Constructors
FundamentalMatrix();
FundamentalMatrix(const gtsam::Matrix3& U, double s, const gtsam::Matrix3& V);
FundamentalMatrix(const gtsam::Matrix3& F);
// Overloaded constructors for specific calibration types
FundamentalMatrix(const gtsam::Matrix3& Ka, const gtsam::EssentialMatrix& E,
const gtsam::Matrix3& Kb);
FundamentalMatrix(const gtsam::Matrix3& Ka, const gtsam::Pose3& aPb,
const gtsam::Matrix3& Kb);
// Methods
gtsam::Matrix3 matrix() const;
// Testable
void print(const std::string& s = "") const;
bool equals(const gtsam::FundamentalMatrix& other, double tol = 1e-9) const;
// Manifold
static size_t Dim();
size_t dim() const;
gtsam::Vector localCoordinates(const gtsam::FundamentalMatrix& F) const;
gtsam::FundamentalMatrix retract(const gtsam::Vector& delta) const;
};
// SimpleFundamentalMatrix class
class SimpleFundamentalMatrix {
// Constructors
SimpleFundamentalMatrix();
SimpleFundamentalMatrix(const gtsam::EssentialMatrix& E, double fa, double fb,
const gtsam::Point2& ca, const gtsam::Point2& cb);
// Methods
gtsam::Matrix3 matrix() const;
// Testable
void print(const std::string& s = "") const;
bool equals(const gtsam::SimpleFundamentalMatrix& other, double tol = 1e-9) const;
// Manifold
static size_t Dim();
size_t dim() const;
gtsam::Vector localCoordinates(const gtsam::SimpleFundamentalMatrix& F) const;
gtsam::SimpleFundamentalMatrix retract(const gtsam::Vector& delta) const;
};
gtsam::Point2 EpipolarTransfer(const gtsam::Matrix3& Fca, const gtsam::Point2& pa,
const gtsam::Matrix3& Fcb, const gtsam::Point2& pb);
#include <gtsam/geometry/CalibratedCamera.h>
class CalibratedCamera {
// Standard Constructors and Named Constructors

View File

@ -6,12 +6,15 @@
*/
#include <CppUnitLite/TestHarness.h>
#include <gtsam/base/Matrix.h>
#include <gtsam/base/Testable.h>
#include <gtsam/geometry/FundamentalMatrix.h>
#include <gtsam/geometry/Rot3.h>
#include <gtsam/geometry/SimpleCamera.h>
#include <gtsam/geometry/Unit3.h>
#include <cmath>
using namespace std::placeholders;
using namespace std;
using namespace gtsam;
@ -24,21 +27,38 @@ GTSAM_CONCEPT_MANIFOLD_INST(FundamentalMatrix)
Rot3 trueU = Rot3::Yaw(M_PI_2);
Rot3 trueV = Rot3::Yaw(M_PI_4);
double trueS = 0.5;
FundamentalMatrix trueF(trueU, trueS, trueV);
FundamentalMatrix trueF(trueU.matrix(), trueS, trueV.matrix());
//*************************************************************************
TEST(FundamentalMatrix, localCoordinates) {
TEST(FundamentalMatrix, LocalCoordinates) {
Vector expected = Z_7x1; // Assuming 7 dimensions for U, V, and s
Vector actual = trueF.localCoordinates(trueF);
EXPECT(assert_equal(expected, actual, 1e-8));
}
//*************************************************************************
TEST(FundamentalMatrix, retract) {
TEST(FundamentalMatrix, Retract) {
FundamentalMatrix actual = trueF.retract(Z_7x1);
EXPECT(assert_equal(trueF, actual));
}
//*************************************************************************
// Test conversion from F matrices, including non-rotations
TEST(FundamentalMatrix, Conversion) {
Matrix3 U = trueU.matrix();
Matrix3 V = trueV.matrix();
std::vector<FundamentalMatrix> Fs = {
FundamentalMatrix(U, trueS, V), FundamentalMatrix(U, trueS, -V),
FundamentalMatrix(-U, trueS, V), FundamentalMatrix(-U, trueS, -V)};
for (const auto& trueF : Fs) {
const Matrix3 F = trueF.matrix();
FundamentalMatrix actual(F);
// We check the matrices as the underlying representation is not unique
CHECK(assert_equal(F, actual.matrix()));
}
}
//*************************************************************************
TEST(FundamentalMatrix, RoundTrip) {
Vector7 d;
@ -61,14 +81,14 @@ TEST(SimpleStereo, Conversion) {
}
//*************************************************************************
TEST(SimpleStereo, localCoordinates) {
TEST(SimpleStereo, LocalCoordinates) {
Vector expected = Z_7x1;
Vector actual = stereoF.localCoordinates(stereoF);
EXPECT(assert_equal(expected, actual, 1e-8));
}
//*************************************************************************
TEST(SimpleStereo, retract) {
TEST(SimpleStereo, Retract) {
SimpleFundamentalMatrix actual = stereoF.retract(Z_9x1);
EXPECT(assert_equal(stereoF, actual));
}

View File

@ -11,6 +11,7 @@ namespace gtsam {
#include <gtsam/geometry/Cal3_S2.h>
#include <gtsam/geometry/CalibratedCamera.h>
#include <gtsam/geometry/EssentialMatrix.h>
#include <gtsam/geometry/FundamentalMatrix.h>
#include <gtsam/geometry/PinholeCamera.h>
#include <gtsam/geometry/Point2.h>
#include <gtsam/geometry/Point3.h>
@ -537,6 +538,8 @@ class ISAM2 {
template <VALUE = {gtsam::Point2, gtsam::Rot2, gtsam::Pose2, gtsam::Point3,
gtsam::Rot3, gtsam::Pose3, gtsam::Cal3_S2, gtsam::Cal3DS2,
gtsam::Cal3Bundler, gtsam::EssentialMatrix,
gtsam::Cal3Bundler, gtsam::FundamentalMatrix,
gtsam::Cal3Bundler, gtsam::SimpleFundamentalMatrix,
gtsam::PinholeCamera<gtsam::Cal3_S2>,
gtsam::PinholeCamera<gtsam::Cal3Bundler>,
gtsam::PinholeCamera<gtsam::Cal3Fisheye>,

View File

@ -62,7 +62,7 @@ def main():
points = SFMdata.createPoints()
# Create the set of ground-truth poses
poses = SFMdata.createPoses(K)
poses = SFMdata.createPoses()
# Create a factor graph
graph = NonlinearFactorGraph()

View File

@ -3,14 +3,14 @@ A structure-from-motion example with landmarks
- The landmarks form a 10 meter cube
- The robot rotates around the landmarks, always facing towards the cube
"""
# pylint: disable=invalid-name, E1101
from typing import List
import numpy as np
import gtsam
from gtsam import Cal3_S2, Point3, Pose3
from gtsam import Point3, Pose3, Rot3
def createPoints() -> List[Point3]:
@ -28,16 +28,49 @@ def createPoints() -> List[Point3]:
return points
def createPoses(K: Cal3_S2) -> List[Pose3]:
"""Generate a set of ground-truth camera poses arranged in a circle about the origin."""
radius = 40.0
height = 10.0
angles = np.linspace(0, 2 * np.pi, 8, endpoint=False)
up = gtsam.Point3(0, 0, 1)
target = gtsam.Point3(0, 0, 0)
poses = []
for theta in angles:
position = gtsam.Point3(radius * np.cos(theta), radius * np.sin(theta), height)
camera = gtsam.PinholeCameraCal3_S2.Lookat(position, target, up, K)
poses.append(camera.pose())
_M_PI_2 = np.pi / 2
_M_PI_4 = np.pi / 4
def createPoses(
init: Pose3 = Pose3(Rot3.Ypr(_M_PI_2, 0, -_M_PI_2), Point3(30, 0, 0)),
delta: Pose3 = Pose3(
Rot3.Ypr(0, -_M_PI_4, 0),
Point3(np.sin(_M_PI_4) * 30, 0, 30 * (1 - np.sin(_M_PI_4))),
),
steps: int = 8,
) -> List[Pose3]:
"""
Create a set of ground-truth poses
Default values give a circular trajectory, radius 30 at pi/4 intervals,
always facing the circle center
"""
poses = [init]
for _ in range(1, steps):
poses.append(poses[-1].compose(delta))
return poses
def posesOnCircle(num_cameras=8, R=30):
"""Create regularly spaced poses with specified radius and number of cameras."""
theta = 2 * np.pi / num_cameras
# Initial pose at angle 0, position (R, 0, 0), facing the center with Y-axis pointing down
init_rotation = Rot3.Ypr(_M_PI_2, 0, -_M_PI_2)
init_position = np.array([R, 0, 0])
init = Pose3(init_rotation, init_position)
# Delta rotation: rotate by -theta around Z-axis (counterclockwise movement)
delta_rotation = Rot3.Ypr(0, -theta, 0)
# Delta translation in world frame
delta_translation_world = np.array([R * (np.cos(theta) - 1), R * np.sin(theta), 0])
# Transform delta translation to local frame of the camera
delta_translation_local = init.rotation().unrotate(delta_translation_world)
# Define delta pose
delta = Pose3(delta_rotation, delta_translation_local)
# Generate poses
return createPoses(init, delta, num_cameras)

View File

@ -31,8 +31,7 @@ def get_data() -> Tuple[gtsam.Values, List[gtsam.BinaryMeasurementUnit3]]:
that lie on a circle and face the center. The poses of 8 cameras are obtained from SFMdata
and the unit translations directions between some camera pairs are computed from their
global translations. """
fx, fy, s, u0, v0 = 50.0, 50.0, 0.0, 50.0, 50.0
wTc_list = SFMdata.createPoses(gtsam.Cal3_S2(fx, fy, s, u0, v0))
wTc_list = SFMdata.createPoses()
# Rotations of the cameras in the world frame.
wRc_values = gtsam.Values()
# Normalized translation directions from camera i to camera j

View File

@ -73,7 +73,7 @@ def visual_ISAM2_example():
points = SFMdata.createPoints()
# Create the set of ground-truth poses
poses = SFMdata.createPoses(K)
poses = SFMdata.createPoses()
# Create an iSAM2 object. Unlike iSAM1, which performs periodic batch steps
# to maintain proper linearization and efficient variable ordering, iSAM2

View File

@ -36,7 +36,7 @@ def main():
# Create the set of ground-truth landmarks
points = SFMdata.createPoints()
# Create the set of ground-truth poses
poses = SFMdata.createPoses(K)
poses = SFMdata.createPoses()
# Create a NonlinearISAM object which will relinearize and reorder the variables
# every "reorderInterval" updates

View File

@ -0,0 +1,285 @@
"""
GTSAM Copyright 2010-2019, Georgia Tech Research Corporation,
Atlanta, Georgia 30332-0415
All Rights Reserved
See LICENSE for the license information
FundamentalMatrix unit tests.
Author: Frank Dellaert
"""
# pylint: disable=no-name-in-module
import unittest
import numpy as np
from gtsam.examples import SFMdata
from numpy.testing import assert_almost_equal
import gtsam
from gtsam import (Cal3_S2, EssentialMatrix, FundamentalMatrix,
PinholeCameraCal3_S2, Point2, Point3, Rot3,
SimpleFundamentalMatrix, Unit3)
class TestFundamentalMatrix(unittest.TestCase):
def setUp(self):
# Create two rotations and corresponding fundamental matrix F
self.trueU = Rot3.Yaw(np.pi / 2)
self.trueV = Rot3.Yaw(np.pi / 4)
self.trueS = 0.5
self.trueF = FundamentalMatrix(self.trueU.matrix(), self.trueS, self.trueV.matrix())
def test_localCoordinates(self):
expected = np.zeros(7) # Assuming 7 dimensions for U, V, and s
actual = self.trueF.localCoordinates(self.trueF)
assert_almost_equal(expected, actual, decimal=8)
def test_retract(self):
actual = self.trueF.retract(np.zeros(7))
self.assertTrue(self.trueF.equals(actual, 1e-9))
def test_RoundTrip(self):
d = np.array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7])
hx = self.trueF.retract(d)
actual = self.trueF.localCoordinates(hx)
assert_almost_equal(d, actual, decimal=8)
class TestSimpleStereo(unittest.TestCase):
def setUp(self):
# Create the simplest SimpleFundamentalMatrix, a stereo pair
self.defaultE = EssentialMatrix(Rot3(), Unit3(1, 0, 0))
self.zero = Point2(0.0, 0.0)
self.stereoF = SimpleFundamentalMatrix(self.defaultE, 1.0, 1.0, self.zero, self.zero)
def test_Conversion(self):
convertedF = FundamentalMatrix(self.stereoF.matrix())
assert_almost_equal(self.stereoF.matrix(), convertedF.matrix(), decimal=8)
def test_localCoordinates(self):
expected = np.zeros(7)
actual = self.stereoF.localCoordinates(self.stereoF)
assert_almost_equal(expected, actual, decimal=8)
def test_retract(self):
actual = self.stereoF.retract(np.zeros(9))
self.assertTrue(self.stereoF.equals(actual, 1e-9))
def test_RoundTrip(self):
d = np.array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7])
hx = self.stereoF.retract(d)
actual = self.stereoF.localCoordinates(hx)
assert_almost_equal(d, actual, decimal=8)
def test_EpipolarLine(self):
# Create a point in b
p_b = np.array([0, 2, 1])
# Convert the point to a horizontal line in a
l_a = self.stereoF.matrix() @ p_b
# Check if the line is horizontal at height 2
expected = np.array([0, -1, 2])
assert_almost_equal(l_a, expected, decimal=8)
class TestPixelStereo(unittest.TestCase):
def setUp(self):
# Create a stereo pair in pixels, zero principal points
self.focalLength = 1000.0
self.defaultE = EssentialMatrix(Rot3(), Unit3(1, 0, 0))
self.zero = Point2(0.0, 0.0)
self.pixelStereo = SimpleFundamentalMatrix(
self.defaultE, self.focalLength, self.focalLength, self.zero, self.zero
)
def test_Conversion(self):
expected = self.pixelStereo.matrix()
convertedF = FundamentalMatrix(self.pixelStereo.matrix())
# Check equality of F-matrices up to a scale
actual = convertedF.matrix()
scale = expected[1, 2] / actual[1, 2]
actual *= scale
assert_almost_equal(expected, actual, decimal=5)
def test_PointInBToHorizontalLineInA(self):
# Create a point in b
p_b = np.array([0, 300, 1])
# Convert the point to a horizontal line in a
l_a = self.pixelStereo.matrix() @ p_b
# Check if the line is horizontal at height 0.3
expected = np.array([0, -0.001, 0.3])
assert_almost_equal(l_a, expected, decimal=8)
class TestRotatedPixelStereo(unittest.TestCase):
def setUp(self):
# Create a stereo pair with the right camera rotated 90 degrees
self.focalLength = 1000.0
self.zero = Point2(0.0, 0.0)
self.aRb = Rot3.Rz(np.pi / 2) # Rotate 90 degrees around the Z-axis
self.rotatedE = EssentialMatrix(self.aRb, Unit3(1, 0, 0))
self.rotatedPixelStereo = SimpleFundamentalMatrix(
self.rotatedE, self.focalLength, self.focalLength, self.zero, self.zero
)
def test_Conversion(self):
expected = self.rotatedPixelStereo.matrix()
convertedF = FundamentalMatrix(self.rotatedPixelStereo.matrix())
# Check equality of F-matrices up to a scale
actual = convertedF.matrix()
scale = expected[1, 2] / actual[1, 2]
actual *= scale
assert_almost_equal(expected, actual, decimal=4)
def test_PointInBToHorizontalLineInA(self):
# Create a point in b
p_b = np.array([300, 0, 1])
# Convert the point to a horizontal line in a
l_a = self.rotatedPixelStereo.matrix() @ p_b
# Check if the line is horizontal at height 0.3
expected = np.array([0, -0.001, 0.3])
assert_almost_equal(l_a, expected, decimal=8)
class TestStereoWithPrincipalPoints(unittest.TestCase):
def setUp(self):
# Now check that principal points also survive conversion
self.focalLength = 1000.0
self.principalPoint = Point2(640 / 2, 480 / 2)
self.aRb = Rot3.Rz(np.pi / 2)
self.rotatedE = EssentialMatrix(self.aRb, Unit3(1, 0, 0))
self.stereoWithPrincipalPoints = SimpleFundamentalMatrix(
self.rotatedE, self.focalLength, self.focalLength, self.principalPoint, self.principalPoint
)
def test_Conversion(self):
expected = self.stereoWithPrincipalPoints.matrix()
convertedF = FundamentalMatrix(self.stereoWithPrincipalPoints.matrix())
# Check equality of F-matrices up to a scale
actual = convertedF.matrix()
scale = expected[1, 2] / actual[1, 2]
actual *= scale
assert_almost_equal(expected, actual, decimal=4)
class TestTripleF(unittest.TestCase):
def setUp(self):
# Generate three cameras on a circle, looking in
self.cameraPoses = SFMdata.posesOnCircle(3, 1.0)
self.focalLength = 1000.0
self.principalPoint = Point2(640 / 2, 480 / 2)
self.triplet = self.generateTripleF(self.cameraPoses)
def generateTripleF(self, cameraPoses):
F = []
for i in range(3):
j = (i + 1) % 3
iPj = cameraPoses[i].between(cameraPoses[j])
E = EssentialMatrix(iPj.rotation(), Unit3(iPj.translation()))
F_ij = SimpleFundamentalMatrix(
E, self.focalLength, self.focalLength, self.principalPoint, self.principalPoint
)
F.append(F_ij)
return {"Fab": F[0], "Fbc": F[1], "Fca": F[2]}
def transferToA(self, pb, pc):
return gtsam.EpipolarTransfer(self.triplet["Fab"].matrix(), pb, self.triplet["Fca"].matrix().transpose(), pc)
def transferToB(self, pa, pc):
return gtsam.EpipolarTransfer(self.triplet["Fab"].matrix().transpose(), pa, self.triplet["Fbc"].matrix(), pc)
def transferToC(self, pa, pb):
return gtsam.EpipolarTransfer(self.triplet["Fca"].matrix(), pa, self.triplet["Fbc"].matrix().transpose(), pb)
def test_Transfer(self):
triplet = self.triplet
# Check that they are all equal
self.assertTrue(triplet["Fab"].equals(triplet["Fbc"], 1e-9))
self.assertTrue(triplet["Fbc"].equals(triplet["Fca"], 1e-9))
self.assertTrue(triplet["Fca"].equals(triplet["Fab"], 1e-9))
# Now project a point into the three cameras
P = Point3(0.1, 0.2, 0.3)
K = Cal3_S2(self.focalLength, self.focalLength, 0.0, self.principalPoint[0], self.principalPoint[1])
p = []
for i in range(3):
# Project the point into each camera
camera = PinholeCameraCal3_S2(self.cameraPoses[i], K)
p_i = camera.project(P)
p.append(p_i)
# Check that transfer works
assert_almost_equal(p[0], self.transferToA(p[1], p[2]), decimal=9)
assert_almost_equal(p[1], self.transferToB(p[0], p[2]), decimal=9)
assert_almost_equal(p[2], self.transferToC(p[0], p[1]), decimal=9)
class TestManyCamerasCircle(unittest.TestCase):
N = 6
def setUp(self):
# Generate six cameras on a circle, looking in
self.cameraPoses = SFMdata.posesOnCircle(self.N, 1.0)
self.focalLength = 1000.0
self.principalPoint = Point2(640 / 2, 480 / 2)
self.manyFs = self.generateManyFs(self.cameraPoses)
def generateManyFs(self, cameraPoses):
F = []
for i in range(self.N):
j = (i + 1) % self.N
iPj = cameraPoses[i].between(cameraPoses[j])
E = EssentialMatrix(iPj.rotation(), Unit3(iPj.translation()))
F_ij = SimpleFundamentalMatrix(
E, self.focalLength, self.focalLength, self.principalPoint, self.principalPoint
)
F.append(F_ij)
return F
def test_Conversion(self):
for i in range(self.N):
expected = self.manyFs[i].matrix()
convertedF = FundamentalMatrix(self.manyFs[i].matrix())
# Check equality of F-matrices up to a scale
actual = convertedF.matrix()
scale = expected[1, 2] / actual[1, 2]
actual *= scale
# print(f"\n{np.round(expected, 3)}", f"\n{np.round(actual, 3)}")
assert_almost_equal(expected, actual, decimal=4)
def test_Transfer(self):
# Now project a point into the six cameras
P = Point3(0.1, 0.2, 0.3)
K = Cal3_S2(self.focalLength, self.focalLength, 0.0, self.principalPoint[0], self.principalPoint[1])
p = []
for i in range(self.N):
# Project the point into each camera
camera = PinholeCameraCal3_S2(self.cameraPoses[i], K)
p_i = camera.project(P)
p.append(p_i)
# Check that transfer works
for a in range(self.N):
b = (a + 1) % self.N
c = (a + 2) % self.N
# We transfer from a to b and from c to b,
# and check that the result lines up with the projected point in b.
transferred = gtsam.EpipolarTransfer(
self.manyFs[a].matrix().transpose(), # need to transpose for a->b
p[a],
self.manyFs[c].matrix(),
p[c],
)
assert_almost_equal(p[b], transferred, decimal=9)
if __name__ == "__main__":
unittest.main()