Symforce
- Introduction ¶
- Tutorials ¶
- Solve Problems in Real World ¶
Introduction ¶
SymForce is a fast symbolic computation and code generation library for robotics applications like computer vision, state estimation, motion planning, and controls. It combines the development speed and flexibility of symbolic mathematics with the performance of autogenerated, highly optimized code in C++ or any target runtime language. SymForce contains three independently useful systems:
Symbolic Toolkit - builds on the SymPy API to provide rigorous geometric and camera types, lie group calculus, singularity handling, and tools to model complex problems
Code Generator - transforms symbolic expressions into blazing-fast, branchless code with clean APIs and minimal dependencies, with a template system to target any language
Optimization Library - a fast tangent-space optimization library based on factor graphs, with a highly optimized implementation for real-time robotics applications
SymForce automatically computes tangent space Jacobians, eliminating the need for any bug-prone handwritten derivatives. Generated functions can be directly used as factors in our nonlinear optimizer. This workflow enables faster runtime functions, faster development time, and fewer lines of handwritten code versus alternative methods.
SymForce is developed and maintained by Skydio . It is used in production to accelerate tasks like SLAM, bundle adjustment, calibration, and sparse nonlinear MPC for autonomous robots at scale.
Tutorials ¶
# Import symforce
import symforce
symforce.set_symbolic_api("sympy")
symforce.set_log_level("warning")
symforce.set_epsilon_to_symbol()
import symforce.symbolic as sf
from symforce.notebook_util import display
#
Geo Package ¶
Matrix ¶
# Define matrix
import symforce.symbolic as sf
from symforce.notebook_util import display
# Construction from 2D list
m1 = sf.Matrix([[1, 2, 3], [4, 5, 6]])
print(m1.shape)
# Construction using specified size + data
m2 = sf.Matrix(2, 3, [1, 2, 3, 4, 5, 6])
print(m2.shape)
# Construction from 2D list
v1 = sf.Matrix([[1], [2], [3]])
print(v1.shape)
# Construction from 1D list. We assume a 1D list represents a column vector.
v2 = sf.Matrix([1, 2, 3])
print(v2.shape)
# Construction using aliases (defined by default for 9x1 vectors and smaller)
v3 = sf.V3(1, 2, 3)
print(v3.shape)
zero_matrix = sf.Matrix33.zero()
identity_matrix = sf.Matrix33.eye()
zero_matrix = sf.Matrix.zeros(3,3)
identity_matrix = sf.Matrix.eye(3,3)
#
Rotations ¶
Quaternion is used as the underlying representation.
# Define rotations
import symforce.symbolic as sf
from symforce.notebook_util import display
# Identity definition
R = sf.Rot3()
display(R)
display(R.to_rotation_matrix())
display(R.to_rotation_matrix().to_numpy())
# Symbolic definition
R_sym = sf.Rot3.symbolic("R")
display(R_sym)
display(R_sym.to_rotation_matrix())
# Symbolic definition
Q_sym = sf.Quaternion.symbolic("Q")
display(Q_sym)
#
symforce.symbolic.Pose3 and sym.Pose3 ¶
SymForce provides
sym
packages with
runtime code
for geometry and camera types, e.g.
sym.Pose3
that are generated from its
symbolic geo and cam packages
, e.g.
symforce.symbolic.Pose3
. As such, there are multiple versions of a class like Pose3 and it can be a common source of confusion.
# symforce.symbolic
import symforce.symbolic as sf
from symforce.notebook_util import display
q = sf.Quaternion(xyz = sf.V3(0.0, 0.0, 0.0), w=sf.Scalar(1.0))
R = sf.Rot3(q)
t = sf.V3(0.0, 0.0, 0.0)
T = sf.Pose3(R, t)
display(T)
#
# sym runtime code
import sym
import symforce.symbolic as sf
print(f"sym package rotation matrix type: {type(sym.Pose3.identity().R.to_rotation_matrix())}")
print(f"symbolic package rotation matrix type: {type(sf.Pose3.identity().R.to_rotation_matrix())}")
print(f"symbolic package rotation matrix to_numpy type: { type(sf.Pose3.identity().R.to_rotation_matrix().to_numpy())}")
# k3d plot helper
import k3d
import math
import numpy as np
import time
# TODO(xipeng.wang): Update this to use symforce directly instead of numpy
class XYZ_RBG_Axes():
def __init__(self, plot, name, R, t, color=0xffff00, axis_length = 1):
self.axis_length = axis_length
self.plot = plot
self.R = R
self.t = t.reshape(3, 1)
self.name = name
origin = self.t
x_axis = np.stack((origin, origin + self.R.dot(np.array([axis_length, 0., 0.])).reshape(3,1)), axis=0)
y_axis = np.stack((origin, origin + self.R.dot(np.array([0., axis_length, 0.])).reshape(3,1)), axis=0)
z_axis = np.stack((origin, origin + self.R.dot(np.array([0., 0., axis_length])).reshape(3,1)), axis=0)
self.xline_plot = k3d.line(x_axis, shader='mesh', width=0.05, color=0xff0000, name=name+'_xaxis')
self.yline_plot = k3d.line(y_axis, shader='mesh', width=0.05, color=0x0000ff, name=name+'_yaxis')
self.zline_plot = k3d.line(z_axis, shader='mesh', width=0.05, color=0x00ff00, name=name+'_zaxis')
self.xpoint = k3d.points(origin + self.R.dot(np.array([axis_length, 0., 0.])).reshape(3,1), point_size=0.3,
shader='3d', color=color, name=self.name+'_end_point_x')
self.ypoint = k3d.points(origin + self.R.dot(np.array([0., axis_length, 0.])).reshape(3,1), point_size=0.3,
shader='3d', color=color, name=self.name+'_end_point_y')
self.zpoint = k3d.points(origin + self.R.dot(np.array([0., 0., axis_length])).reshape(3,1), point_size=0.3,
shader='3d', color=color, name=self.name+'_end_point_z')
self.plot += self.xline_plot
self.plot += self.yline_plot
self.plot += self.zline_plot
self.plot += self.xpoint
self.plot += self.ypoint
self.plot += self.zpoint
def update_pose(self, R, t):
self.R = R
self.t = t.reshape(3,1)
origin = self.t
axis_length = self.axis_length
x_axis = np.stack((origin, origin + self.R.dot(np.array([axis_length, 0., 0.])).reshape(3,1)), axis=0)
y_axis = np.stack((origin, origin + self.R.dot(np.array([0., axis_length, 0.])).reshape(3,1)), axis=0)
z_axis = np.stack((origin, origin + self.R.dot(np.array([0., 0., axis_length])).reshape(3,1)), axis=0)
self.xline_plot.vertices = x_axis
self.xpoint.positions = origin + self.R.dot(np.array([axis_length, 0., 0.])).reshape(3,1)
self.yline_plot.vertices = y_axis
self.ypoint.positions = origin + self.R.dot(np.array([0., axis_length, 0.])).reshape(3,1)
self.zline_plot.vertices = z_axis
self.zpoint.positions = origin + self.R.dot(np.array([0., 0., axis_length])).reshape(3,1)
#
# k3d plot example
plot = k3d.plot(camera_auto_fit=True, grid=(0, 0, 0, 5, 5, 5))
R = sf.Rot3().to_rotation_matrix().to_numpy()
t = sf.V3(0, 0, 0).to_numpy()
C0 = XYZ_RBG_Axes(plot, 'C0', R, t)
R = sf.Rot3.random().to_rotation_matrix().to_numpy()
t = sf.V3(1, 0, 0).to_numpy()
C1 = XYZ_RBG_Axes(plot, 'C1', R, t)
plot.display()
#
Values ¶
Values objects are ordered dict-like containers used to store multiple heterogeneous objects, normally for the purpose of function generation.
# Create Values
import symforce.symbolic as sf
from symforce.values import Values
from symforce.notebook_util import display
inputs = Values(
x=sf.Symbol("x"),
y=sf.Symbol("y")
)
inputs.add(sf.Symbol("z"))
time = Values(
{"h": sf.Symbol("h"),
"m": sf.Symbol("m"),
"s": sf.Symbol("s")
}
)
timezone = Values(time_zone=sf.Symbol("zone"))
# time["t"] = time
inputs["t"] = time
time["timezone"] = timezone
inputs.add("id")
display(inputs)
# A Values serializes to a depth-first traversed list.
# display(inputs.to_storage())
display(inputs.items_recursive())
# display(inputs.keys_recursive())
# display(inputs.values_recursive())
# Index describes which parts of the serialized list correspond to which types.
# The spec is T.Dict[str, IndexEntry] where IndexEntry has attributes
# offset, storage_dim, datatype, shape, item_index
index = inputs.index()
reconstructed_inputs = Values.from_storage_index(inputs.to_storage(), index)
assert inputs == reconstructed_inputs
print(f'Before modifying id: {inputs["id"]}')
inputs["id"] = 1024
inputs.attr.id = 1024
print(f'After modifying id: {inputs["id"]}')
#
# Use scope to add sub-values
with sf.scope("foo"):
inputs.add(sf.Symbol("x"))
with sf.scope("bar"):
inputs.add(sf.Symbol("x"))
inputs.add(sf.Symbol("y"))
inputs.attr.foo.bar.z = sf.Symbol("z")
# This attr can also be used to add new values
inputs.attr.foo.bar.x = 1024
with inputs.scope("foo2"):
inputs["x"] = sf.Symbol("x")
with inputs["foo2"].scope("bar2"):
inputs["foo2"]["x"] = sf.Symbol("x")
display(inputs)
display(inputs.items_recursive())
#
# Values contains a list
initial_values = Values(
# Values key will be distances[0], distances[1]
distances=[sf.V2.symbolic("t"), sf.V2.symbolic("t")],
epsilon=sf.numeric_epsilon,
)
display(initial_values.items_recursive())
#
# Group Ops on values
w_T_b = Values(
T=sf.Pose3.symbolic("T")
)
print(f"tangent dim: {w_T_b.tangent_dim()}, storage dim: {len(w_T_b.to_storage())}")
print(f"storage_D_tangent shape: {w_T_b.storage_D_tangent().shape}")
# display(pose.to_tangent())
p_w = sf.Matrix(3, 1).symbolic("w")
p_b = sf.Matrix(3, 1).symbolic("b")
residual = w_T_b["T"] * p_b - p_w
residual_D_tangent = residual.jacobian(w_T_b["T"])
display(residual)
display(residual_D_tangent.shape)
display(residual_D_tangent)
#
Ops Package ¶
Storage Ops ¶
Data type that can be serialized to and from a vector of scalar quantities.
.storage_dim()
,
.to_storage()
,
.from_storage()
,
.symbolic()
,
.evalf()
,
.subs()
,
.simplify()
# Storage Ops
from symforce.ops import StorageOps
# Element-wise operations on Values object with multiple types of elements
values = Values(
pose=sf.Pose3(),
scalar=sf.Symbol("x"),
)
print(f"Storage dim: {StorageOps.storage_dim(values)}") # 4 quaternion + 3 position + 1 scalar
# Serialize geometric type and reconstruct
T = sf.Pose3.symbolic("T")
T_serialized = StorageOps.to_storage(T)
T_recovered = StorageOps.from_storage(sf.Pose3, T_serialized)
display(T_serialized)
assert(T == T_recovered)
Group Ops ¶
Mathematical group that implements closure, associativity, identity and invertibility.
Methods:
.identity()
,
.inverse()
,
.compose()
,
.between()
# Group Ops
from symforce.ops import GroupOps
# Identity of a pose
w_T_b = sf.Pose3()
display(GroupOps.identity(w_T_b))
# Inverse of a vector
p_b = sf.V3(1, 1, 1)
display(GroupOps.inverse(p_b).T)
# Compose a pose and a point (wrong, they are not the same group)
p_w = w_T_b * p_b # GroupOps.compose(w_T_b, p_b)
# display(p_w)
display(GroupOps.compose(p_b, GroupOps.inverse(p_b)).T)
# Relative rotation -> b1_R_b2 = GroupOps.between(w_R_b1, w_R_b2)
w_R_b1 = sf.Rot3.from_angle_axis(
angle=sf.Scalar(0.2),
axis=sf.V3(0, 1, 2).normalized(),
)
w_R_b2 = sf.Rot3.from_angle_axis(
angle=sf.Scalar(0.1),
axis=sf.V3(1, 0, 3).normalized(),
)
b1_R_b2 = GroupOps.between(w_R_b1, w_R_b2)
display(w_R_b2)
display(GroupOps.simplify(GroupOps.compose(w_R_b1, b1_R_b2)))
Lie Group Ops ¶
Group that is also a differentiable manifold, e.g. SO3, SE3.
Methods:
.tangent_dim()
,
.from_tangent()
,
to_tangent()
,
.retract()
,
.local_coordinates()
,
.storage_D_tangent()
# Lie Group Ops: basics
from symforce.ops import LieGroupOps
R = sf.Rot3.symbolic('theta')
# Underlying dimension of a 3D rotation's tangent space
print(f"3D rotation tangent space: {LieGroupOps.tangent_dim(R)}")
R_tangent = sf.V3(LieGroupOps.to_tangent(R))
display(R_tangent.T)
R_reconstruct_ = sf.Rot3.from_angle_axis(R_tangent.norm(), R_tangent.normalized())
R_reconstruct = LieGroupOps.from_tangent(sf.Rot3, R_tangent)
assert R_reconstruct_ == R_reconstruct
# Lie Group Ops, retract: R_perturbed = R * delta_R, local_coordinates: delta_R = R.inv() * R_perturbed
# Retract perturbs the given element in the tangent space and returns the updated element
R = sf.Rot3.from_yaw_pitch_roll(0, 0, 1)
R_tangent_delta = sf.V3(0, 0, 0.1)
R_perturbed = LieGroupOps.retract(R, R_tangent_delta)
# Local coordinates compute the tangent space perturbation between one element and another
R_tangent_delta_reconstruct = LieGroupOps.local_coordinates(R, R_perturbed)
display(R_tangent_delta.T)
display(R_tangent_delta_reconstruct)
Geo Package Ops Operations ¶
Geo Storage Operations ¶
# Geo Storage Operations
rot = sf.Rot3()
elements = rot.to_storage()
print("Symbolic to_storage test:")
assert len(elements) == rot.storage_dim()
display(elements)
# Construction from scalar list
rot2 = sf.Rot3.from_storage(elements)
assert rot == rot2
# Symbolic operations
print("Symbolic subs test:")
rot_sym = sf.Rot3.symbolic("rot_sym")
rot_num = rot_sym.subs(rot_sym, rot)
display(rot_sym)
display(rot_num)
display(rot_num.simplify()) # Simplify internal symbolic expressions
display(rot_num.evalf()) # Numerical evaluation
#
Geo Group Operations ¶
# Geo Group Operations
R1 = sf.Rot3.random()
R2 = sf.Rot3.random()
# Composition
print("Composition test:")
display(R1.compose(R2)) # For rotations this is the same as R1 * R2
# Identity
print("Identity test:")
R_identity = sf.Rot3.identity()
display(R1)
display(R_identity * R1)
# Inverse
print("Inverse test:")
R1_inv = R1.inverse()
display(R_identity)
display(R1_inv * R1)
# Between
print("Between test:")
R_delta = R1.between(R2)
display(R1 * R_delta)
display(R2)
#
Geo Tangent Operations ¶
# Geo Tangent Operations
# To/From tangent space vector about identity element
print("To/From tangent space vector test:")
R1 = sf.Rot3.random()
tangent_vec = R1.to_tangent()
R1_recovered = sf.Rot3.from_tangent(tangent_vec)
assert len(tangent_vec) == R1.tangent_dim()
display(R1)
display(R1_recovered)
print("Tangent space perturbations test:")
# Tangent space perturbations
# Perturb R1 by the given vector in the tangent space around R1
R_perturb_delta = [0.1, 2.3, -0.5]
R2 = R1.retract(R_perturb_delta)
# Compute the tangent vector pointing from R1 to R2, in the tangent space around R1
recovered_tangent_vec = R1.local_coordinates(R2)
recovered_tangent_vec2 = R1.between(R2).to_tangent()
display(R_perturb_delta)
display(recovered_tangent_vec)
display(recovered_tangent_vec2)
jacobian = R1.storage_D_tangent()
assert jacobian.shape == (R1.storage_dim(), R1.tangent_dim())
#
Special SE(n) ¶
Perturbation on SE(3) is done separately on SO(3) and R(3).
Pose3(R=self.R.retract(vec[:3], epsilon=epsilon),
t=ops.LieGroupOps.retract(self.t, vec[3:], epsilon=epsilon))
# SE(n)
T = sf.Pose3(sf.Rot3.random(), sf.V3(0, 0, 1))
display(T)
T_tangent_delta = [0.1, 0.1, 0.1, 0.1, 0, 0.1]
T_perturb = T.retract(T_tangent_delta)
display(T_perturb)
T_perturb_reconstruct = T.compose(sf.Pose3.from_tangent(T_tangent_delta))
display(T_perturb_reconstruct)
assert T_perturb_reconstruct != T_perturb # This is expected since SE(3) is modeled as SO(3) + R(3)
T_perturb_reconstruct = sf.Pose3(T.rotation().compose(sf.Rot3.from_tangent(T_tangent_delta[0:3])),
T.t + sf.V3(T_tangent_delta[3:]))
assert T_perturb_reconstruct == T_perturb
display(T_perturb_reconstruct)
Code Generation ¶
# Setup
import numpy as np
import os
import symforce
from symforce import codegen
from symforce.codegen import codegen_util
from symforce import ops
import symforce.symbolic as sf
from symforce.values import Values
from symforce.notebook_util import display, display_code, display_code_file
#
Code generation using implicit functions with Values objects ¶
# Generate functions
from symforce.values import Values
inputs = Values(
w_T_b = sf.Pose3.symbolic("T"),
b_t_p = sf.V3.symbolic("p")
)
namespace = "my_sym"
outputs = Values()
outputs["w_t_p"] = inputs["w_T_b"] * inputs.attr.b_t_p
input_values = Values(inputs=inputs)
output_values = Values(outputs=outputs)
point_projection_condegen = codegen.Codegen(
inputs=input_values,
outputs=output_values,
config=codegen.CppConfig(),
name="point_projection",
# If specified, the output with this key is returned rather than filled in as a named output argument
return_key="outputs"
)
point_projection_condegen_data = point_projection_condegen.generate_function(namespace=namespace)
# Print what we generated
print("Files generated in {}:\n".format(point_projection_condegen_data.output_dir))
for f in point_projection_condegen_data.generated_files:
print(" |- {}".format(os.path.relpath(f, point_projection_condegen_data.output_dir)))
display_code_file(point_projection_condegen_data.function_dir / "point_projection.h", "C++")
# display_code_file(point_projection_condegen_data.cpp_types_dir / namespace / "inputs_t.hpp", "C++")
# display_code_file(point_projection_condegen_data.cpp_types_dir / namespace / "outputs_t.hpp", "C++")
Code generation using python functions ¶
# Generate functions
def transform_point(w_T_b: sf.Pose3, b_t_p: sf.V3, epsilon: sf.Scalar = 0) -> sf.V3:
"""
Transform a point from body frame into world frame.
Args:
w_T_b (sf.Pose3): camera pose in the world
b_t_p (sf.V3): point in body frame
epsilon (Scalar): small number to avoid singularities, not used...
Returns:
sf.V3: (x, y, z)
"""
return w_T_b * b_t_p
my_codegen = codegen.Codegen.function(
func=transform_point,
# config=codegen.PythonConfig(),
config=codegen.CppConfig(),
)
my_codegen_data = my_codegen.generate_function()
print("Files generated in {}:\n".format(my_codegen_data.output_dir))
for f in my_codegen_data.generated_files:
print(" |- {}".format(os.path.relpath(f, my_codegen_data.output_dir)))
display_code_file(my_codegen_data.generated_files[0], "C++")
#
# Generate jacobian function
def transform_point(w_T_b: sf.Pose3, b_t_p: sf.V3, epsilon: sf.Scalar = 0) -> sf.V3:
"""
Transform a point from body frame into world frame.
Args:
w_T_b (sf.Pose3): camera pose in the world
b_t_p (sf.V3): point in body frame
epsilon (Scalar): small number to avoid singularities, not used...
Returns:
sf.Pose3: w_Trans_b
sf.V3: b_t_point
sf.V3: w_t_point
"""
return w_T_b, b_t_p, w_T_b * b_t_p
my_codegen = codegen.Codegen.function(
func=transform_point,
# config=codegen.PythonConfig(),
config=codegen.CppConfig(),
input_types=[sf.Pose3, sf.V3, sf.Scalar],
name="my_transform_point",
# We have three outputs
output_names=["w_Trans_b", "point_t_b", "point_t_w"],
# Function returns point_t_w
return_key="point_t_w"
)
codegen_with_jacobians = my_codegen.with_jacobians(
# Just compute wrt the pose and point, not epsilon
# Note the function name has a suffix 01 because it calculates jacobian wrt. the first and second inputs
which_args=["w_T_b", "b_t_p"],
# We only want to calculate jacobian for `point_t_b` and `point_t_w`
which_results=[1, 2],
# Include value, not just jacobians
include_results=True,
# sparse_jacobians=True,
# name="MyUselessJaccobians"
)
data = codegen_with_jacobians.generate_function()
from symforce.notebook_util import display_code_file
display_code_file(data.generated_files[0], "C++")
#
# Generate with_linearization function with only 1 output
def transform_point(w_T_b: sf.Pose3, b_t_p: sf.V3, epsilon: sf.Scalar = 0) -> sf.V3:
"""
Transform a point from body frame into world frame.
Args:
w_T_b (sf.Pose3): camera pose in the world
b_t_p (sf.V3): point in body frame
epsilon (Scalar): small number to avoid singularities, not used...
Returns:
sf.V3: w_t_point
"""
return w_T_b * b_t_p
my_codegen = codegen.Codegen.function(
func=transform_point,
# config=codegen.PythonConfig(),
config=codegen.CppConfig(),
name="my_transform_point",
# We have only 1 output
output_names=["point_t_w"],
# Function returns point_t_w
return_key="point_t_w"
)
codegen_with_jacobians = my_codegen.with_linearization(
# Just compute wrt the pose and point, not epsilon
# Note the function name has a suffix 01 because it calculates jacobian wrt. the first and second inputs
which_args=["w_T_b"],
# Include value, not just jacobians
include_result=True,
#linearization_mode=codegen.LinearizationMode.STACKED_JACOBIAN
# Generate jacobian, hession and rhs for Gauss Netwon
linearization_mode=codegen.LinearizationMode.FULL_LINEARIZATION,
# name="MyUselessJaccobians"
)
data = codegen_with_jacobians.generate_function()
from symforce.notebook_util import display_code_file
display_code_file(data.generated_files[0], "C++")
#
Optimization ¶
Let's walk through solving a simplified
point cloud registration problem
with Symforce. In this example, we know the correspondences between two point clouds and we would like to find a
sf.Pose3
to align two point clouds.
Symforce setup ¶
In this example, we will use symbolic variables to define our residual function so we need to import
symforce.symbolic
.
import symforce.symbolic as sf
We build up input
Values
using runtime types, e.g.
sym.pose3
,
np.ndarray
instead of symbolic types.
from symforce.values import Values
import numpy as np
import sym
For building a factor graph and solve the problem, we need to import
Factor
and
Optimizer
.
from symforce.opt.factor import Factor
from symforce.opt.optimizer import Optimizer
In this example, we will also show the usage of customized cost functions for computing residuals.
from symforce.opt.noise_models import IsotropicNoiseModel
Set up the problem ¶
We first generate 30 points in robot body frame.
np.random.seed(seed=1024)
num_points = 30
points_b = []
for i in range(num_points):
points_b.append(np.random.uniform(low=0.0, high=1.0, size=(3,)))
We create a ground truth
w_T_b_GT
, and use it to transform the 30 points in body frame to world frame.
pose_tangent_delta = np.array([0.1, 0.1, 0, 0, 0, 10])
w_T_b_GT = sym.Pose3.identity().retract(pose_tangent_delta)
points_w = []
for i in range(num_points):
points_w.append(w_T_b_GT * points_b[i])
Build an optimization problem ¶
We define a simple residual function which simply calculates the difference between two corresponded points in the aligned point clouds. Note that we apply a
IsotropicNoiseModel
to whiten the residual. It doesn't really affect the optimization results but you can see the final total error is scaled by the
sigma
.
def projection_residual(w_T_b: sf.Pose3, b_t_p: sf.V3, w_t_p: sf.V3, sigma: sf.Scalar) -> sf.V3:
noise_model = IsotropicNoiseModel.from_sigma(sigma)
return noise_model.whiten(w_T_b*b_t_p - w_t_p)
We build up the inputs to the optimization problem using symforce runtime types. Note that the initial pose value is
sym.Pose3.identity()
which is different from
w_T_b_GT
. We hope the optimizer can give us a solution very close to
w_T_b_GT
.
inputs = Values(
w_T_b=sym.Pose3.identity(),
points_b=points_b,
points_w=points_w,
sigma=10.0,
)
Create
Factor
s from the residual functions and a set of keys.
factors = []
for i in range(num_points):
factors.append(
Factor(
residual=projection_residual,
keys=["w_T_b", f"points_b[{i}]", f"points_w[{i}]", "sigma"],
)
)
Solve the optimization problem ¶
We create an Optimizer with factors and tell it to only optimize the pose key (the rest are held constant). Here we show lots of configure parameters for the optimizer you can tune.
optimizer = Optimizer(
factors=factors,
optimized_keys=["w_T_b"],
debug_stats=False,
params=Optimizer.Params(
verbose = False,
initial_lambda = 1.0,
lambda_up_factor = 4.0,
lambda_down_factor = 1 / 4.0,
lambda_lower_bound = 0.0,
lambda_upper_bound = 10000.0,
use_diagonal_damping = False,
use_unit_damping = False,
keep_max_diagonal_damping = False,
diagonal_damping_min = 1e-6,
iterations = 500,
early_exit_min_reduction = 1e-6,
enable_bold_updates = False,
)
)
Run the optimization! This returns an
Optimizer.Result
object that contains the optimized values and many other things.
results = optimizer.optimize(inputs)
We can print out the pose before and after the optimization and compare it against our
w_T_b_GT
.
print(f"Early exist? : {results.early_exited}")
print("w_T_b_GT: ")
display(w_T_b_GT)
print("Initial w_T_b: ")
display(results.initial_values.attr.w_T_b)
print("Optimized w_T_b: ")
display(results.optimized_values.attr.w_T_b)
We can also print out the total error before and after optimization. Try to change the
sigma
to see how it will change the error value.
initial_linearization = optimizer.linearize(results.initial_values)
print(f"Initial error: {initial_linearization.error()}")
optimized_linearization = optimizer.linearize(results.optimized_values)
print(f"Final error: {optimized_linearization.error()}")
Cameras ¶
Camera Calibrations ¶
Symforce implements several different calibration models, such as linear model(
LinearCameraCal
), polynomial model(
PolynomialCameraCal
), Kannala-Brandt camera model(
SphericalCameraCal
), etc. They basically contains 1) linear model + distortion coeffs 2) projection and un-projection methods could be used by Camera class.
# Linear model
linear_camera_cal = sf.LinearCameraCal.symbolic("cal")
display(linear_camera_cal)
camera_pixel = sf.V2.symbolic("uv")
camera_ray, valid = linear_camera_cal.camera_ray_from_pixel(camera_pixel)
print("camera_ray_from_pixel: ")
display(camera_ray)
#print(f"Is valid? {valid == 1}")
camera_point = sf.V3.symbolic("p")
camera_point_reprojected, _ = linear_camera_cal.pixel_from_camera_point(
camera_point,
)
print("pixel_from_camera_point: ")
display(camera_point_reprojected)
# Jacobians
print("Jacobians: ")
display(camera_point_reprojected.jacobian(linear_camera_cal.focal_length))
display(camera_point_reprojected.jacobian(linear_camera_cal.principal_point))
linear_camera_cal_num = sf.LinearCameraCal(
focal_length=(440, 400),
principal_point=(320, 240),
)
# Numerical substitution for fun...
linear_camera_cal = linear_camera_cal.subs(linear_camera_cal, linear_camera_cal_num)
linear_camera = sf.Camera(
calibration=linear_camera_cal,
image_size=(640, 480),
)
print("Linear Camera: ")
display(linear_camera)