Skip to content

Commit 557b57e

Browse files
author
Felix Exner (fexner)
authored
Fix calibration (#1017)
* Make calibration tests parametrizable * Add two more real-world configurations to tests Note: the second one fails with our current implementation * Fix calibration correction There was an error from using std::atan instead of std::atan2. In most cases that seemed to work fine, but with certain calibrations the calculated angle could end up in another quadrant, so atan was returning the wrong angle. We renamed a lot of variables and changed some of the docstrings in order to hopefully make things more understandable in the future. * Add robot model to calibration tests * Add documentation to calibration algorithm * Add a comment on test suite parametrization
1 parent 4c2971d commit 557b57e

File tree

10 files changed

+354
-133
lines changed

10 files changed

+354
-133
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ repos:
116116
stages: [commit]
117117
entry: ament_copyright
118118
language: system
119-
args: ['--exclude', 'ur_robot_driver/doc/conf.py']
119+
args: ['--exclude', 'ur_robot_driver/doc/conf.py', 'ur_calibration/doc/conf.py']
120120

121121
# Docs - RestructuredText hooks
122122
- repo: https://github.com/PyCQA/doc8

ur_calibration/doc/algorithm.rst

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
Calibration correction algorithm
2+
================================
3+
4+
When extracting the robot's calibration there will be a set of Denavit–Hartenberg (DH) parameters
5+
describing the robot. Due to the calibration process they can seem a bit unintuitive since the
6+
``d``-parameter of the second and third joint can be quite large on those.
7+
8+
For example, let's consider the following dh parameters taken from a real robot:
9+
10+
.. code-block:: yaml
11+
12+
# joint1 joint2 joint3 joint4 joint5 joint6
13+
dh_theta: [-2.4147806894359e-07 1.60233952386695 -1.68607190752171 0.0837331147700119 -1.01260355871158e-07 3.91986209186124e-08 ]
14+
dh_a: [ 2.12234865571206e-05 0.0193171326277006 -0.569251663611088 -4.61409023720934e-05 -6.39280053471802e-05 0 ]
15+
dh_d: [ 0.180539811714259 439.140974079901 -446.027059806332 7.0603368964236 0.119811341150314 0.115670917257426 ]
16+
dh_alpha: [ 1.57014608044242 0.0013941666682559 0.00693818880325995 1.56998468543761 -1.57038520649543 0 ]
17+
18+
One can see that the upper arm is placed 439 m out of the base link with the lower arm being 7 m to
19+
the other side.
20+
21+
We can build a robot description that splits each DH segment into two links: One for ``d`` and
22+
``theta`` representing the rotational part of the joint and one for ``a`` and ``alpha``
23+
representing the "passive" part of the joint displacing the next link.
24+
:numref:`calibration_example` shows (a part of) the description matching the parameters above. The
25+
arm links are left out of the image as they are really far away.
26+
27+
.. _calibration_example:
28+
.. figure:: calibration_example.png
29+
:alt: Example of a calibrated model
30+
31+
This shows an example calibrated model when using the DH parameters directly as explained above.
32+
The two arm links are virtually displaced very far from the physical robot while the TCP ends up
33+
at the correct location again
34+
35+
36+
For explaining the correction algorithm, we will use an artificial set of DH parameters for a
37+
UR10e:
38+
39+
.. code-block:: yaml
40+
41+
# join1 joint2 joint3 joint4 joint5 joint6
42+
dh_theta: [0 0 0 0 0 0 ]
43+
dh_a: [0 -0.6127 -0.57155 0 0 0 ]
44+
dh_d: [0.1807 1 0.5 -1.32585 0.11985 0.11655]
45+
dh_alpha: [1.570796327 0.2 0 1.570796327 -1.570796327 0 ]
46+
47+
The resulting uncorrected model can be seen in :numref:`calibration_uncorrected`. The upper arm is
48+
placed 1 m to the left of the shoulder, the upper arm is placed 0.5 m further out and there's an
49+
added ``alpha`` rotation in joint2.
50+
51+
Note: This is not a valid calibration, so when placing this "calibration" on a robot and using the
52+
correction, we won't get correct tcp pose results. This only serves as a exaggerated example.
53+
54+
.. _calibration_uncorrected:
55+
.. figure:: calibration_uncorrected.png
56+
:alt: Exaggerated calibrated model
57+
58+
This shows an artificial calibration only to show the algorithm. This is no valid calibration!
59+
60+
In :numref:`calibration_uncorrected` the separation between the two DH components can be seen quite
61+
clearly. joint2's ``d`` and ``theta`` parameters are represented by ``upper_arm_d`` and its ``a``
62+
and ``alpha`` parameters result in ``upper_arm_a``.
63+
64+
The "correction" step tries to bring the two arm segments back to their physical representation.
65+
In principle, the d parameter to zero, first. With this change,
66+
the kinematic structure gets destroyed, which has to be corrected:
67+
68+
- With setting ``d`` to 0, both the start (``upper_arm_d``) and end (``upper_arm_a``) points of the
69+
passive segment move along the joint's rotational axis. Instead, the end point of the passive
70+
segment has to move along the rotational axis of the next segment. This requires adapting
71+
``a`` and ``theta``, if the two rotational axes are not parallel.
72+
73+
- The length of moving along the next segment's rotational axis is calculated by intersecting
74+
the next rotational axis with the XY-plane of the moved ``_d`` frame. This gets subtracted from
75+
the next joint's ``d`` parameter.
76+
77+
Note that the parameters from this model are not strict DH parameters anymore, as the two frames at
78+
the tip of the upper and lower arm have to get an additional rotation to compensate the change of
79+
the arm segment orientation, when the tip is moving along its rotation axis.
80+
81+
The resulting "DH parameters" are then reassembled into six individual transforms that can become
82+
the six frames of the robot's kinematic chain. This is exported in a yaml representation and gets
83+
read by the description package.
84+
85+
Also, no correction of the visual meshes is performed. Strictly speaking, the visual
86+
model is not 100 % correct, but with a calibrated model and ideal meshes this cannot be achieved and
87+
the inaccuracies introduced by this are considered negligible.
88+
89+
The example as visualized in :numref:`calibration_example` looks as follows if a description with
90+
the correct parameters is loaded:
91+
92+
.. figure:: calibration_example_corrected.png
93+
:alt: Example with corrected kinematics structure
94+
95+
This shows the model from :numref:`calibration_example` with the calibration correction applied.
96+
The robot is correctly assembled and the ``base->tool0`` transformation is exactly the same as
97+
on the robot controller.
39.8 KB
Loading
98 KB
Loading
209 KB
Loading

ur_calibration/doc/conf.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
project = "ur_calibration"
2+
copyright = "2022, Universal Robots A/S"
3+
author = "Felix Exner"
4+
5+
# The short X.Y version
6+
version = ""
7+
# The full version, including alpha/beta/rc tags
8+
release = ""
9+
10+
# -- General configuration ---------------------------------------------------
11+
12+
# If your documentation needs a minimal Sphinx version, state it here.
13+
#
14+
# needs_sphinx = '1.0'
15+
16+
# Add any Sphinx extension module names here, as strings. They can be
17+
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
18+
# ones.
19+
extensions = []
20+
21+
# Add any paths that contain templates here, relative to this directory.
22+
templates_path = ["_templates"]
23+
24+
# The suffix(es) of source filenames.
25+
# You can specify multiple suffix as a list of string:
26+
#
27+
source_suffix = [".rst"]
28+
29+
# The master toctree document.
30+
master_doc = "index"
31+
32+
numfig = True
33+
34+
# The language for content autogenerated by Sphinx. Refer to documentation
35+
# for a list of supported languages.
36+
#
37+
# This is also used if you do content translation via gettext catalogs.
38+
# Usually you set "language" from the command line for these cases.
39+
language = None
40+
41+
# List of patterns, relative to source directory, that match files and
42+
# directories to ignore when looking for source files.
43+
# This pattern also affects html_static_path and html_extra_path.
44+
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
45+
46+
# The name of the Pygments (syntax highlighting) style to use.
47+
pygments_style = None
48+
49+
50+
# -- Options for HTML output -------------------------------------------------
51+
52+
# The theme to use for HTML and HTML Help pages. See the documentation for
53+
# a list of builtin themes.
54+
#
55+
html_theme = "alabaster"
56+
57+
# Theme options are theme-specific and customize the look and feel of a theme
58+
# further. For a list of options available for each theme, see the
59+
# documentation.
60+
#
61+
# html_theme_options = {}
62+
63+
# Add any paths that contain custom static files (such as style sheets) here,
64+
# relative to this directory. They are copied after the builtin static files,
65+
# so a file named "default.css" will overwrite the builtin "default.css".
66+
html_static_path = ["_static"]
67+
68+
# Custom sidebar templates, must be a dictionary that maps document names
69+
# to template names.
70+
#
71+
# The default sidebars (for documents that don't match any pattern) are
72+
# defined by theme itself. Builtin themes are using these templates by
73+
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
74+
# 'searchbox.html']``.
75+
#
76+
# html_sidebars = {}
77+
78+
79+
# -- Options for HTMLHelp output ---------------------------------------------
80+
81+
# Output file base name for HTML help builder.
82+
htmlhelp_basename = "ur_calibration_doc"
83+
84+
85+
# -- Options for LaTeX output ------------------------------------------------

ur_calibration/doc/index.rst

Lines changed: 4 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,36 +10,8 @@ make use of this in ROS, you first have to extract the calibration information f
1010
Though this step is not necessary, to control the robot using this driver, it is highly recommended
1111
to do so, as end effector positions might be off in the magnitude of centimeters.
1212

13-
Nodes
14-
-----
13+
.. toctree::
14+
:maxdepth: 2
1515

16-
calibration_correction
17-
^^^^^^^^^^^^^^^^^^^^^^
18-
19-
This node extracts calibration information directly from a robot, calculates the URDF correction and
20-
saves it into a .yaml file.
21-
22-
In the launch folder of the ur_calibration package is a helper script:
23-
24-
.. code-block:: bash
25-
26-
$ ros2 launch ur_calibration calibration_correction.launch.py \
27-
robot_ip:=<robot_ip> target_filename:="${HOME}/my_robot_calibration.yaml"
28-
29-
For the parameter ``robot_ip`` insert the IP address on which the ROS pc can reach the robot. As
30-
``target_filename`` provide an absolute path where the result will be saved to.
31-
32-
With that, you can launch your specific robot with the correct calibration using
33-
34-
.. code-block:: bash
35-
36-
$ ros2 launch ur_robot_driver ur_control.launch.py \
37-
ur_type:=ur5e \
38-
robot_ip:=192.168.56.101 \
39-
kinematics_params_file:="${HOME}/my_robot_calibration.yaml"
40-
41-
Adapt the robot model matching to your robot.
42-
43-
Ideally, you would create a package for your custom workcell, as explained in `the custom workcell
44-
tutorial
45-
<https://github.com/UniversalRobots/Universal_Robots_ROS2_Tutorials/blob/main/my_robot_cell/doc/start_ur_driver.rst#extract-the-calibration>`_.
16+
usage
17+
algorithm

ur_calibration/doc/usage.rst

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
Usage
2+
=====
3+
4+
To use the calibration correction this package provides a launchfile that extracts calibration
5+
information directly from a robot, calculates the URDF correction and saves it into a .yaml file:
6+
7+
.. code-block:: bash
8+
9+
$ ros2 launch ur_calibration calibration_correction.launch.py \
10+
robot_ip:=<robot_ip> target_filename:="${HOME}/my_robot_calibration.yaml"
11+
12+
For the parameter ``robot_ip`` insert the IP address on which the ROS pc can reach the robot. As
13+
``target_filename`` provide an absolute path where the result will be saved to.
14+
15+
With that, you can launch your specific robot with the correct calibration using
16+
17+
.. code-block:: bash
18+
19+
$ ros2 launch ur_robot_driver ur_control.launch.py \
20+
ur_type:=ur5e \
21+
robot_ip:=192.168.56.101 \
22+
kinematics_params_file:="${HOME}/my_robot_calibration.yaml"
23+
24+
Adapt the robot model matching to your robot.
25+
26+
Ideally, you would create a package for your custom workcell, as explained in `the custom workcell
27+
tutorial
28+
<https://github.com/UniversalRobots/Universal_Robots_ROS2_Tutorials/blob/main/my_robot_cell/doc/start_ur_driver.rst#extract-the-calibration>`_.

ur_calibration/src/calibration.cpp

Lines changed: 49 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -73,72 +73,87 @@ void Calibration::correctAxis(const size_t link_index)
7373
// the kinematic structure gets destroyed, which has to be corrected:
7474
// - With setting d to 0, both the start and end points of the passive segment move along the
7575
// rotational axis of the start segment. Instead, the end point of the passive segment has to
76-
// move along the rotational axis of the next segment. This creates a change in a and and theta, if
76+
// move along the rotational axis of the next segment. This creates a change in a and theta, if
7777
// the two rotational axes are not parallel.
7878
//
7979
// - The length of moving along the next segment's rotational axis is calculated by intersecting
8080
// the rotational axis with the XY-plane of the first segment.
81+
//
82+
auto& d_theta_segment = chain_[2 * link_index];
83+
auto& a_alpha_segment = chain_[2 * link_index + 1];
84+
85+
auto& d = d_theta_segment(2, 3);
86+
auto& a = a_alpha_segment(0, 3);
8187

82-
if (chain_[2 * link_index](2, 3) == 0.0) {
88+
if (d == 0.0) {
8389
// Nothing to do here.
8490
return;
8591
}
8692

87-
Eigen::Matrix4d fk_current = Eigen::Matrix4d::Identity();
88-
Eigen::Vector3d current_passive = Eigen::Vector3d::Zero();
93+
// Start of the next joint's d_theta segment relative to the joint before the current one
94+
Eigen::Matrix4d next_joint_root = Eigen::Matrix4d::Identity();
95+
next_joint_root *= d_theta_segment * a_alpha_segment;
8996

90-
Eigen::Matrix4d fk_next_passive = Eigen::Matrix4d::Identity();
91-
fk_next_passive *= chain_[link_index * 2] * chain_[link_index * 2 + 1];
92-
Eigen::Vector3d eigen_passive = fk_next_passive.topRightCorner(3, 1);
97+
Eigen::Vector3d next_root_position = next_joint_root.topRightCorner(3, 1);
9398

94-
Eigen::Vector3d eigen_next = (fk_next_passive * chain_[(link_index + 1) * 2]).topRightCorner(3, 1);
99+
const auto& next_d_theta_segment = chain_[(link_index + 1) * 2];
100+
Eigen::Vector3d next_d_theta_end = (next_joint_root * next_d_theta_segment).topRightCorner(3, 1);
95101

96102
// Construct a representation of the next segment's rotational axis
97-
Eigen::ParametrizedLine<double, 3> next_line;
98-
next_line = Eigen::ParametrizedLine<double, 3>::Through(eigen_passive, eigen_next);
103+
Eigen::ParametrizedLine<double, 3> next_rotation_axis;
104+
next_rotation_axis = Eigen::ParametrizedLine<double, 3>::Through(next_root_position, next_d_theta_end);
99105

100-
RCLCPP_DEBUG_STREAM(rclcpp::get_logger("ur_calibration"), "next_line:" << std::endl
101-
<< "Base:" << std::endl
102-
<< next_line.origin() << std::endl
103-
<< "Direction:" << std::endl
104-
<< next_line.direction());
106+
RCLCPP_DEBUG_STREAM(rclcpp::get_logger("ur_calibration"), "next rotation axis:" << std::endl
107+
<< "Base:" << std::endl
108+
<< next_rotation_axis.origin()
109+
<< std::endl
110+
<< "Direction:" << std::endl
111+
<< next_rotation_axis.direction());
105112

106113
// XY-Plane of first segment's start
107-
Eigen::Hyperplane<double, 3> plane(fk_current.topLeftCorner(3, 3) * Eigen::Vector3d(0, 0, 1), current_passive);
108-
109-
// Intersect the rotation axis with the XY-Plane. We use both notations, the length and
110-
// intersection point, as we will need both. The intersection_param is the length moving along the
111-
// plane (change in the next segment's d param), while the intersection point will be used for
112-
// calculating the new angle theta.
113-
double intersection_param = next_line.intersectionParameter(plane);
114-
Eigen::Vector3d intersection = next_line.intersectionPoint(plane) - current_passive;
115-
double new_theta = std::atan(intersection.y() / intersection.x());
114+
Eigen::Hyperplane<double, 3> plane(Eigen::Vector3d(0, 0, 1), Eigen::Vector3d::Zero());
115+
116+
// Intersect the rotation axis of the next joint with the XY-Plane.
117+
// * The intersection_param is the length moving along the rotation axis until intersecting the plane.
118+
// * The intersection point will be used for calculating the new angle theta.
119+
double intersection_param = next_rotation_axis.intersectionParameter(plane);
120+
Eigen::Vector3d intersection_point = next_rotation_axis.intersectionPoint(plane);
121+
122+
// A non-zero a parameter will result in an intersection point at (a, 0) even without any
123+
// additional rotations. This effect has to be subtracted from the resulting theta value.
124+
double subtraction_angle = 0.0;
125+
if (std::abs(a) > 0) {
126+
// This is pi
127+
subtraction_angle = atan(1) * 4;
128+
}
129+
double new_theta = std::atan2(intersection_point.y(), intersection_point.x()) - subtraction_angle;
116130
// Upper and lower arm segments on URs all have negative length due to dh params
117-
double new_length = -1 * intersection.norm();
118-
RCLCPP_DEBUG_STREAM(rclcpp::get_logger("ur_calibration"), "Wrist line intersecting at " << std::endl << intersection);
131+
double new_link_length = -1 * intersection_point.norm();
132+
RCLCPP_DEBUG_STREAM(rclcpp::get_logger("ur_calibration"), "Next joint's rotation axis intersecting at "
133+
<< std::endl
134+
<< intersection_point);
119135
RCLCPP_DEBUG_STREAM(rclcpp::get_logger("ur_calibration"), "Angle is " << new_theta);
120-
RCLCPP_DEBUG_STREAM(rclcpp::get_logger("ur_calibration"), "Length is " << new_length);
136+
RCLCPP_DEBUG_STREAM(rclcpp::get_logger("ur_calibration"), "Length is " << new_link_length);
121137
RCLCPP_DEBUG_STREAM(rclcpp::get_logger("ur_calibration"), "Intersection param is " << intersection_param);
122138

123139
// as we move the passive segment towards the first segment, we have to move away the next segment
124140
// again, to keep the same kinematic structure.
125-
double sign_dir = next_line.direction().z() > 0 ? 1.0 : -1.0;
141+
double sign_dir = next_rotation_axis.direction().z() > 0 ? 1.0 : -1.0;
126142
double distance_correction = intersection_param * sign_dir;
127143

128144
// Set d parameter of the first segment to 0 and theta to the calculated new angle
129145
// Correct arm segment length and angle
130-
chain_[2 * link_index](2, 3) = 0.0;
131-
chain_[2 * link_index].topLeftCorner(3, 3) =
132-
Eigen::AngleAxisd(new_theta, Eigen::Vector3d::UnitZ()).toRotationMatrix();
146+
d = 0.0;
147+
d_theta_segment.topLeftCorner(3, 3) = Eigen::AngleAxisd(new_theta, Eigen::Vector3d::UnitZ()).toRotationMatrix();
133148

134149
// Correct arm segment length and angle
135-
chain_[2 * link_index + 1](0, 3) = new_length;
136-
chain_[2 * link_index + 1].topLeftCorner(3, 3) =
150+
a = new_link_length;
151+
a_alpha_segment.topLeftCorner(3, 3) =
137152
Eigen::AngleAxisd(robot_parameters_.segments_[link_index].theta_ - new_theta, Eigen::Vector3d::UnitZ())
138153
.toRotationMatrix() *
139154
Eigen::AngleAxisd(robot_parameters_.segments_[link_index].alpha_, Eigen::Vector3d::UnitX()).toRotationMatrix();
140155

141-
// Correct next joint
156+
// Correct next joint's d parameter
142157
chain_[2 * link_index + 2](2, 3) -= distance_correction;
143158
}
144159

0 commit comments

Comments
 (0)