-
Notifications
You must be signed in to change notification settings - Fork 123
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rotate position in motion model constraint #365
base: devel
Are you sure you want to change the base?
Rotate position in motion model constraint #365
Conversation
@svwilliams @ayrton04 I'm not sure how to request a review directly so just commenting here |
I'm going to tag @efernandez, as Anyway, just thinking aloud here. We have three poses:
The residual we want is effectively the
But I would also probably eschew the Eigen rotation matrix and just do it directly:
In any case, I'm not sure if it's correct for the residual to be in the frame of |
@ayrton04 The reasoning behind putting the pose error into pose1's frame is mainly due to an issue found in this PR where the covariance is not in the same frame as the error. Since these constraints are relative, its normal to assume the covariance should be in the local frame. Currently the covariance and pose error are in the global frame, which can lead to someone accidentally defining their covariance in the local frame |
I'm trying to get my head wrapped around this. "What frame is the covariance represented in?" is a perennial question in my experience. In devel right now, the residuals are computed as:
But that doesn't seem like how we would want a relative pose to work.
Both And that matches my intuition about what frame the covariance should be in. The measurement is a transform from Pose1 to Pose2. I.e. The robot moved forward 1m. Thus, the measurement is represented in the Pose1 frame. It makes certain intuitive sense for the covariance to be represented in the same frame as the measurement. |
And as an implementation note, we should be able to compute the predicted delta directly from the
|
Makes sense to me! |
// rotate the position residual into the local frame | ||
auto sin_pred_inv = std::sin(-yaw_pred); | ||
auto cos_pred_inv= std::cos(-yaw_pred); | ||
|
||
residuals[0] = cos_pred_inv * delta_position_pred_x - sin_pred_inv * delta_position_pred_y; | ||
residuals[1] = sin_pred_inv * delta_position_pred_x + cos_pred_inv * delta_position_pred_y; | ||
residuals[2] = parameters[6][0] - yaw_pred; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see why residuals[0]
and residual[1]
no longer depend on the previous pose (defined in parameters[0]
and parameters[1]
, i.e. position1_x
, position1_y
and yaw1
) and the next pose (defined in parameters[5]
and parameters[6]
, i.e. position2_x
, position2_y
and yaw2
).
Shouldn't we be computing the delta between those two and then compute the residuals as the delta between that and the predicted pose. That is:
// rotate the position residual into the local frame | |
auto sin_pred_inv = std::sin(-yaw_pred); | |
auto cos_pred_inv= std::cos(-yaw_pred); | |
residuals[0] = cos_pred_inv * delta_position_pred_x - sin_pred_inv * delta_position_pred_y; | |
residuals[1] = sin_pred_inv * delta_position_pred_x + cos_pred_inv * delta_position_pred_y; | |
residuals[2] = parameters[6][0] - yaw_pred; | |
const Eigen::Matrix<T, 2, 1> position1(parameters[0][0], parameters[0][1]); | |
const Eigen::Matrix<T, 2, 1> position2(parameters[5][0], parameters[5][1]); | |
const Eigen::Matrix<T, 2, 1> delta_position = fuse_core::rotationMatrix2D(parameters[1][0]).transpose() * (position2 - position1); | |
const T delta_yaw = parameters[6][0] - parameters[1][0]; // omitting fuse_core::wrapAngle2D because it is done later on | |
const Eigen::Matrix<T, 2, 1> delta_position_pred(delta_position_pred_x, delta_position_pred_y); | |
residuals.template head<2>() = fuse_core::rotationMatrix2D(delta_yaw).transpose() * (delta_position_pred - delta_position); | |
residuals[2] = delta_yaw - yaw_pred; // omitting fuse_core::wrapAngle2D because it is done later on |
In a way, this is similar to
fuse/fuse_constraints/include/fuse_constraints/normal_delta_pose_2d_cost_functor.h
Lines 118 to 125 in 2c652c7
Eigen::Map<const Eigen::Matrix<T, 2, 1>> position1_vector(position1); | |
Eigen::Map<const Eigen::Matrix<T, 2, 1>> position2_vector(position2); | |
Eigen::Matrix<T, 3, 1> full_residuals_vector; | |
full_residuals_vector.template head<2>() = | |
fuse_core::rotationMatrix2D(orientation1[0]).transpose() * (position2_vector - position1_vector) - | |
b_.head<2>().template cast<T>(); | |
full_residuals_vector(2) = fuse_core::wrapAngle2D(orientation2[0] - orientation1[0] - T(b_(2))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case, I feel it would just be better to keep what I originally had, and just rotate the error back into the local frame
@@ -147,27 +145,36 @@ bool Unicycle2DStateCostFunctor::operator()( | |||
const T* const acc_linear2, | |||
T* residual) const | |||
{ | |||
T position_pred[2]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe the comment I made above applies here as well. Indeed, I suspect with your change the compiler must be complaining of unused arguments, isn't it?
@@ -128,7 +128,7 @@ TEST(CostFunction, evaluateCostFunction) | |||
|
|||
for (size_t i = 0; i < num_parameter_blocks; ++i) | |||
{ | |||
EXPECT_MATRIX_NEAR(J_autodiff[i], J[i], std::numeric_limits<double>::epsilon()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was probably too strict, but if we change the cost function or functor, so the residuals are computed differently, I'd expect the analytic jacobians are also computed differently. So I'm afraid that even a small error here is actually a consequence of an incorrect analytic jacobian. Maybe the test values are hiding that because they don't have a strong contribution to the jacobians that should've probably been updated to account for the residuals computation changes. 🤔
@@ -99,7 +99,17 @@ TEST(CostFunction, evaluateCostFunction) | |||
|
|||
// Check jacobians are correct using a gradient checker | |||
ceres::NumericDiffOptions numeric_diff_options; | |||
ceres::GradientChecker gradient_checker(&cost_function, nullptr, numeric_diff_options); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like a change from another MR
If possible, get rid of this change here
const Eigen::Matrix<T, 2, 1> position1_map(position1[0], position1[1]); | ||
const Eigen::Matrix<T, 2, 1> position2_map(position2[0], position2[1]); | ||
const Eigen::Matrix<T, 2, 1> delta_position = fuse_core::rotationMatrix2D(-yaw1[0]) * (position2_map - position1_map); | ||
const T delta_yaw = yaw2[0] - yaw1[0]; // omitting fuse_core::wrapAngle2D because it is done later on | ||
const Eigen::Matrix<T, 2, 1> delta_position_pred_map(delta_position_pred[0], delta_position_pred[1]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These aren't really Eigen maps, but I just realized you can use position1
because it's already taken.
Just highlighting that. I don't really have a better option, so it's ok for me. Maybe others have a better suggestion 🤔
init_position[0] = (T)0.0; | ||
init_position[1] = (T)0.0; | ||
init_yaw[0] = (T)0.0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe it's more common to see the following:
init_position[0] = (T)0.0; | |
init_position[1] = (T)0.0; | |
init_yaw[0] = (T)0.0; | |
init_position[0] = T(0.0); | |
init_position[1] = T(0.0); | |
init_yaw[0] = T(0.0); |
This now LGTM. I just made some minor comments. Does this look good to you now @svwilliams @ayrton04 ? Does it align with what you mentioned before @svwilliams ? |
double position_pred_x; | ||
double position_pred_y; | ||
double delta_position_pred_x; | ||
double delta_position_pred_y; | ||
double yaw_pred; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be renamed to delta_yaw_pred
to be consistent with the other variables
const Eigen::Vector2d delta_position_pred(delta_position_pred_x, delta_position_pred_y); | ||
|
||
Eigen::Map<Eigen::Matrix<double, 8, 1>> residuals_map(residuals); | ||
residuals_map.head<2>() = fuse_core::rotationMatrix2D(-delta_yaw) * (delta_position - delta_position_pred); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand why the difference of position changes are being transformed into....is this the Pose2 frame?
I feel like this should simply be:
residuals_map(0) = delta_position(0) - delta_position_pred(0);
residuals_map(1) = delta_position(1) - delta_position_pred(1);
Both delta_position
and delta_position_pred
are represented in the Pose1 frame. If we just subtract the positions, that error will also be in the Pose1 frame/orientaiton. And that seems like the natural frame to compute the measurement covariance in as well.
residuals[5] = parameters[8][0] - vel_yaw_pred; | ||
residuals[6] = parameters[9][0] - acc_linear_pred_x; | ||
residuals[7] = parameters[9][1] - acc_linear_pred_y; | ||
const double delta_yaw = parameters[6][0] - parameters[1][0]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be consistent with naming
const double delta_yaw = parameters[6][0] - parameters[1][0]; | |
const double delta_yaw_est = parameters[6][0] - parameters[1][0]; |
const fuse_core::Matrix2d R_delta_yaw_inv = fuse_core::rotationMatrix2D(-delta_yaw); | ||
|
||
Eigen::Map<Eigen::Matrix<double, 8, 1>> residuals_map(residuals); | ||
residuals_map.head<2>() = R_delta_yaw_inv * (delta_position_pred - delta_position_est); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The delta_position_est
is computed as Pose1^1 * Pose2. That effectively transforms Pose2 into the frame of Pose1.
Similarly, delta_position_pred
is computed with Pose1 as the origin. So that quantity is also in the frame of Pose1.
If we did a simple subtraction, residual[0] = delta_position_est.x - delta_position_prod.x
, then the residual would also be represented in the frame of Pose1. Which seems like the natural frame to compute the measurement covariance as well.
Here you are rotating the error into the ... Pose2 frame?
I feel like this should just be:
residuals_map(0) = delta_position_est.x - delta_position_pred.x;
residuals_map(1) = delta_position_est.y - delta_position_pred.y;
residuals_map(2) = delta_yaw_est - delta_yaw_pred;
...
But that is not a firm belief. I could be convinced otherwise.
@svwilliams @efernandez what did we conclude on this one? |
@ayrton04 - @jakemclaughlin6 still has to address @svwilliams comments, but this is a low priority task for us atm. Other than that, I believe we're all happy with the changes proposed in this PR. |
I got a little stuck on computing the manual jacobians for the new cost functor so I put this on the backburner for a bit until I have time to get back to it |
@ayrton04 @svwilliams @efernandez I fixed the manual jacobians and the tests are passing. Let me know if we would want more tests and I can make some more, but the existing one should be fairly comprehensive as its running 100 randomized cases |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That Jacobians look way simpler. I haven't really look at the math, but if the test is passing now, that should be because they're good now.
I just made a few suggestion to simplify the code a bit more, since some operations in the Jacobians are also done for the residuals computation.
fuse_models/include/fuse_models/unicycle_2d_state_cost_function.h
Outdated
Show resolved
Hide resolved
fuse_models/include/fuse_models/unicycle_2d_state_cost_function.h
Outdated
Show resolved
Hide resolved
fuse_models/include/fuse_models/unicycle_2d_state_cost_function.h
Outdated
Show resolved
Hide resolved
fuse_models/include/fuse_models/unicycle_2d_state_cost_function.h
Outdated
Show resolved
Hide resolved
fuse_models/include/fuse_models/unicycle_2d_state_cost_function.h
Outdated
Show resolved
Hide resolved
fuse_models/include/fuse_models/unicycle_2d_state_cost_functor.h
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM - just two minor comments
fuse_models/include/fuse_models/unicycle_2d_state_cost_function.h
Outdated
Show resolved
Hide resolved
fuse_models/include/fuse_models/unicycle_2d_state_cost_functor.h
Outdated
Show resolved
Hide resolved
Co-authored-by: Enrique Fernandez Perdomo <[email protected]>
@svwilliams is this good to merge, or are you guys going to do some validations with this before choosing to merge it? |
Sorry, I still want to do some validations on this PR. Or at least re-review it. |
No description provided.