-
❓ Questions and HelpHi guys, thank you for open sourcing Theseus. :) At the moment, I am struggling with finding the best way to pass the spline knots, that need to be optimized, to the error function. I tried several ways. Each reprojection residual depends on two camera poses, as I am parametrizing object points with inverse depth (i.e. I need the pose of the reference camera and of the current one in the objective). I wrote a SplineEstimator module. Each spline (one in SO3 and one in R3) has multiple knots in th.SO3 and th.Vector3. class SplineEstimator3D(nn.Module):
self.so3_spline = SO3Spline(0, 0, dt_ns=dt_ns_so3, N=N, device=self.device)
self.r3_spline = RDSpline(0, 0, dt_ns=dt_ns_r3, dim=3, N=N, device=self.device)
...
# add optim variables --> all spline knots
self.optim_vars = []
for i in range(len(self.r3_spline.knots)):
self.optim_vars.append(self.r3_spline.knots[i])
for i in range(len(self.so3_spline.knots)):
self.optim_vars.append(self.so3_spline.knots[i])
... Then I take each view in the trajectory and iterate the observed object points to gather observations and reprojection residuals. To achieve this I am passing the index variable knot_ids as an aux_vars to the error function. That help me to gather the relevant optim_vars from self.optim_vars. Maybe this is already wrong?! def add_rs_view(self, view, view_id, recon):
with torch.no_grad():
# iterate observations of that view
tracks = view.TrackIds()
aux_vars = []
# pass knot ids to optimizer,
# we unfold those in the cost function
knot_ids = torch.zeros((1,len(tracks),self.N,4)).int()
knot_us = torch.zeros((1,len(tracks),4)).int()
...
for idx, t_id in enumerate(tracks):
ref_view_id = recon.Track(t_id).ReferenceViewId()
ref_view = recon.View(ref_view_id)
img_obs_time_ns = time_util.S_TO_NS * (
view.GetTimestamp() + self.line_delay.tensor[0]*view.GetFeature(t_id).point[1])
img_ref_time_ns = time_util.S_TO_NS * (
ref_view.GetTimestamp() + self.line_delay.tensor[0]*ref_view.GetFeature(t_id).point[1])
u_so3_obs, s_so3_obs, suc1 = self._calc_time_so3(img_obs_time_ns)
u_r3_obs, s_r3_obs, suc2 = self._calc_time_r3(img_obs_time_ns)
u_so3_ref, s_so3_ref, suc3 = self._calc_time_so3(img_ref_time_ns)
u_r3_ref, s_r3_ref, suc4 = self._calc_time_r3(img_ref_time_ns)
for i in range(self.so3_spline.N):
knot_ids[0,idx,i,0] = s_so3_ref + i
knot_ids[0,idx,i,1] = s_r3_ref + i
knot_ids[0,idx,i,2] = s_so3_obs + i
knot_ids[0,idx,i,3] = s_r3_obs + i
knot_us[0,idx,0] = u_so3_ref
knot_us[0,idx,1] = u_r3_ref
knot_us[0,idx,2] = u_so3_obs
knot_us[0,idx,3] = u_r3_obs
aux_vars = [
th.Variable(tensor=knot_ids.float(), name="knot_ids_"+str_cnt),
th.Variable(tensor=knot_us.float(), name="knot_us_"+str_cnt),
...
]
cost_function = th.AutoDiffCostFunction(
self.optim_vars,
self._rs_error,
2*len(tracks),
aux_vars=aux_vars,
name="rs_repro_cost_"+str_cnt,
autograd_vectorize=True,
autograd_mode=th.AutogradMode.LOOP_BATCH,
autograd_strict=False)
self.objective.add(cost_function)
self.cnt_repro_err += 1 My error function then looks like below. def _rs_error(self, optim_vars, aux_vars):
r3_knots = optim_vars[:len(optim_vars)//2]
so3_knots = optim_vars[len(optim_vars)//2:]
s = aux_vars[0].tensor[0]
u = aux_vars[1].tensor[0]
....
# (obs, N, )
s_so3_ref = s[:,:,0].int()
s_r3_ref = s[:,:,1].int()
s_so3_obs = s[:,:,2].int()
s_r3_obs = s[:,:,3].int()
num_obs = s_so3_ref.shape[0]
u_so3_ref = u[:,0]
u_r3_ref = u[:,1]
u_so3_obs = u[:,2]
u_r3_obs = u[:,3]
all_R_refs = torch.cat([so3_knots[idx].tensor[0] for idx in s_so3_ref.flatten()],0).reshape(self.N,num_obs,3,3)
all_p_refs = torch.cat([r3_knots[idx].tensor[0] for idx in s_r3_ref.flatten()],0).reshape(self.N,num_obs,3)
all_R_obs = torch.cat([so3_knots[idx].tensor[0] for idx in s_so3_obs.flatten()],0).reshape(self.N,num_obs,3,3)
all_p_obs = torch.cat([r3_knots[idx].tensor[0] for idx in s_r3_obs.flatten()],0).reshape(self.N,num_obs,3)
# evalute reference rolling shutter pose
R_w_i_ref = self.spline_helper.evaluate_lie_vec(
all_R_refs, u_ld_ref_so3,
self.inv_dt_so3.tensor, derivatives=0, num_meas=num_obs)[0]
t_w_i_ref = self.spline_helper.evaluate_euclidean_vec(
all_p_refs, u_ld_ref_r3,
self.inv_dt_r3.tensor, derivatives=0, num_meas=num_obs)
R_w_i_obs = self.spline_helper.evaluate_lie_vec(
all_R_obs, u_ld_obs_so3,
self.inv_dt_so3.tensor, derivatives=0, num_meas=num_obs)[0]
t_w_i_obs = self.spline_helper.evaluate_euclidean_vec(
all_p_obs, u_ld_obs_r3,
self.inv_dt_r3.tensor, derivatives=0, num_meas=num_obs)
...
repro_error = (x_camera[:,:2,0] / x_camera[:,2] - obs_obs).flatten().unsqueeze(0)
return repro_error Unfortunately, the optimization is extremely slow and is not really working either.
Thanks already for reading! |
Beta Was this translation helpful? Give feedback.
Replies: 7 comments 21 replies
-
Hi @urbste. Thanks for your interest in Theseus and for reaching out! I will try to set aside to look at this tomorrow. In the mean time, do you have any of this math written or a reference that you can share? I'm not too familiar with this, and would love to understand the abstract problem better before I suggest something. |
Beta Was this translation helpful? Give feedback.
-
Hi. Thanks already for your answer. Building on this is: Steffen |
Beta Was this translation helpful? Give feedback.
-
My spline implementation is based on that C++ code: https://gitlab.com/VladyslavUsenko/basalt-headers/-/tree/master/include/basalt/spline |
Beta Was this translation helpful? Give feedback.
-
I started looking at the code and the paper, but it's going to take me some time to fully digest :) I'll also check with the rest of the team to see if someone else is already familiar with this. In the meantime, I have some questions. I downloaded your code and ran it, and noticed that all your cost functions are receiving the full set of optimization variables. Is this intentional? Typically, every cost is only a function of a subset of the optimization variables. Maybe this the reason why you are doing the complex indexing that you mentioned above that breaks vmap? Is it possible to write your problem in a way that you have separate cost functions that only receive the subset of optimization variables that they need? For example, if I understand the code correctly, your aux vars never change (I don't see them being modified by your If this rewrite is possible, I strongly suggest to do so, because it may have quite a large effect on running time. In the current version torch's autograd needs to evaluate 458 jacobians for each cost function, and a lot of these are actually zeros (in one cost function I checked only 10 were nonzero). Thus a lot of computation seems to be wasted here. This would also make your cost function cost much simpler, which should also speed up computation. Does this make sense? Let me know if anything here is unclear or if I'm misunderstanding something. PS: I'm not sure I understood the first question in your original post, you can indeed pass vars of different types as optim vars (your code does so already with SO3 and R3). Also, it looks like you are using batch size = 1, so in this case using |
Beta Was this translation helpful? Give feedback.
-
Also, another thing that should speed up things would be to use our sparse solvers. In the That being said, I still think that simplifying the objective formulation has the more room for impact. |
Beta Was this translation helpful? Give feedback.
-
Hi @urbste, thanks for the interest! Some of the details in your application are a bit hard to parse from the implementation. Would it be possible for you to share a sketch of the factor graph visualization of your objective, and the equation number from the reference paper for the objective you are implementing. A pointer to your Ceres implementation would also be helpful. For now some high-level responses to your questions in case they help:
The autodiff cost already allows this, where you can pass a sequence of
This might be related to the point above; |
Beta Was this translation helpful? Give feedback.
-
@urbste Following our conversation, I thought a bit more about this and came up with something like the mock code below. Instead of passing a function to BTW, I suspect the part that vmap doesn't like is the tensor reshaping, which I think you should also be able to avoid using class MockReprErr:
def __init__(self, so3_knot_idx, r3_knot_idx):
self.so3_knot_idx = so3_knot_idx
self.r3_knot_idx = r3_knot_idx
def __call__(self, optim_vars, aux_vars):
so3_knots_ = optim_vars[: len(optim_vars) // 2] # each (B, 3, 3)
r3_knots_ = optim_vars[len(optim_vars) // 2 :] # each (B, 3)
# Shapes will be (4, B, 3, 3) and (4, B, 3)
so3_knots_tensor_ = torch.stack(
[so3_knots[i].tensor for i in self.so3_knot_idx], dim=1)
r3_knots_tensor_ = torch.stack(
[r3_knots[i].tensor for i in self.so3_knot_idx], dim=1)
dummy_err_1_ = th.SO3(tensor=so3_knots_tensor_[:, 1, ...]).local(th.SO3()) # (B, 3)
dummy_err_2_ = th.Point3(tensor=r3_knots_tensor_[:, 1, ...]).local(th.Point3()) # (B, 3)
total_dummy_err_ = torch.cat([dummy_err_1_, dummy_err_2_], dim=1) # (B, 6)
return total_dummy_err_
obj = th.Objective()
so3_knots = []
r3_knots = []
for i in range(5):
so3_knots.append(th.SO3.rand(1))
r3_knots.append(th.Point3.rand(1))
weight = th.ScaleCostWeight(1.0)
obj.add(
th.AutoDiffCostFunction(
optim_vars=so3_knots + r3_knots,
err_fn=MockReprErr([0, 1, 2, 3], [0, 1, 2, 3]),
dim=6,
cost_weight=weight,
autograd_mode="vmap",
name="mock_repr_1",
)
)
obj.add(
th.AutoDiffCostFunction(
optim_vars=so3_knots + r3_knots,
err_fn=MockReprErr([1, 2, 3, 4], [0, 1, 2, 3]),
dim=6,
cost_weight=weight,
autograd_mode="vmap",
name="mock_repr_2",
)
)
layer = th.TheseusLayer(th.LevenbergMarquardt(obj))
layer.forward(optimizer_kwargs={"verbose": True, "damping": 0.1}) |
Beta Was this translation helpful? Give feedback.
@urbste Following our conversation, I thought a bit more about this and came up with something like the mock code below. Instead of passing a function to
err_fn
I pass a callable object that stores the indices internally, so that they don't have to be passed as aux vars. The code below works for me.BTW, I suspect the part that vmap doesn't like is the tensor reshaping, which I think you should also be able to avoid using
torch.stack
with the appropriate dimension. Let me know if the example below makes sense.