homogeneous_transformation 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +24 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +25 -0
- data/README.md +197 -0
- data/Rakefile +5 -0
- data/homogeneous_transformation.gemspec +28 -0
- data/lib/homogeneous_transformation.rb +136 -0
- data/lib/homogeneous_transformation/version.rb +3 -0
- data/tests/tests_HomogeneousTransformation.rb +234 -0
- metadata +136 -0
data/.gitignore
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
|
19
|
+
log.org
|
20
|
+
tester.rb
|
21
|
+
*~
|
22
|
+
coverage/*
|
23
|
+
doc/*
|
24
|
+
other/*
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
Copyright (c) 2016, Cory Crean
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
6
|
+
* Redistributions of source code must retain the above copyright
|
7
|
+
notice, this list of conditions and the following disclaimer.
|
8
|
+
* Redistributions in binary form must reproduce the above copyright
|
9
|
+
notice, this list of conditions and the following disclaimer in the
|
10
|
+
documentation and/or other materials provided with the distribution.
|
11
|
+
* The name(s) of the author(s) may not be used to endorse or promote
|
12
|
+
products derived from this software without specific prior written
|
13
|
+
permission.
|
14
|
+
|
15
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
16
|
+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
17
|
+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
18
|
+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CORY CREAN BE
|
19
|
+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
20
|
+
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
21
|
+
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
22
|
+
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
23
|
+
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
24
|
+
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
|
25
|
+
IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
# HomogeneousTransformation
|
2
|
+
|
3
|
+
Homogeneous transformations are common in robotics, where each of the
|
4
|
+
bodies that make up the robot has a coordinate frame associated with
|
5
|
+
it. In general, the coordinate frames for two different bodies will
|
6
|
+
have different orientations and different locations in space. If you
|
7
|
+
have a vector that describes the location of a point relative to one
|
8
|
+
reference frame, the question arises of how to find a vector that
|
9
|
+
describes the location of the same point relative to a different
|
10
|
+
reference frame. A homogeneous transformation provides a
|
11
|
+
straightforward way to perform the conversion. See the usage
|
12
|
+
instructions below for more details.
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
Add this line to your application's Gemfile:
|
17
|
+
|
18
|
+
gem 'homogeneous_transformation'
|
19
|
+
|
20
|
+
And then execute:
|
21
|
+
|
22
|
+
$ bundle
|
23
|
+
|
24
|
+
Or install it yourself as:
|
25
|
+
|
26
|
+
$ gem install homogeneous_transformation
|
27
|
+
|
28
|
+
## Usage
|
29
|
+
|
30
|
+
To use the HomogeneousTransformation class in your program, include
|
31
|
+
the following line:
|
32
|
+
|
33
|
+
```
|
34
|
+
require 'homogeneous_transformation'
|
35
|
+
```
|
36
|
+
|
37
|
+
The following typedef has bee provided for your convenience:
|
38
|
+
|
39
|
+
```
|
40
|
+
HomTran = HomogeneousTransformation
|
41
|
+
```
|
42
|
+
|
43
|
+
We will use it throughout the remainder of this readme for the sake of
|
44
|
+
brevity.
|
45
|
+
|
46
|
+
In order to initialize a HomogeneousTransformation, you need to create
|
47
|
+
a UnitQuaternion object that represents the HomTran's orientation, and
|
48
|
+
you need to provide a vector that gives the HomTran's location in
|
49
|
+
space. The [unit_quaternion
|
50
|
+
gem](https://rubygems.org/gems/unit_quaternion) is required
|
51
|
+
automatically when you require the homogeneous_transformation gem.
|
52
|
+
|
53
|
+
```
|
54
|
+
q = UnitQuaternion.fromAngleAxis(Math::PI/2, Vector[1,1,1])
|
55
|
+
t = Vector[1,2,3]
|
56
|
+
frame = HomTran.new(q, t)
|
57
|
+
=> #<HomogeneousTransformation:0x00000002595a68 @q=(0.7071067811865476, Vector[0.408248290463863, 0.408248290463863, 0.408248290463863]), @t=Vector[1, 2, 3]>
|
58
|
+
```
|
59
|
+
|
60
|
+
You can recover the orientation or translation for a given HomTran
|
61
|
+
object using the following methods:
|
62
|
+
|
63
|
+
```
|
64
|
+
frame.getQuaternion()
|
65
|
+
=> (0.7071067811865476, Vector[0.408248290463863, 0.408248290463863, 0.408248290463863])
|
66
|
+
|
67
|
+
frame.getTranslation()
|
68
|
+
=> Vector[1, 2, 3]
|
69
|
+
```
|
70
|
+
|
71
|
+
You can get the 4x4 matrix representation of the HomTran as follows:
|
72
|
+
|
73
|
+
```
|
74
|
+
frame.getMatrix()
|
75
|
+
=> Matrix[[0.3333333333333335, -0.24401693585629253, 0.9106836025229592, 1], [0.9106836025229592, 0.3333333333333335, -0.24401693585629253, 2], [-0.24401693585629253, 0.9106836025229592, 0.3333333333333335, 3], [0, 0, 0, 1]]
|
76
|
+
```
|
77
|
+
|
78
|
+
You can also set the orientation, translation, or matrix
|
79
|
+
representation for an existing HomTran object using the setQuaternion,
|
80
|
+
setTranslation, and setMatrix methods.
|
81
|
+
|
82
|
+
Now for the useful part. If you have a HomTran object that describes
|
83
|
+
the location of a child frame relative to its parent frame, and you
|
84
|
+
have a vector expressed in the child frame, you can get the
|
85
|
+
corresponding vector in the parent frame using the transform method:
|
86
|
+
|
87
|
+
```
|
88
|
+
v = Vector[1, 2, 3]
|
89
|
+
frame.transform(v)
|
90
|
+
=> Vector[3.577350269189626, 2.8452994616207485, 5.577350269189626]
|
91
|
+
```
|
92
|
+
|
93
|
+
If you want the inverse of a HomTran, for example, to transform a
|
94
|
+
vector from its representation in the parent frame to its
|
95
|
+
representation in the child frame, you can use the inverse method:
|
96
|
+
|
97
|
+
```
|
98
|
+
frame.inverse()
|
99
|
+
=> #<HomogeneousTransformation:0x000000025a7a10 @q=(0.7071067811865476, Vector[-0.408248290463863, -0.408248290463863, -0.408248290463863]), @t=Vector[-1.4226497308103743, -3.154700538379252, -1.4226497308103747]>
|
100
|
+
```
|
101
|
+
|
102
|
+
Finally, to compose two transformations described by HomTran objects,
|
103
|
+
use the multiplication operator:
|
104
|
+
|
105
|
+
```
|
106
|
+
frame2 = HomTran.new(UnitQuaternion.new(1,2,3,4), Vector[1, 0, 0])
|
107
|
+
=> #<HomogeneousTransformation:0x000000025bc820 @q=(0.18257418583505536, Vector[0.3651483716701107, 0.5477225575051661, 0.7302967433402214]), @t=Vector[1, 0, 0]>
|
108
|
+
|
109
|
+
frame2 * frame
|
110
|
+
#<HomogeneousTransformation:0x00000002334648 @q=(-0.5417209483763564, Vector[0.25819888974716115, 0.6109051323707207, 0.5163977794943223]), @t=Vector[2.7999999999999994, 2.0, 2.5999999999999996]>
|
111
|
+
```
|
112
|
+
|
113
|
+
Note that for any frame, frame * frame.inverse() (or frame.inverse() *
|
114
|
+
frame) must yield the identity transformation:
|
115
|
+
|
116
|
+
```
|
117
|
+
result = frame * frame.inverse()
|
118
|
+
=> #<HomogeneousTransformation:0x000000024c0a20 @q=(1.0, Vector[0.0, 0.0, 0.0]), @t=Vector[-4.440892098500626e-16, -4.440892098500626e-16, -8.881784197001252e-16]>
|
119
|
+
|
120
|
+
result.getMatrix()
|
121
|
+
=> Matrix[[1.0, 0.0, 0.0, -4.440892098500626e-16], [0.0, 1.0, 0.0, -4.440892098500626e-16], [0.0, 0.0, 1.0, -8.881784197001252e-16], [0, 0, 0, 1]]
|
122
|
+
```
|
123
|
+
|
124
|
+
## Advanced Features
|
125
|
+
|
126
|
+
The initialize, setQuaternion, and setTranslation methods all accept
|
127
|
+
two additional parameters: a HomTran object called rel_to, and a
|
128
|
+
boolean value called local. The value of local is only used if rel_to
|
129
|
+
is not nil.
|
130
|
+
|
131
|
+
By default, when initializing a HomTran object, or setting its
|
132
|
+
orientation or position, this class assumes that the transformation is
|
133
|
+
being specified relative to the "global" or "inertial" reference
|
134
|
+
frame, or some other common parent frame. However, if a value for
|
135
|
+
rel_to is passed to any of the methods mentioned above, the
|
136
|
+
orientation and rotation are specified relative to the rel_to frame.
|
137
|
+
|
138
|
+
If local is true (its default value), then the location and
|
139
|
+
orientation should be specified in the rel_to frame. If local is
|
140
|
+
false, they should be specified in the "global" frame (or rel_to's
|
141
|
+
parent frame).
|
142
|
+
|
143
|
+
Perhaps an example will clarify things. Consider the following frame:
|
144
|
+
|
145
|
+
```
|
146
|
+
frame1 = HomTran.new(UnitQuaternion.fromAngleAxis(Math::PI/2, Vector[0, 0, 1]), Vector[1, 0, 0])
|
147
|
+
=> #<HomogeneousTransformation:0x00000002451490 @q=(0.7071067811865476, Vector[0.0, 0.0, 0.7071067811865475]), @t=Vector[1, 0, 0]>
|
148
|
+
```
|
149
|
+
|
150
|
+
The frame is rotated by PI/2 radians about the global z-axis, and
|
151
|
+
offset by 1 unit along the global x-axis. Next, say we want to create
|
152
|
+
a new frame with the same orientation, but offset by another 1 unit
|
153
|
+
along the global x-axis. We could create such a frame as follows:
|
154
|
+
|
155
|
+
```
|
156
|
+
frame2 = HomTran.new(UnitQuaternion.new(), Vector[1, 0, 0], frame1, false)
|
157
|
+
=> #<HomogeneousTransformation:0x000000025d62c0 @q=(0.7071067811865476, Vector[0.0, 0.0, 0.7071067811865475]), @t=Vector[2, 0, 0]>
|
158
|
+
|
159
|
+
frame2.getTranslation()
|
160
|
+
=> Vector[2, 0, 0]
|
161
|
+
```
|
162
|
+
|
163
|
+
Notice that we specified [1, 0, 0] for frame2's translation, but,
|
164
|
+
since we passed in frame1 for the rel_to argument, the resulting
|
165
|
+
translation is [1, 0, 0] plus frame1's translation. Also, notice that
|
166
|
+
local was false, so we translated frame2 along the global x-axis.
|
167
|
+
|
168
|
+
Since frame1 is rotated by PI/2 about the global z-axis, frame2's
|
169
|
+
x-axis is aligned with the global y-axis. So, if we create a new
|
170
|
+
frame similar to frame2, but with local = true, we will translate it
|
171
|
+
along frame1's x-axis, which aligns with the global y-axis. So, the
|
172
|
+
resulting translation should be [1, 1, 0]. Let's try it:
|
173
|
+
|
174
|
+
```
|
175
|
+
frame3 = HomTran.new(UnitQuaternion.new(), Vector[1, 0, 0], frame1, true)
|
176
|
+
=> #<HomogeneousTransformation:0x000000025e7228 @q=(0.7071067811865476, Vector[0.0, 0.0, 0.7071067811865475]), @t=Vector[1.0000000000000002, 1.0, 0.0]>
|
177
|
+
|
178
|
+
frame3.getTranslation()
|
179
|
+
=> Vector[1.0000000000000002, 1.0, 0.0]
|
180
|
+
```
|
181
|
+
|
182
|
+
Exactly what we expected!
|
183
|
+
|
184
|
+
A note of caution: when you create a HomTran using the rel_to
|
185
|
+
parameter, this class calculates the resulting offset and orientation
|
186
|
+
relative to the "global" frame and returns the corresponding HomTran.
|
187
|
+
So, any subsequent changes to the rel_to frame will not affect the
|
188
|
+
child frame, unless you update it manually using the setQuaternion or
|
189
|
+
setTranslation methods with the rel_to parameter.
|
190
|
+
|
191
|
+
## Contributing
|
192
|
+
|
193
|
+
1. Fork it
|
194
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
195
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
196
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
197
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'homogeneous_transformation/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "homogeneous_transformation"
|
8
|
+
spec.version = HomogeneousTransformation::VERSION
|
9
|
+
spec.authors = ["Cory Crean"]
|
10
|
+
spec.email = ["cory.crean@gmail.com"]
|
11
|
+
spec.description = %q{Provides a HomogeneousTransformation class to convert back and forth between the representations of vectors in different reference frames}
|
12
|
+
spec.summary = %q{Provides a HomogeneousTransformation class}
|
13
|
+
spec.homepage = "https://github.com/ccrean/HomTran_ruby"
|
14
|
+
spec.license = "BSD"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.required_ruby_version = ">=1.9.3"
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
24
|
+
spec.add_development_dependency "rake"
|
25
|
+
spec.add_development_dependency "simplecov", "~> 0.11.2"
|
26
|
+
spec.add_development_dependency "simplecov-html", "~> 0.10.0"
|
27
|
+
spec.add_runtime_dependency "unit_quaternion", ">= 0.0.5"
|
28
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# Author:: Cory Crean (mailto:cory.crean@gmail.com)
|
2
|
+
# Copyright:: Copyright (c) 2016 Cory Crean
|
3
|
+
# License:: BSD
|
4
|
+
#
|
5
|
+
# A homogeneous transformation class, for converting between the
|
6
|
+
# representations of vectors in different reference frames.
|
7
|
+
|
8
|
+
require_relative 'homogeneous_transformation/version'
|
9
|
+
require 'unit_quaternion'
|
10
|
+
require 'matrix'
|
11
|
+
|
12
|
+
class HomogeneousTransformation
|
13
|
+
# Creates a new HomogeneousTransformation from a quaternion and a vector.
|
14
|
+
#
|
15
|
+
# Params:
|
16
|
+
# +q+:: A UnitQuaternion object representing this frame's orientation relative to the rel_to frame.
|
17
|
+
# +translation+:: A 3-vector representing this frame's translation relative to the rel_to frame.
|
18
|
+
# +rel_to+:: A HomogeneousTransformation object relative to which the current frame's orientation and translation are specified. If rel_to is nil, the orientation and translation will be specified relative to the global reference frame.
|
19
|
+
# +local+:: A boolean value. If local is false, the relative position and orientation will be measured in the global reference frame. If local is true, the relative position and orientation will be meausured in rel_to's reference frame.
|
20
|
+
def initialize(q = UnitQuaternion.new(1, 0, 0, 0),
|
21
|
+
translation = Vector[0, 0, 0],
|
22
|
+
rel_to = nil, local = true)
|
23
|
+
setQuaternion(q, rel_to, local)
|
24
|
+
setTranslation(translation, rel_to, local)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Creates a homogeneous transformation given a suitable 4x4 matrix.
|
28
|
+
#
|
29
|
+
# Params:
|
30
|
+
# +m+:: A 4x4 matrix. The upper left 3x3 submatrix must be orthonormal, and the final row must be [0, 0, 0, 1].
|
31
|
+
def self.fromMatrix(m)
|
32
|
+
h = HomogeneousTransformation.new()
|
33
|
+
h.setMatrix(m)
|
34
|
+
return h
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns the quaternion that represents the orientation of the homogeneous transformation.
|
38
|
+
def getQuaternion()
|
39
|
+
return @q
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns the 3-vector that specifies the translation of the homogeneous transformation.
|
43
|
+
def getTranslation()
|
44
|
+
return @t
|
45
|
+
end
|
46
|
+
|
47
|
+
# Sets the quaternion for the homogeneous transformation.
|
48
|
+
#
|
49
|
+
# Params:
|
50
|
+
# +q+:: A UnitQuaternion object representing the orientation of this frame.
|
51
|
+
# +rel_to+:: A HomogeneousTransformation object relative to which the orientation is specified. If rel_to is nil, the orientation will be specified relative to the global reference frame.
|
52
|
+
# +local+:: A boolean value. If local is false, the quaternion q specifies the orientation relative to rel_to as measured in the global coordinate frame. If local is true, then q specifies the orientation relative to rel_to as measured in rel_to's coordinate frame.
|
53
|
+
def setQuaternion(q, rel_to = nil, local = true)
|
54
|
+
if rel_to
|
55
|
+
if local
|
56
|
+
@q = rel_to.getQuaternion() * q
|
57
|
+
else
|
58
|
+
@q = q * rel_to.getQuaternion()
|
59
|
+
end
|
60
|
+
else
|
61
|
+
@q = q
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Sets the translation for the homogeneous transformation.
|
66
|
+
#
|
67
|
+
# Params:
|
68
|
+
# +t+:: A vector describing the frame's translation.
|
69
|
+
# +rel_to+:: A HomogeneousTransformation object relative to which the translation is specified. If rel_to is nil, the translation will be specified relative to the global reference frame.
|
70
|
+
# +local+:: A boolean value. If local is false, then t specifies the translation relative to rel_to as measured in the global coordinate frame. If local is true, then t specifies the translation relative to rel_to as measured in rel_to's coordinate frame.
|
71
|
+
def setTranslation(t, rel_to = nil, local = true)
|
72
|
+
if rel_to
|
73
|
+
if local
|
74
|
+
@t = rel_to.getTranslation() +
|
75
|
+
rel_to.getQuaternion().transform(t)
|
76
|
+
else
|
77
|
+
@t = rel_to.getTranslation() + t
|
78
|
+
end
|
79
|
+
else
|
80
|
+
@t = t
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Sets the value of the homogeneous transformation given a suitable 4x4 matrix.
|
85
|
+
#
|
86
|
+
# Params:
|
87
|
+
# +m+:: A 4x4 matrix. The upper left 3x3 submatrix must be orthonormal, and the final row must be [0, 0, 0, 1].
|
88
|
+
def setMatrix(m)
|
89
|
+
if m.row_size() != 4 or m.column_size() != 4
|
90
|
+
raise(ArgumentError, "Matrix must be 4x4")
|
91
|
+
end
|
92
|
+
if (m.row(3) - Vector[0,0,0,1]).norm() > 1e-15
|
93
|
+
raise(ArgumentError, "Final row must be [0, 0, 0, 1]")
|
94
|
+
end
|
95
|
+
@q.setRotationMatrix(m.minor(0..2, 0..2))
|
96
|
+
@t = m.column(3)[0..2]
|
97
|
+
end
|
98
|
+
|
99
|
+
# Takes a vector v representing a point in the local frame and returns the
|
100
|
+
# vector to that same point relative to the global frame.
|
101
|
+
#
|
102
|
+
# Params:
|
103
|
+
# +v+:: A vector in the local reference frame.
|
104
|
+
#
|
105
|
+
# Returns:
|
106
|
+
# The representation of v in the global reference frame.
|
107
|
+
def transform(v)
|
108
|
+
return @t + @q.transform(v)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Returns the inverse of the homogeneous transformation.
|
112
|
+
def inverse
|
113
|
+
q_inv = @q.inverse()
|
114
|
+
t_inv = q_inv.transform(-1 * @t)
|
115
|
+
return HomogeneousTransformation.new(q_inv, t_inv)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Returns the 4x4 matrix representation of the homogeneous transformation.
|
119
|
+
def getMatrix
|
120
|
+
rot_mat = @q.getRotationMatrix()
|
121
|
+
m = Matrix.columns([*rot_mat.transpose(), @t])
|
122
|
+
m = Matrix.rows([*m, [0, 0, 0, 1]])
|
123
|
+
return m
|
124
|
+
end
|
125
|
+
|
126
|
+
# Compose two homogeneous transformations.
|
127
|
+
def *(other)
|
128
|
+
q_result = self.getQuaternion() * other.getQuaternion()
|
129
|
+
t_result = self.getQuaternion().transform(other.getTranslation()) +
|
130
|
+
self.getTranslation()
|
131
|
+
result = HomogeneousTransformation.new(q_result, t_result)
|
132
|
+
return result
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
HomTran = HomogeneousTransformation
|
@@ -0,0 +1,234 @@
|
|
1
|
+
# Name: tests_HomogeneousTransformation.rb
|
2
|
+
# Description: Test cases for the HomogeneousTransformation class
|
3
|
+
# Author: Cory Crean
|
4
|
+
# E-mail: cory.crean@gmail.com
|
5
|
+
# Copyright (c) 2016, Cory Crean
|
6
|
+
|
7
|
+
require 'simplecov'
|
8
|
+
SimpleCov.start
|
9
|
+
require 'test/unit'
|
10
|
+
require 'matrix'
|
11
|
+
require 'unit_quaternion'
|
12
|
+
require_relative '../lib/homogeneous_transformation'
|
13
|
+
|
14
|
+
def isIdentityMatrix(m, tol)
|
15
|
+
for i, j in [0,1,2].product([0,1,2])
|
16
|
+
if i == j
|
17
|
+
if (m[i,j] - 1).abs > tol
|
18
|
+
return false
|
19
|
+
end
|
20
|
+
else
|
21
|
+
if m[i,j].abs > tol
|
22
|
+
return false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
return true
|
27
|
+
end
|
28
|
+
|
29
|
+
def areEqualMatrices(m1, m2, tol)
|
30
|
+
m1.zip(m2).each() do |v1, v2|
|
31
|
+
if (v1 - v2).abs() > tol
|
32
|
+
puts("error = ", (v1 - v2).abs())
|
33
|
+
return false
|
34
|
+
end
|
35
|
+
end
|
36
|
+
return true
|
37
|
+
end
|
38
|
+
|
39
|
+
def buildMatrix(q, t)
|
40
|
+
m = q.getRotationMatrix()
|
41
|
+
m = Matrix[*m.transpose(), t].transpose()
|
42
|
+
m = Matrix[*m, [0, 0, 0, 1]]
|
43
|
+
end
|
44
|
+
|
45
|
+
class TestHomogeneousTransformation < Test::Unit::TestCase
|
46
|
+
|
47
|
+
def setup
|
48
|
+
@quats = [ ::UnitQuaternion.new(1,2,3,4),
|
49
|
+
::UnitQuaternion.new(0.1, 0.01, 2.3, 4),
|
50
|
+
::UnitQuaternion.new(1234.4134, 689.6124, 134.124, 0.5),
|
51
|
+
::UnitQuaternion.new(1,1,1,1),
|
52
|
+
UnitQuaternion.new(1, 0, 0, 0),
|
53
|
+
]
|
54
|
+
@angles = [ 2*Math::PI, Math::PI, Math::PI/2, Math::PI/4,
|
55
|
+
0.5, 0.25, 0.1234, ]
|
56
|
+
@axes = [ Vector[ 1, 1, 1 ], Vector[ 1, 0, 0 ], Vector[ 0, 1, 0 ],
|
57
|
+
Vector[ 0, 0, 1 ], Vector[ 1, 2, 3 ], ]
|
58
|
+
for angle, axis in @angles.product(@axes)
|
59
|
+
@quats << UnitQuaternion.fromAngleAxis(angle, axis)
|
60
|
+
end
|
61
|
+
|
62
|
+
positions = (0..1).step(0.5).to_a
|
63
|
+
@translations = []
|
64
|
+
positions.product(positions, positions).each() do |a, b, c|
|
65
|
+
@translations << Vector[a, b, c]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_initialize
|
70
|
+
ht = HomTran.new()
|
71
|
+
assert_equal(ht.getQuaternion(), UnitQuaternion.new(1, 0, 0, 0))
|
72
|
+
assert_equal(ht.getTranslation(), Vector[0, 0, 0])
|
73
|
+
|
74
|
+
q = UnitQuaternion.new(1,2,3,4)
|
75
|
+
t = Vector[5,6,7]
|
76
|
+
ht = HomTran.new(q, t)
|
77
|
+
assert_equal(ht.getQuaternion(), q)
|
78
|
+
assert_equal(ht.getTranslation(), t)
|
79
|
+
|
80
|
+
@quats.product(@translations).each() do |q2, t2|
|
81
|
+
fr = HomTran.new(q2, t2, ht)
|
82
|
+
assert_in_delta( (fr.getQuaternion() - q * q2).norm(), 0, 1e-15)
|
83
|
+
assert_in_delta( (fr.getTranslation() - (t + q.transform(t2))).norm(),
|
84
|
+
0, 1e-15)
|
85
|
+
|
86
|
+
fr = HomTran.new(q2, t2, ht, false)
|
87
|
+
assert_in_delta( (fr.getQuaternion() - q2 * q).norm(), 0, 1e-15)
|
88
|
+
assert_in_delta( (fr.getTranslation() - (t + t2)).norm(), 0, 1e-15)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def test_setQuaternion
|
93
|
+
for q in @quats
|
94
|
+
fr1 = HomTran.new()
|
95
|
+
fr1.setQuaternion(q)
|
96
|
+
assert_equal(fr1.getQuaternion(), q)
|
97
|
+
end
|
98
|
+
for q1, q2 in @quats.product(@quats)
|
99
|
+
fr1 = HomTran.new(q1)
|
100
|
+
fr2 = HomTran.new()
|
101
|
+
fr2.setQuaternion(q2, fr1)
|
102
|
+
assert_in_delta( (fr2.getQuaternion() - q1 * q2).norm(), 0, 1e-15 )
|
103
|
+
|
104
|
+
fr2.setQuaternion(q2, fr1, false)
|
105
|
+
assert_in_delta( (fr2.getQuaternion() - q2 * q1).norm(), 0, 1e-15 )
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def test_setTranslation
|
110
|
+
@quats.product(@translations).each() do |q, t|
|
111
|
+
fr1 = HomTran.new(q)
|
112
|
+
fr1.setTranslation(t)
|
113
|
+
assert_in_delta( (fr1.getTranslation() - t).norm(), 0, 1e-15 )
|
114
|
+
|
115
|
+
fr2 = HomTran.new()
|
116
|
+
fr2.setTranslation(t, fr1)
|
117
|
+
assert_in_delta( (fr1.getTranslation() +
|
118
|
+
fr1.getQuaternion().transform(t) -
|
119
|
+
fr2.getTranslation()).norm(), 0, 1e-15 )
|
120
|
+
|
121
|
+
fr2.setTranslation(t, fr1, false)
|
122
|
+
assert_in_delta( (fr2.getTranslation - 2 * t).norm(), 0, 1e-15)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def test_transform
|
127
|
+
t = Vector[1,2,3]
|
128
|
+
fr = HomTran.new(UnitQuaternion.fromAngleAxis(Math::PI/2,
|
129
|
+
Vector[0, 0, 1]), t)
|
130
|
+
assert_in_delta( (fr.transform(Vector[2, 0, 0]) -
|
131
|
+
(t + Vector[0, 2, 0])).norm(), 0, 1e-15)
|
132
|
+
assert_in_delta( (fr.transform(Vector[0, 2, 0]) -
|
133
|
+
(t + Vector[-2, 0, 0])).norm(), 0, 1e-15)
|
134
|
+
|
135
|
+
fr = HomTran.new(UnitQuaternion.fromAngleAxis(Math::PI/2,
|
136
|
+
Vector[0, 1, 0]), t)
|
137
|
+
assert_in_delta( (fr.transform(Vector[2, 0, 0]) -
|
138
|
+
(t + Vector[0, 0, -2])).norm(), 0, 1e-15)
|
139
|
+
assert_in_delta( (fr.transform(Vector[0, 0, 2]) -
|
140
|
+
(t + Vector[2, 0, 0])).norm(), 0, 1e-15)
|
141
|
+
|
142
|
+
fr = HomTran.new(UnitQuaternion.fromAngleAxis(Math::PI/2,
|
143
|
+
Vector[1, 0, 0]),
|
144
|
+
t)
|
145
|
+
assert_in_delta( (fr.transform(Vector[2, 0, 0]) -
|
146
|
+
(t + Vector[2, 0, 0])).norm(), 0, 1e-15)
|
147
|
+
assert_in_delta( (fr.transform(Vector[0, 2, 0]) -
|
148
|
+
(t + Vector[0, 0, 2])).norm(), 0, 1e-15)
|
149
|
+
assert_in_delta( (fr.transform(Vector[0, 0, 2]) -
|
150
|
+
(t + Vector[0, -2, 0])).norm(), 0, 1e-15)
|
151
|
+
end
|
152
|
+
|
153
|
+
def test_inverse
|
154
|
+
vectors = [ Vector[1,2,3], Vector[0,0,1], Vector[0.1,0.1,0.1] ]
|
155
|
+
@quats.product(@translations).each() do |q, t|
|
156
|
+
fr = HomTran.new(q, t)
|
157
|
+
vectors.each() do |v|
|
158
|
+
transformed = fr.inverse().transform(fr.transform(v))
|
159
|
+
assert_in_delta( (v - transformed).norm(), 0, 1e-14)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def test_matrix
|
165
|
+
@quats.product(@translations).each() do |q, t|
|
166
|
+
fr = HomTran.new(q, t)
|
167
|
+
fr_inv = fr.inverse()
|
168
|
+
assert(isIdentityMatrix( fr.getMatrix() * fr_inv.getMatrix(), 1e-15))
|
169
|
+
end
|
170
|
+
|
171
|
+
@quats.product(@translations).each() do |q, t|
|
172
|
+
fr = HomTran.new()
|
173
|
+
fr.setMatrix(buildMatrix(q, t))
|
174
|
+
assert(areEqualMatrices(fr.getQuaternion().getRotationMatrix(),
|
175
|
+
q.getRotationMatrix(), 1e-15))
|
176
|
+
assert(areEqualMatrices(fr.getTranslation(), t, 1e-15))
|
177
|
+
end
|
178
|
+
|
179
|
+
@quats.product(@translations).each() do |q, t|
|
180
|
+
fr = HomTran.fromMatrix(buildMatrix(q, t))
|
181
|
+
assert(areEqualMatrices(fr.getQuaternion().getRotationMatrix(),
|
182
|
+
q.getRotationMatrix(), 1e-15))
|
183
|
+
assert(areEqualMatrices(fr.getTranslation(), t, 1e-15))
|
184
|
+
end
|
185
|
+
|
186
|
+
q = UnitQuaternion.new(1,2,3,4)
|
187
|
+
t = Vector[1,2,3]
|
188
|
+
m = buildMatrix(q, t)
|
189
|
+
m2 = Matrix[m.row(0), m.row(1), m.row(2), [0.01, 0, 0, 1]]
|
190
|
+
ht = HomTran.new()
|
191
|
+
assert_raise(ArgumentError) do
|
192
|
+
ht.setMatrix(m2)
|
193
|
+
end
|
194
|
+
assert_raise(ArgumentError) do
|
195
|
+
HomTran.fromMatrix(m2)
|
196
|
+
end
|
197
|
+
|
198
|
+
m2 = Matrix[m.row(0), m.row(1), m.row(2), [0, 0, 0, 1.0001]]
|
199
|
+
assert_raise(ArgumentError) do
|
200
|
+
ht.setMatrix(m2)
|
201
|
+
end
|
202
|
+
assert_raise(ArgumentError) do
|
203
|
+
HomTran.fromMatrix(m2)
|
204
|
+
end
|
205
|
+
|
206
|
+
m2 = Matrix[ [1, 0.001, 0, 1], [0, 1, 0, 2], [0, 0, 1, 3],
|
207
|
+
[0, 0, 0, 1] ]
|
208
|
+
assert_raise(ArgumentError) do
|
209
|
+
ht.setMatrix(m2)
|
210
|
+
end
|
211
|
+
assert_raise(ArgumentError) do
|
212
|
+
HomTran.fromMatrix(m2)
|
213
|
+
end
|
214
|
+
|
215
|
+
m2 = Matrix[ [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0] ]
|
216
|
+
assert_raise(ArgumentError) do
|
217
|
+
ht.setMatrix(m2)
|
218
|
+
end
|
219
|
+
assert_raise(ArgumentError) do
|
220
|
+
HomTran.fromMatrix(m2)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def test_multiply
|
225
|
+
parts = @quats.product(@translations)
|
226
|
+
for i in 0...(parts.length() - 1)
|
227
|
+
fr1 = HomTran.new(*parts[i])
|
228
|
+
fr2 = HomTran.new(*parts[i+1])
|
229
|
+
result = fr1 * fr2
|
230
|
+
assert(areEqualMatrices(result.getMatrix(),
|
231
|
+
fr1.getMatrix() * fr2.getMatrix(), 1e-15))
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
metadata
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: homogeneous_transformation
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Cory Crean
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2016-08-04 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.3'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.3'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: simplecov
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 0.11.2
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.11.2
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: simplecov-html
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 0.10.0
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 0.10.0
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: unit_quaternion
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 0.0.5
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 0.0.5
|
94
|
+
description: Provides a HomogeneousTransformation class to convert back and forth
|
95
|
+
between the representations of vectors in different reference frames
|
96
|
+
email:
|
97
|
+
- cory.crean@gmail.com
|
98
|
+
executables: []
|
99
|
+
extensions: []
|
100
|
+
extra_rdoc_files: []
|
101
|
+
files:
|
102
|
+
- .gitignore
|
103
|
+
- Gemfile
|
104
|
+
- LICENSE.txt
|
105
|
+
- README.md
|
106
|
+
- Rakefile
|
107
|
+
- homogeneous_transformation.gemspec
|
108
|
+
- lib/homogeneous_transformation.rb
|
109
|
+
- lib/homogeneous_transformation/version.rb
|
110
|
+
- tests/tests_HomogeneousTransformation.rb
|
111
|
+
homepage: https://github.com/ccrean/HomTran_ruby
|
112
|
+
licenses:
|
113
|
+
- BSD
|
114
|
+
post_install_message:
|
115
|
+
rdoc_options: []
|
116
|
+
require_paths:
|
117
|
+
- lib
|
118
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
119
|
+
none: false
|
120
|
+
requirements:
|
121
|
+
- - ! '>='
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: 1.9.3
|
124
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
125
|
+
none: false
|
126
|
+
requirements:
|
127
|
+
- - ! '>='
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '0'
|
130
|
+
requirements: []
|
131
|
+
rubyforge_project:
|
132
|
+
rubygems_version: 1.8.23
|
133
|
+
signing_key:
|
134
|
+
specification_version: 3
|
135
|
+
summary: Provides a HomogeneousTransformation class
|
136
|
+
test_files: []
|