1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
|
/* Copyright (c) 2008-2025 the MRtrix3 contributors.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Covered Software is provided under this License on an "as is"
* basis, without warranty of any kind, either expressed, implied, or
* statutory, including, without limitation, warranties that the
* Covered Software is free of defects, merchantable, fit for a
* particular purpose or non-infringing.
* See the Mozilla Public License v. 2.0 for more details.
*
* For more details, see http://www.mrtrix.org/.
*/
#include <Eigen/Geometry>
#include <unsupported/Eigen/MatrixFunctions>
#include <algorithm>
#include "command.h"
#include "math/math.h"
#include "math/average_space.h"
#include "image.h"
#include "file/nifti_utils.h"
#include "transform.h"
#include "file/key_value.h"
using namespace MR;
using namespace App;
const char* operations[] = {
"invert",
"half",
"rigid",
"header",
"average",
"interpolate",
"decompose",
"align_vertices_rigid",
"align_vertices_rigid_scale",
NULL
};
void usage ()
{
AUTHOR = "Max Pietsch (maximilian.pietsch@kcl.ac.uk)";
SYNOPSIS = "Perform calculations on linear transformation matrices";
ARGUMENTS
+ Argument ("inputs", "the input(s) for the specified operation").allow_multiple()
+ Argument ("operation", "the operation to perform, one of: " + join(operations, ", ") + " (see description section for details).").type_choice (operations)
+ Argument ("output", "the output transformation matrix.").type_file_out ();
EXAMPLES
+ Example ("Invert a transformation",
"transformcalc matrix_in.txt invert matrix_out.txt",
"")
+ Example ("Calculate the matrix square root of the input transformation (halfway transformation)",
"transformcalc matrix_in.txt half matrix_out.txt",
"")
+ Example ("Calculate the rigid component of an affine input transformation",
"transformcalc affine_in.txt rigid rigid_out.txt",
"")
+ Example ("Calculate the transformation matrix from an original image and an image with modified header",
"transformcalc mov mapmovhdr header output",
"")
+ Example ("Calculate the average affine matrix of a set of input matrices",
"transformcalc input1.txt ... inputN.txt average matrix_out.txt",
"")
+ Example ("Create interpolated transformation matrix between two inputs",
"transformcalc input1.txt input2.txt interpolate matrix_out.txt",
"Based on matrix decomposition with linear interpolation of "
"translation, rotation and stretch described in: "
"Shoemake, K., Hill, M., & Duff, T. (1992). Matrix Animation and Polar Decomposition. "
"Matrix, 92, 258-264. doi:10.1.1.56.1336")
+ Example ("Decompose transformation matrix M into translation, rotation and stretch and shear (M = T * R * S)",
"transformcalc matrix_in.txt decompose matrixes_out.txt",
"The output is a key-value text file containing: "
"scaling: vector of 3 scaling factors in x, y, z direction; "
"shear: list of shear factors for xy, xz, yz axes; "
"angles: list of Euler angles about static x, y, z axes in radians in the range [0:pi]x[-pi:pi]x[-pi:pi]; "
"angle_axis: angle in radians and rotation axis; "
"translation : translation vector along x, y, z axes in mm; "
"R: composed roation matrix (R = rot_x * rot_y * rot_z); "
"S: composed scaling and shear matrix")
+ Example ("Calculate transformation that aligns two images based on sets of corresponding landmarks",
"transformcalc input moving.txt fixed.txt align_vertices_rigid rigid.txt",
"Similary, 'align_vertices_rigid_scale' produces an affine matrix (rigid and global scale). "
"Vertex coordinates are in scanner space, corresponding vertices must be stored in the same row "
"of moving.txt and fixed.txt. Requires 3 or more vertices in each file. "
"Algorithm: Kabsch 'A solution for the best rotation to relate two sets of vectors' DOI:10.1107/S0567739476001873");
}
template <typename T> int sgn(T val) {
return (T(0) < val) - (val < T(0));
}
transform_type align_corresponding_vertices (const Eigen::MatrixXd &src_vertices, const Eigen::MatrixXd &trg_vertices, bool scale) {
// this function aligns two sets of vertices which must have corresponding vertices stored in the same row
//
// scale == false --> Kabsch
// minimise (src_vertices.row(i) - M * trg_vertices.row(i) + t).squaredNorm();
//
// scale == true
// nonrigid version of Kabsch algorithm that also includes scale (not shear)
//
assert(src_vertices.rows() == trg_vertices.rows());
const size_t n = trg_vertices.rows();
if (n < 3)
throw Exception ("vertex alignment requires at least 3 points");
assert(src_vertices.cols() == trg_vertices.cols());
assert(src_vertices.cols() == 3 && "align_corresponding_vertices implemented only for 3D data");
Eigen::VectorXd trg_centre = trg_vertices.colwise().mean();
Eigen::VectorXd src_centre = src_vertices.colwise().mean();
Eigen::MatrixXd trg_centred = trg_vertices.rowwise() - trg_centre.transpose();
Eigen::MatrixXd src_centred = src_vertices.rowwise() - src_centre.transpose();
Eigen::MatrixXd cov = (src_centred.adjoint() * trg_centred) / default_type (n - 1);
Eigen::JacobiSVD<Eigen::Matrix3d> svd (cov, Eigen::ComputeFullU | Eigen::ComputeFullV);
// rotation matrix
Eigen::Matrix3d R = svd.matrixV() * svd.matrixU().transpose();
// calculate determinant of V*U^T to disambiguate rotation sign
default_type f_det = R.determinant();
Eigen::Vector3d e(1, 1, (f_det < 0)? -1 : 1);
// recompute the rotation if the determinant was negative
if (f_det < 0)
R.noalias() = svd.matrixV() * e.asDiagonal() * svd.matrixU().transpose();
// renormalize the rotation, R needs to be Matrix3d
R = Eigen::Quaterniond(R).normalized().toRotationMatrix();
if (scale) {
default_type fsq_t = 0.0;
default_type fsq_s = 0.0;
for (size_t i = 0; i < n; ++ i) {
fsq_t += trg_centred.row(i).squaredNorm();
fsq_s += src_centred.row(i).squaredNorm();
}
// calculate and apply the scale
default_type fscale = sqrt(fsq_t / fsq_s); // Umeyama: svd.singularValues().dot(e) / fsq;
DEBUG("scaling: "+str(fscale));
R *= fscale;
}
transform_type T1 = transform_type::Identity();
transform_type T2 = transform_type::Identity();
transform_type T3 = transform_type::Identity();
T1.translation() = trg_centre;
T2.linear() = R;
T3.translation() = -src_centre;
return T1 * T2 * T3;
}
void run ()
{
const size_t num_inputs = argument.size() - 2;
const int op = argument[num_inputs];
const std::string& output_path = argument.back();
switch (op) {
case 0: { // invert
if (num_inputs != 1)
throw Exception ("invert requires 1 input");
transform_type input = load_transform (argument[0]);
save_transform (input.inverse(), output_path);
break;
}
case 1: { // half
if (num_inputs != 1)
throw Exception ("half requires 1 input");
Eigen::Transform<default_type, 3, Eigen::Projective> input = load_transform (argument[0]);
transform_type output;
Eigen::Matrix<default_type, 4, 4> half = input.matrix().sqrt();
output.matrix() = half.topLeftCorner(3,4);
save_transform (output, output_path);
break;
}
case 2: { // rigid
if (num_inputs != 1)
throw Exception ("rigid requires 1 input");
transform_type input = load_transform (argument[0]);
transform_type output (input);
output.linear() = input.rotation();
save_transform (output, output_path);
break;
}
case 3: { // header
if (num_inputs != 2)
throw Exception ("header requires 2 inputs");
auto orig_header = Header::open (argument[0]);
auto modified_header = Header::open (argument[1]);
transform_type forward_transform = Transform(modified_header).voxel2scanner * Transform(orig_header).voxel2scanner.inverse();
save_transform (forward_transform.inverse(), output_path);
break;
}
case 4: { // average
if (num_inputs < 2)
throw Exception ("average requires at least 2 inputs");
transform_type transform_out;
Eigen::Transform<default_type, 3, Eigen::Projective> Tin;
Eigen::MatrixXd Min;
vector<Eigen::MatrixXd> matrices;
for (size_t i = 0; i < num_inputs; i++) {
DEBUG(str(argument[i]));
Tin = load_transform (argument[i]);
matrices.push_back(Tin.matrix());
}
Eigen::MatrixXd average_matrix;
Math::matrix_average ( matrices, average_matrix);
transform_out.matrix() = average_matrix.topLeftCorner(3,4);
save_transform (transform_out, output_path);
break;
}
case 5: { // interpolate
if (num_inputs != 3)
throw Exception ("interpolation requires 3 inputs");
transform_type transform1 = load_transform (argument[0]);
transform_type transform2 = load_transform (argument[1]);
default_type t = parse_floats(argument[2])[0];
transform_type transform_out;
if (t < 0.0 || t > 1.0)
throw Exception ("t has to be in the interval [0,1]");
Eigen::MatrixXd M1 = transform1.linear();
Eigen::MatrixXd M2 = transform2.linear();
if (sgn(M1.determinant()) != sgn(M2.determinant()))
WARN("transformation determinants have different signs");
Eigen::Matrix3d R1 = transform1.rotation();
Eigen::Matrix3d R2 = transform2.rotation();
Eigen::Quaterniond Q1(R1);
Eigen::Quaterniond Q2(R2);
Eigen::Quaterniond Qout;
// stretch (shear becomes roation and stretch)
Eigen::MatrixXd S1 = R1.transpose() * M1;
Eigen::MatrixXd S2 = R2.transpose() * M2;
if (!M1.isApprox(R1*S1))
WARN ("M1 matrix decomposition might have failed");
if (!M2.isApprox(R2*S2))
WARN ("M2 matrix decomposition might have failed");
transform_out.translation() = ((1.0 - t) * transform1.translation() + t * transform2.translation());
Qout = Q1.slerp(t, Q2);
transform_out.linear() = Qout * ((1 - t) * S1 + t * S2);
INFO("\n"+str(transform_out.matrix().format(
Eigen::IOFormat(Eigen::FullPrecision, 0, ", ", ",\n", "[", "]", "[", "]"))));
save_transform (transform_out, output_path);
break;
}
case 6: { // decompose
if (num_inputs != 1)
throw Exception ("decomposition requires 1 input");
transform_type transform = load_transform (argument[0]);
Eigen::MatrixXd M = transform.linear();
Eigen::Matrix3d R = transform.rotation();
Eigen::MatrixXd S = R.transpose() * M;
if (!M.isApprox(R*S))
WARN ("matrix decomposition might have failed");
Eigen::Vector3d euler_angles = R.eulerAngles(0, 1, 2);
assert (R.isApprox((Eigen::AngleAxisd(euler_angles[0], Eigen::Vector3d::UnitX())
* Eigen::AngleAxisd(euler_angles[1], Eigen::Vector3d::UnitY())
* Eigen::AngleAxisd(euler_angles[2], Eigen::Vector3d::UnitZ())).matrix()));
Eigen::RowVector4d angle_axis;
{
auto AA = Eigen::AngleAxis<default_type> (R);
angle_axis(0) = AA.angle();
angle_axis.block<1,3>(0,1) = AA.axis();
}
File::OFStream out (output_path);
out << "# " << App::command_history_string << "\n";
Eigen::IOFormat fmt(Eigen::FullPrecision, Eigen::DontAlignCols, " ", "\n", "", "", "", "\n");
out << "scaling: " << Eigen::RowVector3d(S(0,0), S(1,1), S(2,2)).format(fmt);
out << "shear: " << Eigen::RowVector3d(S(0,1), S(0,2), S(1,2)).format(fmt);
out << "angles: " << euler_angles.transpose().format(fmt);
out << "angle_axis: " << angle_axis.format(fmt);
out << "translation: " << transform.translation().transpose().format(fmt);
out << "R: " << R.row(0).format(fmt);
out << "R: " << R.row(1).format(fmt);
out << "R: " << R.row(2).format(fmt);
out << "S: " << S.row(0).format(fmt);
out << "S: " << S.row(1).format(fmt);
out << "S: " << S.row(2).format(fmt);
out << "jacobian_det: " << str(transform.linear().topLeftCorner<3,3>().determinant()) << "\n";
break;
}
case 7: case 8: { // align_vertices_rigid and align_vertices_rigid_scale
if (num_inputs != 2)
throw Exception ("align_vertices_rigid requires 2 inputs");
const Eigen::MatrixXd target_vertices = load_matrix (argument[0]);
const Eigen::MatrixXd moving_vertices = load_matrix (argument[1]);
const transform_type T = align_corresponding_vertices (moving_vertices, target_vertices, op==8);
save_transform (T, output_path);
break;
}
default: assert (0);
}
}
|