Skip to content

Commit 94a07be

Browse files
mergify[bot]Felix Exner (fexner)
andauthored
Fix calibration (#1023)
* 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 (cherry picked from commit 557b57e) # Conflicts: # ur_calibration/doc/index.rst Co-authored-by: Felix Exner (fexner) <[email protected]>
1 parent 1baca12 commit 94a07be

File tree

10 files changed

+367
-101
lines changed

10 files changed

+367
-101
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: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
2+
ur_calibration
3+
==============
4+
5+
Package for extracting the factory calibration from a UR robot and changing it to be used by ``ur_description`` to gain a correct URDF model.
6+
7+
Each UR robot is calibrated inside the factory giving exact forward and inverse kinematics. To also
8+
make use of this in ROS, you first have to extract the calibration information from the robot.
9+
10+
Though this step is not necessary, to control the robot using this driver, it is highly recommended
11+
to do so, as end effector positions might be off in the magnitude of centimeters.
12+
13+
.. toctree::
14+
:maxdepth: 2
15+
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)