boofcv.alg.geo.pose.PnPInfinitesimalPlanePoseEstimation Maven / Gradle / Ivy
/*
* Copyright (c) 2011-2018, Peter Abeles. All Rights Reserved.
*
* This file is part of BoofCV (http://boofcv.org).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package boofcv.alg.geo.pose;
import boofcv.abst.geo.Estimate1ofEpipolar;
import boofcv.factory.geo.FactoryMultiView;
import boofcv.struct.geo.AssociatedPair;
import georegression.geometry.GeometryMath_F64;
import georegression.struct.point.Point2D_F64;
import georegression.struct.point.Point3D_F64;
import georegression.struct.point.Vector3D_F64;
import georegression.struct.se.Se3_F64;
import georegression.transform.se.SePointOps_F64;
import org.ddogleg.struct.FastQueue;
import org.ejml.UtilEjml;
import org.ejml.data.DMatrix2x2;
import org.ejml.data.DMatrix3x3;
import org.ejml.data.DMatrixRMaj;
import org.ejml.dense.fixed.CommonOps_DDF2;
import org.ejml.dense.row.CommonOps_DDRM;
import org.ejml.ops.ConvertDMatrixStruct;
import java.util.List;
/**
* A plane based pose estimation algorithm [1]. Works by first finding the homography between two sets of 2D points. Then
* solve for the pose using 3D pose information of the points. Since this is plane based the 3D points must all
* lie on a plane (z=0). A minimum of 4 points is required to estimate the pose. The planar assumption enables
* it to run very fast and accurately.
*
* [1] Collins, Toby, and Adrien Bartoli. "Infinitesimal plane-based pose estimation." International journal
* of computer vision 109.3 (2014): 252-286.
*
* @author Peter Abeles
*/
public class PnPInfinitesimalPlanePoseEstimation {
// Computes the homography
Estimate1ofEpipolar estimateHomography;
// Offset required to center points in world
Point2D_F64 center = new Point2D_F64();
Vector3D_F64 center3 = new Vector3D_F64();
// Homography matrix
DMatrixRMaj H = new DMatrixRMaj(3,3);
// final pose estimates
Se3_F64 pose0 = new Se3_F64();
Se3_F64 pose1 = new Se3_F64();
double error0,error1;
//=========== Internal workspace
// J is a 2x2 matrix
// v is a 2x1 vector
DMatrix2x2 J = new DMatrix2x2();
double v1,v2;
private DMatrixRMaj K_x = new DMatrixRMaj(3,3);
DMatrixRMaj R_v = new DMatrixRMaj(3,3);
private DMatrixRMaj tmp = new DMatrixRMaj(3,3);
private DMatrix2x2 A = new DMatrix2x2();
private DMatrix2x2 AA = new DMatrix2x2();
private DMatrix2x2 B = new DMatrix2x2();
private DMatrix2x2 R22 = new DMatrix2x2();
private Vector3D_F64 l0 = new Vector3D_F64();
private Vector3D_F64 l1 = new Vector3D_F64();
private Vector3D_F64 ca = new Vector3D_F64();
// Used to solve for translation
private DMatrixRMaj W = new DMatrixRMaj(1,3);
private DMatrixRMaj WW = new DMatrixRMaj(3,3);
private DMatrixRMaj y = new DMatrixRMaj(1,1);
private DMatrixRMaj Wty = new DMatrixRMaj(1,1);
// Input adjust centering it
FastQueue pointsAdj = new FastQueue<>(AssociatedPair.class,true);
Point3D_F64 tmpP = new Point3D_F64();
public PnPInfinitesimalPlanePoseEstimation(Estimate1ofEpipolar estimateHomography) {
this.estimateHomography = estimateHomography;
}
public PnPInfinitesimalPlanePoseEstimation() {
this(FactoryMultiView.homographyTLS());
}
/**
* Estimates the transform from world coordinate system to camera given known points and observations.
* For each observation p1=World 3D location. z=0 is implicit. p2=Observed location of points in image in
* normalized image coordinates
*
* @param points List of world coordinates in 2D (p1) and normalized image coordinates (p2)
* @return true if successful or false if it fails to estimate
*/
public boolean process( List points )
{
if( points.size() < estimateHomography.getMinimumPoints())
throw new IllegalArgumentException("At least "+estimateHomography.getMinimumPoints()+" must be provided");
// center location of points in model
zeroMeanWorldPoints(points);
// make sure there are no accidental references to the original points
points = pointsAdj.toList();
if( !estimateHomography.process(points,H) )
return false;
// make sure H[2,2] == 1
CommonOps_DDRM.divide(H,H.get(2,2));
// Jacobian of pi(H[u_0 1]^T) at u_0 = 0
// pi is plane to image transform
J.a11 = H.unsafe_get(0,0) - H.unsafe_get(2,0)*H.unsafe_get(0,2);
J.a12 = H.unsafe_get(0,1) - H.unsafe_get(2,1)*H.unsafe_get(0,2);
J.a21 = H.unsafe_get(1,0) - H.unsafe_get(2,0)*H.unsafe_get(1,2);
J.a22 = H.unsafe_get(1,1) - H.unsafe_get(2,1)*H.unsafe_get(1,2);
// v = (H[0,1],H[1,2]) = pi(H[u_0 1]^T) at u_0 = 0
// projection of u_0 into normalized coordinates
v1 = H.unsafe_get(0,2);
v2 = H.unsafe_get(1,2);
// Solve for rotations
IPPE(pose0.R,pose1.R);
// Solve for translations
estimateTranslation(pose0.R,points,pose0.T);
estimateTranslation(pose1.R,points,pose1.T);
// compute the reprojection error for each pose
error0 = computeError(points,pose0);
error1 = computeError(points,pose1);
// Make sure the best pose is the first one
if( error0 > error1 ) {
double e = error0; error0 = error1; error1 = e;
Se3_F64 s = pose0;pose0 = pose1; pose1 = s;
}
// Undo centering adjustment
center3.set(-center.x,-center.y,0);
GeometryMath_F64.addMult(pose0.T,pose0.R,center3,pose0.T);
GeometryMath_F64.addMult(pose1.T,pose1.R,center3,pose1.T);
return true;
}
/**
* Computes reprojection error to select best model
*/
double computeError( List points , Se3_F64 worldToCamera ) {
double error = 0;
for (int i = 0; i < points.size(); i++) {
AssociatedPair pair = points.get(i);
tmpP.set(pair.p1.x,pair.p1.y,0);
SePointOps_F64.transform(worldToCamera,tmpP,tmpP);
error += pair.p2.distance2(tmpP.x/tmpP.z,tmpP.y/tmpP.z);
}
return Math.sqrt(error/points.size());
}
/**
* Ensure zero mean for world location. Creates a local copy of the input
*/
private void zeroMeanWorldPoints(List points) {
center.set(0,0);
pointsAdj.reset();
for (int i = 0; i < points.size(); i++) {
AssociatedPair pair = points.get(i);
Point2D_F64 p = pair.p1;
pointsAdj.grow().p2.set(pair.p2);
center.x += p.x;
center.y += p.y;
}
center.x /= points.size();
center.y /= points.size();
for (int i = 0; i < points.size(); i++) {
Point2D_F64 p = points.get(i).p1;
pointsAdj.get(i).p1.set( p.x - center.x, p.y - center.y);
}
}
/**
* Estimate's the translation given the previously found rotation
* @param R Rotation matrix
* @param T (Output) estimated translation
*/
void estimateTranslation( DMatrixRMaj R , List points , Vector3D_F64 T )
{
final int N = points.size();
W.reshape(N*2,3);
y.reshape(N*2,1);
Wty.reshape(3,1);
DMatrix3x3 Rtmp = new DMatrix3x3();
ConvertDMatrixStruct.convert(R,Rtmp);
int indexY = 0,indexW = 0;
for (int i = 0; i < N; i++) {
AssociatedPair p = points.get(i);
// rotate into camera frame
double u1 = Rtmp.a11*p.p1.x + Rtmp.a12*p.p1.y;
double u2 = Rtmp.a21*p.p1.x + Rtmp.a22*p.p1.y;
double u3 = Rtmp.a31*p.p1.x + Rtmp.a32*p.p1.y;
W.data[indexW++] = 1;
W.data[indexW++] = 0;
W.data[indexW++] = -p.p2.x;
W.data[indexW++] = 0;
W.data[indexW++] = 1;
W.data[indexW++] = -p.p2.y;
y.data[indexY++] = p.p2.x*u3 - u1;
y.data[indexY++] = p.p2.y*u3 - u2;
}
//======= Compute Pseudo Inverse
// WW = inv(W^T*W)
CommonOps_DDRM.multTransA(W,W,WW);
CommonOps_DDRM.invert(WW);
// W^T*y
CommonOps_DDRM.multTransA(W,y,Wty);
// translation = inv(W^T*W)*W^T*y
W.reshape(3,1);
CommonOps_DDRM.mult(WW,Wty,W);
T.x = W.data[0];
T.y = W.data[1];
T.z = W.data[2];
}
/**
* Solves the IPPE problem
*/
protected void IPPE( DMatrixRMaj R1 , DMatrixRMaj R2 ) {
// Equation 23 - Compute R_v from v
double norm_v = Math.sqrt(v1*v1 + v2*v2);
if( norm_v <= UtilEjml.EPS ) {
// the plane is fronto-parallel to the camera, so set the corrective rotation Rv to identity.
// There will be only one solution to pose.
CommonOps_DDRM.setIdentity(R_v);
} else {
compute_Rv();
}
// [B|0] = [I2|-v]*R_v
compute_B(B,R_v,v1,v2);
CommonOps_DDF2.invert(B,B);
// A = inv(B)*J
CommonOps_DDF2.mult(B,J,A);
// Find the largest singular value of A
double gamma = largestSingularValue2x2(A);
// Compute R22 from A
CommonOps_DDF2.scale(1.0/gamma,A,R22);
// B = I2 - R22^T * Rss
CommonOps_DDF2.setIdentity(B);
CommonOps_DDF2.multAddTransA(-1,R22,R22,B);
double b1 = Math.sqrt(B.a11);
double b2 = Math.signum(B.a12)*Math.sqrt(B.a22);
// [c;a] = [R22;b^T]*[1;0] cross [R22;b^T]*[0;1]
l0.set(R22.a11,R22.a21,b1);
l1.set(R22.a12,R22.a22,b2);
ca.cross(l0,l1); // ca = [c;a]
// This will be the solution for the two rotation matrices
// R1 = R_v*[R22, +c; b^T , a ]
constructR(R1,R_v,R22,b1,b2,ca,1,tmp);
constructR(R2,R_v,R22,b1,b2,ca,-1,tmp);
}
/**
* R = R_v*[R22, sgn*c; sgn*b^T , a ]
*/
static void constructR( DMatrixRMaj R, DMatrixRMaj R_v , DMatrix2x2 R22 ,
double b1 , double b2 , Vector3D_F64 ca ,
double sign , DMatrixRMaj tmp )
{
tmp.data[0]= R22.a11;
tmp.data[1]= R22.a12;
tmp.data[2]= sign*ca.x;
tmp.data[3]= R22.a21;
tmp.data[4]= R22.a22;
tmp.data[5]= sign*ca.y;
tmp.data[6]= sign*b1;
tmp.data[7]= sign*b2;
tmp.data[8]= ca.z;
CommonOps_DDRM.mult(R_v,tmp,R);
}
/**
* [B|0] = [I2|-v]*R_v
*/
static void compute_B(DMatrix2x2 B , DMatrixRMaj R_v , double v1 , double v2 ) {
B.a11 = R_v.data[0]+R_v.data[6]*-v1;
B.a12 = R_v.data[1]+R_v.data[7]*-v1;
B.a21 = R_v.data[3]+R_v.data[6]*-v2;
B.a22 = R_v.data[4]+R_v.data[7]*-v2;
}
double largestSingularValue2x2(DMatrix2x2 A ) {
// Possible more numerically stable version found online
// double a = A.a11,b = A.a12, c = A.a21, d = A.a22;
// double Theta = 0.5 * Math.atan2(2*a*c + 2*b*d, a*a + b*b - c*c - d*d);
// double Phi = 0.5 * Math.atan2(2*a*b + 2*c*d, a*a - b*b + c*c - d*d);
// double s11 = ( a* cos(Theta) + c*sin(Theta))*cos(Phi) + ( b*cos(Theta) + d*sin(Theta))*sin(Phi);
// double s22 = ( a*sin(Theta) - c*cos(Theta))*sin(Phi) + (-b*sin(Theta) + d*cos(Theta))*cos(Phi);
// return s11;
// Equation 22 in the paper is incorrect. It's missing the outer square root.
// When trying to figure out what was going on people mentioned that this form of the equation is prone
// to numerical cancellation
CommonOps_DDF2.multTransB(A,A,AA);
double d = AA.a11-AA.a22;
return Math.sqrt(0.5*(AA.a11+AA.a22+Math.sqrt(d*d + 4*AA.a12*AA.a12)));
}
/**
* R_v is a 3x3 matrix
* R_v = I + sin(theta)*[k]_x + (1-cos(theta))[k]_x^2
*/
void compute_Rv() {
double t = Math.sqrt(v1*v1 + v2*v2);
double s = Math.sqrt(t*t + 1);
double cosT = 1.0/s;
double sinT = Math.sqrt(1-1.0/(s*s));
K_x.data[0] = 0; K_x.data[1] = 0; K_x.data[2] = v1;
K_x.data[3] = 0; K_x.data[4] = 0; K_x.data[5] = v2;
K_x.data[6] = -v1; K_x.data[7] = -v2; K_x.data[8] = 0;
CommonOps_DDRM.divide(K_x,t);
CommonOps_DDRM.setIdentity(R_v);
CommonOps_DDRM.addEquals(R_v,sinT,K_x);
CommonOps_DDRM.multAdd(1.0-cosT,K_x,K_x,R_v);
}
public DMatrixRMaj getHomography() {
return H;
}
public int getMinimumPoints() {
return estimateHomography.getMinimumPoints();
}
public Se3_F64 getWorldToCamera0() {
return pose0;
}
public Se3_F64 getWorldToCamera1() {
return pose1;
}
public double getError0() {
return error0;
}
public double getError1() {
return error1;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy