/*
* 作成日: 2008/10/03
*/
package jp.ac.fit.asura.nao.misc;
import javax.vecmath.Matrix3f;
import javax.vecmath.SingularMatrixException;
import javax.vecmath.Vector3f;
import jp.ac.fit.asura.nao.motion.MotionUtils;
import jp.ac.fit.asura.nao.physical.RobotFrame;
import jp.ac.fit.asura.nao.physical.Robot.Frames;
import jp.ac.fit.asura.nao.sensation.FrameState;
import jp.ac.fit.asura.nao.sensation.SomaticContext;
import jp.ac.fit.asura.vecmathx.GfMatrix;
import jp.ac.fit.asura.vecmathx.GfVector;
import org.apache.log4j.Logger;
/**
* 運動学/逆運動学計算.
*
* @author sey
*
* @version $Id: Kinematics.java 717 2008-12-31 18:16:20Z sey $
*
*/
public class Kinematics {
public static float SCALE = 0.5f;
public static float LANGLE = 0.25f;
private static final Logger log = Logger.getLogger(Kinematics.class);
/**
* routeで指定した関節からヤコビ行列を計算する.
*
* @param route
* @param context
* @return
*/
public static void calculateJacobian(GfMatrix jacobian, Frames[] route,
SomaticContext context) {
log.trace("calculate jacobian");
assert route != null;
assert jacobian.getNumCol() == route.length;
assert jacobian.getNumRow() == 6;
// JointStateから取得する
FrameState endFrame = context.get(route[route.length - 1]);
Vector3f end = endFrame.getBodyPosition();
// Bodyから基準座標系への位置ベクトルを,すべての位置ベクトルに足す
// ことで,基準座標系からの絶対位置を表現する
for (int i = 0; i < route.length; i++) {
FrameState fs = context.get(route[i]);
// このフレームの座標
Vector3f pos = fs.getBodyPosition();
Vector3f deltaPos = new Vector3f(end);
deltaPos.sub(pos);
Vector3f zi = new Vector3f();
MatrixUtils.setAxis(fs.getAxisAngle(), zi);
fs.getBodyRotation().transform(zi);
jacobian.setElement(3, i, zi.x);
jacobian.setElement(4, i, zi.y);
jacobian.setElement(5, i, zi.z);
Vector3f cross = zi;
cross.cross(zi, deltaPos);
jacobian.setElement(0, i, cross.x);
jacobian.setElement(1, i, cross.y);
jacobian.setElement(2, i, cross.z);
}
}
/**
* 逆運動学の計算. Body関節を基準にtargetの姿勢を満たす関節角度を計算します.
*
* 計算に成功した場合は目標姿勢との自乗済み誤差(|err|^2)を返します.
*
* 計算に失敗した場合はSingularPostureExceptionをスローします.
*
* @param context
* @param target
* @return
* @throws SingularPostureException
*/
public static float calculateInverse(SomaticContext context,
FrameState target) throws SingularPostureException {
return calculateInverse(context, Frames.Body, target, new GfVector(
new float[] { 0.5f, 0.5f, 0.5f, 1, 1, 1 }));
}
/**
* 逆運動学の計算. 重み付き.
*
* weightを設定することで,各項目の精度を指定することが出来る.
*
* NOTE: 各weightをweight*SCALE > 1なると計算が不安定になるので注意.
*
* @param context
* @param target
* @param weightX
* @param weightY
* @param weightZ
* @param weightPitch
* @param weightYaw
* @param weightRoll
* @return
* @throws SingularPostureException
*/
public static float calculateInverse(SomaticContext context,
FrameState target, float weightX, float weightY, float weightZ,
float weightPitch, float weightYaw, float weightRoll)
throws SingularPostureException {
return calculateInverse(context, Frames.Body, target, new GfVector(
new float[] { weightX, weightY, weightZ, weightPitch,
weightYaw, weightRoll }));
}
/**
* 逆運動学の計算. Body関節を基準にtargetの姿勢を満たす関節角度を計算します.
*
* 計算に成功した場合は目標姿勢との自乗済み誤差(|err|^2)を返します.
*
* 計算に失敗した場合はSingularPostureExceptionをスローします.
*
* ランダムリセットを使わないver.
*
* @param ss
* @param id
* @param position
*/
public static float calculateInverse(SomaticContext context, Frames src,
FrameState target, GfVector weight) throws SingularPostureException {
log.debug("calculate inverse kinematics");
log.debug("target position " + target.getBodyPosition());
log.debug("target rotation " + target.getBodyRotation());
NDFilter.Float nd = new NDFilter.Float();
AverageFilter.Float avg = new AverageFilter.Float(8);
final float EPS = 1f; // 1e-3ぐらいまでが精度の限界か.
Frames f = target.getId();
Frames[] route = context.getRobot().findJointRoute(src, f);
GfVector err = new GfVector(6);
GfVector dq = new GfVector(route.length);
GfMatrix jacobi = new GfMatrix(6, route.length);
int rows = jacobi.getNumRow();
int cols = jacobi.getNumCol();
GfMatrix u = new GfMatrix(rows, rows);
GfMatrix w = new GfMatrix(rows, cols);
GfMatrix v = new GfMatrix(cols, cols);
// 繰り返し回数にけっこうブレが大きい模様.
// 数億分の一ぐらいの確率で1000回を超えることもある
for (int i = 0; i < 4096; i++) {
// 順運動学で現在の姿勢を計算
calculateForward(context);
// 目標値との差をとる
calcError(target, context.get(target.getId()), err);
err.scale(weight);
// log.trace("IK loop " + i + " error:" + err.normSquared());
float errNormSq = err.normSquared();
float diff = nd.eval(errNormSq);
// 誤差が規定内かつ計算が収束しているか.
if (errNormSq < EPS && diff >= -EPS / 8) {
// 関節値は可動域内か?
boolean inRange = true;
for (int j = 0; j < route.length; j++) {
FrameState fs = context.get(route[j]);
if (!MotionUtils.isInRange(fs.getFrame(), fs.getAngle())) {
log.debug("Clipping " + fs.getId() + ": "
+ MathUtils.toDegrees(fs.getAngle()) + " to ["
+ fs.getFrame().getMinAngleDeg() + ":"
+ fs.getFrame().getMaxAngleDeg() + "]");
inRange = false;
// 稼働域内でクリッピング
fs.getAxisAngle().angle = MotionUtils.clipping(fs
.getFrame(), fs.getAngle());
}
}
// 稼働域内であれば終了
if (inRange)
return errNormSq;
// 関節値が稼働域外であれば、丸めた結果を元に再計算する
calculateForward(context);
calcError(target, context.get(target.getId()), err);
err.scale(weight);
errNormSq = err.normSquared();
if (errNormSq < EPS) {
// 丸めた結果がそのまま使えるなら終了
log.trace("rounded");
return errNormSq;
} else if (errNormSq > 32) { // 目標値と全然違うなら最初からやり直す
setAngleRandom(context, route); // 見つかるまで無限ループする? // i = 0;
log.debug("Calculation failed. Retrying... step:" + i
+ " error:" + errNormSq);
nd.clear();
avg.clear();
continue;
}
// 丸めた結果が目標値に近いならそのまま続行する
} else if (!Float.isNaN(diff)) {
float ma = avg.eval(diff);
// log.trace(errNormSq + " d:" + diff + " ma:" + ma);
if (Math.abs(ma) < 0.125f || diff == 0) {
// 目標値と全然違うなら最初からやり直す
setAngleRandom(context, route); // 見つかるまで無限ループする? // i =
// 0;
log.debug("Calculation retrying... step:" + i + " error:"
+ errNormSq + " d:" + diff + " ma:" + ma);
nd.clear();
avg.clear();
continue;
}
}
calculateJacobian(jacobi, route, context);
assert jacobi != null && jacobi.getNumRow() == 6
&& jacobi.getNumCol() == route.length : jacobi;
// 逆運動学計算.
// Δerr[6x1] = jacobi[6xN] * dq[Nx1]
// この連立方程式をといてdqを求めなければならない. N=6であれば
// dq[Nx1] = (jacobi^-1)[Nx6] * err[6x1]
// でとける.
//
// 関節数 N > 6の場合, 何らかの制約条件によって解を絞り込む必要がある.
// 関節数 N < 6の場合は解を持たないので、
// Naoの腕や脚(HipYawPitch除く)ではN < 6となるので、厳密解は期待できない場合が多い
//
// ここでは、errに重みベクトルweightを掛け合わせることでN < 6の場合の重みを制御する
//
// SVDBackSolveで特異値分解(SVD)を使い連立一次方程式を解く.
// 連立一次方程式の解法ではLU分解(LUD), 特異値分解(SVD)による擬似逆行列, SR-Inverseなどがある.
// 実はN=6ではinvert()のほうが速い?
try {
// MatrixUtils.solve(jacobi, err, dq);
int rank = jacobi.SVD(u, w, v);
dq.SVDBackSolve(u, w, v, err);
} catch (SingularMatrixException e) {
log.error("", e);
setAngleRandom(context, route);
continue;
}
// dqを処理して関節角度に適用する
dq.scale(SCALE);
float max = Math.abs(dq.getElement(0));
for (int j = 1; j < dq.getSize(); j++) {
if (Math.abs(dq.getElement(j)) > max)
max = Math.abs(dq.getElement(j));
}
// 最大変位をLANGLEで正規化
if (max > LANGLE)
dq.scale(LANGLE / max);
for (int j = 0; j < route.length; j++) {
assert !Float.isNaN(dq.getElement(j)) : dq;
assert Math.abs(dq.getElement(j)) <= LANGLE + MathUtils.EPSf : dq;
FrameState fs = context.get(route[j]);
fs.getAxisAngle().angle += dq.getElement(j);
fs.getAxisAngle().angle = MathUtils.normalizeAnglePI(fs
.getAxisAngle().angle);
}
}
// 特異姿勢にはいった可能性がある.
log.error("Inverse kinematics failed.");
log.error("error " + err.normSquared() + " vectors:" + err);
calculateJacobian(jacobi, route, context);
log.error(jacobi);
for (FrameState fs : context.getFrames()) {
log.error(fs.getId() + " = " + MathUtils.toDegrees(fs.getAngle()));
}
throw new SingularPostureException();
}
private static void setAngleRandom(SomaticContext sc, Frames[] route) {
log.trace("Set random with route called.");
for (Frames f : route) {
FrameState fs = sc.get(f);
if (f.isJoint()) {
float max = fs.getFrame().getMaxAngle();
float min = fs.getFrame().getMinAngle();
fs.getAxisAngle().angle = MathUtils.rand(min, max);
}
}
}
/**
* ロボット座標系での二つの関節の位置と姿勢の差を返します.
*
* @param expected
* @param actual
* @param err
*/
protected static void calcError(FrameState expected, FrameState actual,
GfVector err) {
log.trace("calculate error");
assert err.getSize() == 6;
Vector3f p1 = expected.getBodyPosition();
Vector3f p2 = actual.getBodyPosition();
// 目標(expected)との位置の差をとる.
err.setElement(0, p1.x - p2.x);
err.setElement(1, p1.y - p2.y);
err.setElement(2, p1.z - p2.z);
Matrix3f r1 = expected.getBodyRotation();
Matrix3f r2 = actual.getBodyRotation();
assert MathUtils.epsEquals(r1.determinant(), 1);
assert MathUtils.epsEquals(r2.determinant(), 1);
// r2^-1は回転行列の逆(すなわち、-θ)を表す.
// r2^-1 * r1とすることで、回転角度的にはθ = r2 - r1を求めている.
// よって、rotErrはr1とr2の角度の差を表している.
Matrix3f rotErr = new Matrix3f();
// 本当は逆行列を計算するべきだが、直交行列なので転置=逆行列になるはず.
rotErr.transpose(r2);
// rotErr.invert(r2);
// assert MathUtils.epsEquals(rotErr.determinant(), 1);
rotErr.mul(r1);
// normalizeで直交性を回復...したいが、重い.
// rotErr.normalize();
// assert MathUtils.epsEquals(rotErr.determinant(), 1);
// 回転角度の差を角速度ベクトルomegaに変換する.
// omegaは1秒間でrotErr分の回転角度を実現するための角速度である.
// すなわち、回転行列を角度とみなすと、形式的にはomega = rotErr [rad/s] となる.
Vector3f omega = new Vector3f();
MatrixUtils.rot2omega(rotErr, omega);
// ω' = Rωとして、r2にあわせて角速度ベクトルomegaを回転する.
r2.transform(omega);
// 角速度ベクトルをセット
err.setElement(3, omega.x);
err.setElement(4, omega.y);
err.setElement(5, omega.z);
}
/**
* ロボット全体の順運動学の計算.
*
* @param ss
*/
public static void calculateForward(SomaticContext ss) {
log.trace("calculate forward kinematics");
// Bodyから再帰的にPositionを計算
RobotFrame rf = ss.getRobot().get(Frames.Body);
FrameState fs = ss.get(Frames.Body);
assert fs.getPosition().equals(rf.getTranslation());
// Bodyの座標をセット
fs.getRotation().set(fs.getAxisAngle());
fs.getBodyRotation().set(fs.getAxisAngle());
fs.getBodyPosition().set(fs.getPosition());
// 子フレームがあれば再帰的に計算する
if (rf.getChildren() != null)
for (RobotFrame child : rf.getChildren())
forwardKinematics(ss, child.getId());
}
/**
* 順運動学の計算. 再帰用メソッド.
*
* @param ss
* @param id
*/
private static void forwardKinematics(SomaticContext ss, Frames id) {
log.trace("calculate fk recursively");
RobotFrame rf = ss.getRobot().get(id);
FrameState fs = ss.get(id);
// Body及び親フレームは計算されていることが前提
assert id != Frames.Body && rf.getParent() != null;
// 親フレームの値
FrameState parent = ss.get(rf.getParent().getId());
Matrix3f parentRotation = parent.getBodyRotation();
Vector3f parentPosition = parent.getBodyPosition();
// このフレームの値
Matrix3f rotation = fs.getRotation();
Matrix3f robotRotation = fs.getBodyRotation();
// 回転行列をセット
rotation.set(fs.getAxisAngle());
// 親フレームからの回転行列をチェーンする
robotRotation.mul(parentRotation, rotation);
// robotRotation.normalize();
assert MathUtils.epsEquals(rotation.determinant(), 1) : rotation
.determinant();
Vector3f position = fs.getPosition();
Vector3f robotPosition = fs.getBodyPosition();
// 旋回関節のみを想定しているので、親フレームからの位置ベクトルは変化しない
assert position.equals(rf.getTranslation());
// 親フレームからの位置ベクトルをロボット座標系でのベクトルに直す
// robotPosition = parentRotation*position
parentRotation.transform(position, robotPosition);
// 親フレームの位置ベクトルと繋げてこのフレームの位置ベクトルをつくる
// robotPosition += parentPosition
robotPosition.add(parentPosition);
// 子フレームがあれば再帰的に計算する
if (rf.getChildren() != null)
for (RobotFrame child : rf.getChildren())
forwardKinematics(ss, child.getId());
}
/**
* 重心位置の計算. 結果はSomaticContextにセットされる.
*
* @param ss
*/
public static void calculateCenterOfMass(SomaticContext ss) {
log.trace("calculate center of mass");
// Bodyから再帰的にPositionを計算
RobotFrame rf = ss.getRobot().get(Frames.Body);
FrameState fs = ss.get(Frames.Body);
assert fs.getPosition().equals(rf.getTranslation());
float mass = rf.getGrossMass();
Vector3f com = new Vector3f();
if (rf.getChildren() != null)
for (RobotFrame child : rf.getChildren())
calcCenterOfMassRecursively(ss, child.getId(), com);
com.scale(1 / mass);
ss.getCenterOfMass().set(com);
}
/**
* 重心位置の計算. 再帰呼び出し用.
*
* @param ss
*/
private static void calcCenterOfMassRecursively(SomaticContext ss,
Frames id, Vector3f com) {
log.trace("calculate CoM recursively");
RobotFrame rf = ss.getRobot().get(id);
FrameState fs = ss.get(id);
Vector3f frameCom = fs.getBodyCenterOfMass();
Matrix3f mat = fs.getBodyRotation();
mat.transform(rf.getCenterOfMass(), frameCom);
frameCom.add(fs.getBodyPosition());
com.scaleAdd(rf.getMass(), frameCom, com);
// 子フレームがあれば再帰的に計算する
if (rf.getChildren() != null)
for (RobotFrame child : rf.getChildren())
calcCenterOfMassRecursively(ss, child.getId(), com);
}
}