pl_vio线特征
pl_vio线特征
0.引言
1.LineFeatureTracker核心逻辑解读
- 数据结构定义:
struct Line {
Point2f StartPt;
Point2f EndPt;
float lineWidth;
Point2f Vp;
Point2f Center;
Point2f unitDir; // [cos(theta), sin(theta)]
float length;
float theta;
// para_a * x + para_b * y + c = 0
float para_a;
float para_b;
float para_c;
float image_dx;
float image_dy;
float line_grad_avg;
float xMin;
float xMax;
float yMin;
float yMax;
unsigned short id;
int colorIdx;
};
class FrameLines {
public:
int frame_id;
Mat img;
vector<Line> vecLine;
vector<int> lineID;
// opencv3 lsd+lbd
std::vector<KeyLine> keylsd;
Mat lbd_descr;
};
- keylsd:提取的线段结果(经过长度筛选的,太短的不要)
- keylbd_descr:对应线段描述子结果(经过长度筛选的,太短的不要)
- 新进的img提取线特征:
- 如果是第一帧img:forwframe_->lineID.push_back(allfeature_cnt++);(allfeature_cnt是整个系统全局的线特征计数),因此lineID是全局的ID,整个系统中线特征具有唯一的ID。update:后面更新的是没匹配上的。
- 如果不是第一帧img,则暂时赋值为-1,但forwframe_->lineID数组的size的大小是和keylsd相同的;
- 匹配后将-1进行更改:forwframe_->lineID[mt.queryIdx] = curframe_->lineID[mt.trainIdx];
- Key = 当前帧线段的ID
- Value = 前一帧线段的lineID[mt.trainIdx]
- 判断当前帧水平,垂直方向线段数量是否分别足够35条,不够的补上。
- 匹配后将-1进行更改:forwframe_->lineID[mt.queryIdx] = curframe_->lineID[mt.trainIdx];
匹配的结果,对应关系,其实就是存储的全局ID信息。
代码注释,可惜另一个函数的关键内容lineDetector
没放出来,这个函数的vecLine只在最后做了一个转换。frame_id没维护。
void LineFeatureTracker::readImage(const cv::Mat &_img) {
cv::Mat img;
TicToc t_p;
frame_cnt++;
// cv::remap(_img, img, undist_map1_, undist_map2_, CV_INTER_LINEAR);
cv::remap(_img, img, undist_map1_, undist_map2_, cv::INTER_LINEAR);
// cv::imshow("lineimg",img);
// cv::waitKey(1);
// ROS_INFO("undistortImage costs: %fms", t_p.toc());
if (EQUALIZE) // 直方图均衡化
{
cv::Ptr<cv::CLAHE> clahe = cv::createCLAHE(3.0, cv::Size(8, 8));
clahe->apply(img, img);
}
bool first_img = false;
if (forwframe_ == nullptr) // 系统初始化的第一帧图像
{
forwframe_.reset(new FrameLines);
curframe_.reset(new FrameLines);
forwframe_->img = img;
curframe_->img = img; // 上一帧
first_img = true;
} else {
forwframe_.reset(new FrameLines); // 初始化一个新的帧
forwframe_->img = img; // 当前帧
}
TicToc t_li;
Ptr<line_descriptor::LSDDetectorC> lsd_ =
line_descriptor::LSDDetectorC::createLSDDetectorC();
// lsd parameters
line_descriptor::LSDDetectorC::LSDOptions opts;
opts.refine = 1; // 1 The way found lines will be refined
opts.scale = 0.5; // 0.8 The scale of the image that will be used to find
// the lines. Range (0..1].
opts.sigma_scale = 0.6; // 0.6 Sigma for Gaussian filter. It is
// computed as sigma = _sigma_scale/_scale.
opts.quant =
2.0; // 2.0 Bound to the quantization error on the gradient norm
opts.ang_th = 22.5; // 22.5 Gradient angle tolerance in degrees
opts.log_eps = 1.0; // 0 Detection threshold: -log10(NFA) >
// log_eps. Used only when advance refinement is chosen
opts.density_th = 0.6; // 0.7 Minimal density of aligned region points
// in the enclosing rectangle.
opts.n_bins =
1024; // 1024 Number of bins in pseudo-ordering of gradient modulus.
double min_line_length =
0.125; // Line segments shorter than that are rejected
// opts.refine = 1;
// opts.scale = 0.5;
// opts.sigma_scale = 0.6;
// opts.quant = 2.0;
// opts.ang_th = 22.5;
// opts.log_eps = 1.0;
// opts.density_th = 0.6;
// opts.n_bins = 1024;
// double min_line_length = 0.125;
opts.min_length = min_line_length * (std::min(img.cols, img.rows));
std::vector<KeyLine> lsd, keylsd;
// void LSDDetectorC::detect( const std::vector<Mat>& images,
// std::vector<std::vector<KeyLine> >& keylines, int scale, int numOctaves,
// const std::vector<Mat>& masks ) const
lsd_->detect(img, lsd, 2, 1, opts);
// visualize_line(img,lsd);
// step 1: line extraction
// TicToc t_li;
// std::vector<KeyLine> lsd, keylsd;
// Ptr<LSDDetector> lsd_;
// lsd_ = cv::line_descriptor::LSDDetector::createLSDDetector();
// lsd_->detect( img, lsd, 2, 2 );
sum_time += t_li.toc();
ROS_INFO("line detect costs: %fms", t_li.toc());
Mat lbd_descr, keylbd_descr;
// step 2: lbd descriptor
TicToc t_lbd;
Ptr<BinaryDescriptor> bd_ = BinaryDescriptor::createBinaryDescriptor();
bd_->compute(img, lsd, lbd_descr);
// std::cout<<"lbd_descr = "<<lbd_descr.size()<<std::endl;
//
for (int i = 0; i < (int)lsd.size(); i++) {
if (lsd[i].octave == 0 && lsd[i].lineLength >= 60) { // 对长度做一个筛选
keylsd.push_back(lsd[i]);
keylbd_descr.push_back(lbd_descr.row(i));
}
}
// std::cout<<"lbd_descr = "<<lbd_descr.size()<<std::endl;
// ROS_INFO("lbd_descr detect costs: %fms", keylsd.size() * t_lbd.toc() /
// lsd.size() );
sum_time += keylsd.size() * t_lbd.toc() / lsd.size();
///
forwframe_->keylsd = keylsd; // 当前帧的关键线段
forwframe_->lbd_descr = keylbd_descr;
for (size_t i = 0; i < forwframe_->keylsd.size(); ++i) {
if (first_img)
forwframe_->lineID.push_back(allfeature_cnt++);
else
forwframe_->lineID.push_back(-1); // give a negative id
}
// if(!first_img)
// {
// std::vector<DMatch> lsd_matches;
// Ptr<BinaryDescriptorMatcher> bdm_;
// bdm_ = BinaryDescriptorMatcher::createBinaryDescriptorMatcher();
// bdm_->match(forwframe_->lbd_descr, curframe_->lbd_descr, lsd_matches);
// visualize_line_match(forwframe_->img.clone(), curframe_->img.clone(),
// forwframe_->keylsd, curframe_->keylsd, lsd_matches);
// // std::cout<<"lsd_matches = "<<lsd_matches.size()<<"
// forwframe_->keylsd = "<<keylbd_descr.size()<<" curframe_->keylsd =
// "<<keylbd_descr.size()<<std::endl;
// }
if (curframe_->keylsd.size() > 0) {
/* compute matches */
TicToc t_match;
std::vector<DMatch> lsd_matches;
Ptr<BinaryDescriptorMatcher> bdm_;
bdm_ = BinaryDescriptorMatcher::createBinaryDescriptorMatcher();
// 上一帧与当前帧进行匹配
bdm_->match(forwframe_->lbd_descr, curframe_->lbd_descr, lsd_matches);
// ROS_INFO("lbd_macht costs: %fms", t_match.toc());
sum_time += t_match.toc();
mean_time = sum_time / frame_cnt;
// ROS_INFO("line feature tracker mean costs: %fms", mean_time);
/* select best matches */
std::vector<DMatch> good_matches;
std::vector<KeyLine> good_Keylines;
good_matches.clear();
for (int i = 0; i < (int)lsd_matches.size(); i++) {
if (lsd_matches[i].distance < 30) { // 距离小于30的匹配,认为是好的匹配
DMatch mt = lsd_matches[i];
KeyLine line1 = forwframe_->keylsd[mt.queryIdx];
KeyLine line2 = curframe_->keylsd[mt.trainIdx];
Point2f serr =
line1.getStartPoint() - line2.getEndPoint(); // start error
Point2f eerr = line1.getEndPoint() - line2.getEndPoint(); // end error
// std::cout<<"11111111111111111 =
// "<<abs(line1.angle-line2.angle)<<std::endl;
// 线段在图像里不会跑得特别远,距离和角度的筛选
if ((serr.dot(serr) < 200 * 200) && (eerr.dot(eerr) < 200 * 200) &&
abs(line1.angle - line2.angle) < 0.1)
good_matches.push_back(
lsd_matches[i]); // 一个元素存储的是一对匹配关系
}
}
vector<int> success_id;
// std::cout << forwframe_->lineID.size() <<" " <<curframe_->lineID.size();
for (int k = 0; k < good_matches.size(); ++k) {
DMatch mt = good_matches[k];
// forwframe_->lineID[mt.queryIdx]
forwframe_->lineID[mt.queryIdx] = curframe_->lineID[mt.trainIdx];
success_id.push_back(curframe_->lineID[mt.trainIdx]);
}
// visualize_line_match(forwframe_->img.clone(), curframe_->img.clone(),
// forwframe_->keylsd, curframe_->keylsd, good_matches);
// 把没追踪到的线存起来
vector<KeyLine> vecLine_tracked, vecLine_new;
vector<int> lineID_tracked, lineID_new;
Mat DEscr_tracked, Descr_new;
// 将跟踪的线和没跟踪上的线进行区分
// vecLine_new、lineID_new、 Descr_new当前帧和上一帧没匹配失败的线段信息
for (size_t i = 0; i < forwframe_->keylsd.size(); ++i) {
if (forwframe_->lineID[i] == -1) {
forwframe_->lineID[i] = allfeature_cnt++;
vecLine_new.push_back(forwframe_->keylsd[i]);
lineID_new.push_back(forwframe_->lineID[i]);
Descr_new.push_back(forwframe_->lbd_descr.row(i));
}
else { // vecLine_tracked、lineID_tracked、
// DEscr_tracked当前帧和上一帧匹配成功的线段信息
vecLine_tracked.push_back(forwframe_->keylsd[i]);
lineID_tracked.push_back(forwframe_->lineID[i]);
DEscr_tracked.push_back(forwframe_->lbd_descr.row(i));
}
}
vector<KeyLine> h_Line_new, v_Line_new;
vector<int> h_lineID_new, v_lineID_new;
Mat h_Descr_new, v_Descr_new;
// 遍历匹配失败的线段,将水平线(horizontal line)和垂直(vertical
// line)线分开
for (size_t i = 0; i < vecLine_new.size(); ++i) {
// 将角度在45度到135度或者-45度到-135度之间的线认为是水平线,其他的线认为是垂直线。是不是反了?
if ((((vecLine_new[i].angle >= 3.14 / 4 &&
vecLine_new[i].angle <= 3 * 3.14 / 4)) ||
(vecLine_new[i].angle <= -3.14 / 4 &&
vecLine_new[i].angle >= -3 * 3.14 / 4))) {
h_Line_new.push_back(vecLine_new[i]);
h_lineID_new.push_back(lineID_new[i]);
h_Descr_new.push_back(Descr_new.row(i));
} else {
v_Line_new.push_back(vecLine_new[i]);
v_lineID_new.push_back(lineID_new[i]);
v_Descr_new.push_back(Descr_new.row(i));
}
}
int h_line, v_line;
h_line = v_line = 0;
// 遍历匹配成功的线段,统计水平线(horizontal line)和垂直(vertical
// line)线个数
for (size_t i = 0; i < vecLine_tracked.size(); ++i) {
if ((((vecLine_tracked[i].angle >= 3.14 / 4 &&
vecLine_tracked[i].angle <= 3 * 3.14 / 4)) ||
(vecLine_tracked[i].angle <= -3.14 / 4 &&
vecLine_tracked[i].angle >= -3 * 3.14 / 4))) {
h_line++;
} else {
v_line++;
}
}
// 计算匹配成功的线段中水平线(horizontal line)和垂直(vertical
// line)线的个数是否分别足够35个,不够的话就补充
int diff_h = 35 - h_line;
int diff_v = 35 - v_line;
// std::cout<<"h_line = "<<h_line<<" v_line = "<<v_line<<std::endl;
// 补充水平线线条
if (diff_h > 0) {
int kkk = 1;
if (diff_h > h_Line_new.size())
diff_h = h_Line_new.size();
else
kkk = int(h_Line_new.size() / diff_h);
// 作者原本估计是想随机性一点的选择,但是没实现
for (int k = 0; k < diff_h; ++k) {
vecLine_tracked.push_back(h_Line_new[k]);
lineID_tracked.push_back(h_lineID_new[k]);
DEscr_tracked.push_back(h_Descr_new.row(k));
}
// std::cout <<"h_kkk = " <<kkk<<" diff_h = "<<diff_h<<"
// h_Line_new.size() = "<<h_Line_new.size()<<std::endl;
}
// 补充垂直线条
if (diff_v > 0) {
int kkk = 1;
if (diff_v > v_Line_new.size())
diff_v = v_Line_new.size();
else
kkk = int(v_Line_new.size() / diff_v);
for (int k = 0; k < diff_v; ++k) {
vecLine_tracked.push_back(v_Line_new[k]);
lineID_tracked.push_back(v_lineID_new[k]);
DEscr_tracked.push_back(v_Descr_new.row(k));
} // std::cout <<"v_kkk = " <<kkk<<" diff_v = "<<diff_v<<"
// v_Line_new.size() = "<<v_Line_new.size()<<std::endl;
}
// int diff_n = 50 - vecLine_tracked.size(); //
// 跟踪的线特征少于50了,那就补充新的线特征, 还差多少条线 if( diff_n > 0) //
// 补充线条
// {
// for (int k = 0; k < vecLine_new.size(); ++k) {
// vecLine_tracked.push_back(vecLine_new[k]);
// lineID_tracked.push_back(lineID_new[k]);
// DEscr_tracked.push_back(Descr_new.row(k));
// }
// }
forwframe_->keylsd = vecLine_tracked;
forwframe_->lineID = lineID_tracked;
forwframe_->lbd_descr = DEscr_tracked;
}
// 将opencv的KeyLine数据转为季哥的Line
for (int j = 0; j < forwframe_->keylsd.size(); ++j) {
Line l;
KeyLine lsd = forwframe_->keylsd[j];
l.StartPt = lsd.getStartPoint();
l.EndPt = lsd.getEndPoint();
l.length = lsd.lineLength;
forwframe_->vecLine.push_back(l);
}
curframe_ = forwframe_;
}
最后forwframe_->vecLine
属性,赋值了三个
l.StartPt = lsd.getStartPoint();
l.EndPt = lsd.getEndPoint();
l.length = lsd.lineLength;
ROS发布的信息
- feature_lines->points : 线段起点
- feature_lines->channels.push_back(id_of_line); :与上一帧的匹配关系
- feature_lines->channels.push_back(u_of_endpoint);:线段终点 u 坐标
- feature_lines->channels.push_back(v_of_endpoint);:线段终点 v 坐标
在ROS中,sensor_msgs::PointCloud
是一个用于表示3D点云的消息类型。它的数据结构如下:
- std_msgs/Header header:消息头,包含时间戳和坐标帧信息。
- geometry_msgs/Point32[] points:点云中的点,每个点包含x、y和z坐标。
- std_msgs/ChannelFloat32[] channels:每个通道包含一个名字和一组与点对应的浮点数。这可以用来存储与每个点相关的额外数据,如颜色、强度等,这里存储的信息如上所述。
2.estimator_node中线段的处理
2.1.订阅信息解压
map<int, vector<pair<int, Vector4d>>> lines;
for (unsigned int i = 0; i < line_msg->points.size(); i++) {
int v = line_msg->channels[0].values[i] + 0.5;
// std::cout<< "receive id: " << v / NUM_OF_CAM << "\n";
int feature_id = v / NUM_OF_CAM;
int camera_id =
v % NUM_OF_CAM; // 被几号相机观测到的,如果是单目,camera_id = 0
double x_startpoint = line_msg->points[i].x;
double y_startpoint = line_msg->points[i].y;
double x_endpoint = line_msg->channels[1].values[i];
double y_endpoint = line_msg->channels[2].values[i];
// ROS_ASSERT(z == 1);
lines[feature_id].emplace_back(
camera_id,
Vector4d(x_startpoint, y_startpoint, x_endpoint, y_endpoint));
}
map<int, vector<pair<int, Vector4d>>> lines;
其实这里的vetor大小始终是1
- key : 与上一帧线特征的对应关系
- value : camera_id + 线段起点 + 线段终点
2.2.线特征管理
现根据之前匹配的对应关系构造出lineFeaturePerId
信息:
// 根据frame进行维护,这里是最初读取的数据
class lineFeaturePerFrame {
public:
lineFeaturePerFrame(const Vector4d &line) { lineobs = line; }
lineFeaturePerFrame(const Vector8d &line) {
lineobs = line.head<4>();
lineobs_R = line.tail<4>();
}
Vector4d lineobs; // 每一帧上的观测
Vector4d lineobs_R;
double z;
bool is_used;
double parallax;
MatrixXd A;
VectorXd b;
double dep_gradient;
};
// 根据id进行维护,该id的线段可能被多帧观测到
class lineFeaturePerId {
public:
const int feature_id; //线特征的全局ID,全局唯一
int start_frame;
// linefeature_per_frame 是个向量容器,存着这个特征在每一帧上的观测量。
// 如:feature_per_frame[0],存的是ft在start_frame上的观测值;
// feature_per_frame[1]存的是start_frame+1上的观测
// 同一id在不同帧上的观测,统一放到一起,后续会进行三角化
vector<lineFeaturePerFrame> linefeature_per_frame;
int used_num;
bool is_outlier;
bool is_margin;
bool is_triangulation;
Vector6d line_plucker;
Vector4d obs_init;
Vector4d obs_j;
Vector6d line_plk_init; // used to debug
Vector3d ptw1; // used to debug
Vector3d ptw2; // used to debug
Eigen::Vector3d tj_; // tij
Eigen::Matrix3d Rj_;
Eigen::Vector3d ti_; // tij
Eigen::Matrix3d Ri_;
int removed_cnt;
int all_obs_cnt; // 这个id的线特征总共被观测了多少次
int solve_flag; // 0 haven't solve yet; 1 solve succ; 2 solve fail;
lineFeaturePerId(int _feature_id, int _start_frame)
: feature_id(_feature_id),
start_frame(_start_frame),
used_num(0),
solve_flag(0),
is_triangulation(false) {
removed_cnt = 0;
all_obs_cnt = 1;
}
int endFrame();
};
构造的逻辑,理解了之前的对应关系是怎么存储的这里就很好理解了:
// 遍历同一帧上的线特征
for (auto &id_line : lines) {
// 使用起点终点初始化
lineFeaturePerFrame f_per_fra(id_line.second[0].second); // 观测
// 这是与上一帧的匹配关系,也可以认为是全局线特征id
int feature_id = id_line.first;
// cout << "line id: "<< feature_id << "\n";
// 在feature里找id号为feature_id的特征
auto it = find_if(linefeature.begin(), linefeature.end(),
[feature_id](const lineFeaturePerId &it) {
return it.feature_id == feature_id;
});
// 如果之前没存这个特征,说明是新的
if (it == linefeature.end()) {
linefeature.push_back(lineFeaturePerId(feature_id, frame_count));
linefeature.back().linefeature_per_frame.push_back(f_per_fra);
} else if (it->feature_id == feature_id) {
it->linefeature_per_frame.push_back(f_per_fra);
it->all_obs_cnt++;
}
}
3.线段三角化
3.1.普吕克线坐标
在图 2 a 2 \mathrm{a} 2a 中, 普吕克坐标中的 3 D 3 \mathrm{D} 3D 空间线 L \mathcal{L} L 由 L = ( n ? , d ? ) ? ∈ R 6 \mathcal{L}=\left(\mathbf{n}^{\top}, \mathrm{d}^{\top}\right)^{\top} \in \mathbb{R}^6 L=(n?,d?)?∈R6 表示, 其中 d ∈ R 3 \mathrm{d} \in \mathbb{R}^3 d∈R3 是线方向向量, n ∈ R 3 \mathrm{n} \in \mathbb{R}^3 n∈R3 是由直线和坐标原点确定的平面法向量。Plücker 坐标被过度参数化,因为向量 n \mathrm{n} n 和 d \mathrm{d} d 之间存在隐式约束,即 n ? d = 0 \mathrm{n}^{\top} \mathrm{d}=0 n?d=0 。因此, Plücker坐标不能直接用于无约束优化。然而, 使用由法线向量和方向向量表示的 3D 线, 可以简单地从两个几何视图执行三角测量, 并且也可以方便地对线几何变换进行建模。
对于线几何变换, 给定从世界坐标系
w
\mathrm{w}
w 到相机坐标系
c
\mathrm{c}
c 的变换矩阵
T
c
w
=
[
R
c
w
P
c
w
0
1
]
\mathbf{T}_{c w}=\left[\begin{array}{cc}\mathbf{R}_{c w} & \mathbf{P}_{c w} \\ \mathbf{0} & \mathbf{1}\end{array}\right]
Tcw?=[Rcw?0?Pcw?1?], 我们可以将线的 Plücker 坐标变换为 [30]
L
c
=
[
n
c
d
c
]
=
J
c
w
L
w
=
[
R
c
w
[
p
c
w
]
x
R
c
w
0
R
c
w
]
L
w
\mathcal{L}^c=\left[\begin{array}{c} \mathbf{n}^c \\ \mathbf{d}^c \end{array}\right]=J_{c w} \mathcal{L}_w=\left[\begin{array}{cc} \mathbf{R}_{c w} & {\left[\mathbf{p}_{c w}\right]_x \mathbf{R}_{c w}} \\ 0 & \mathbf{R}_{c w} \end{array}\right] \mathcal{L}^w
Lc=[ncdc?]=Jcw?Lw?=[Rcw?0?[pcw?]x?Rcw?Rcw??]Lw
其中 [ ? ] × [\cdot]_{\times} [?]×?是三维向量的斜对称矩阵, J c w J_{c w} Jcw? 是用于将线从帧 w w w 变换到帧 c \mathrm{c} c 的变换矩阵。
当在两个不同的摄像机视图中观察到新的线地标时,可以轻松计算出普吕克坐标。如图
2
?
b
2 \mathrm{~b}
2?b 所示,3D 线
L
\mathcal{L}
L 由摄像机
c
1
c_1
c1? 和
c
2
c_2
c2? 捕获为
z
L
c
1
\mathrm{z}_L^{c_1}
zLc1?? 和
z
l
c
2
\mathrm{z}_l^{c_2}
zlc2??。归一化图像平面中的线段
z
L
c
1
\mathrm{z}_L^{c_1}
zLc1?? 可以由两个端点
s
c
1
=
[
u
s
,
v
s
,
1
]
T
\mathrm{s}^{c_1}=\left[u_s, v_s, 1\right]^T
sc1?=[us?,vs?,1]T 和
e
c
1
=
[
u
e
,
v
e
,
1
]
?
\mathrm{e}^{c_1}=\left[u_e, v_e, 1\right]^{\top}
ec1?=[ue?,ve?,1]? 表示。三个不共线的点,包括线段的两个端点和坐标原点
C
=
[
x
0
,
y
0
,
z
0
]
?
\mathrm{C}=\left[x_0, y_0, z_0\right]^{\top}
C=[x0?,y0?,z0?]?, 确定
3
D
3 \mathrm{D}
3D 空间中的平面
π
=
[
π
x
,
π
y
,
π
z
,
π
w
]
?
\pi=\left[\pi_x, \pi_y, \pi_z, \pi_w\right]^{\top}
π=[πx?,πy?,πz?,πw?]? :
π
x
(
x
?
x
0
)
+
π
y
(
y
?
y
0
)
+
π
z
(
z
?
z
0
)
=
0
\pi_x\left(x-x_0\right)+\pi_y\left(y-y_0\right)+\pi_z\left(z-z_0\right)=0
πx?(x?x0?)+πy?(y?y0?)+πz?(z?z0?)=0
这里
[
π
x
π
y
π
z
]
=
[
s
c
1
]
×
e
c
1
,
π
w
=
π
x
x
0
+
π
y
y
0
+
π
z
z
0
\left[\begin{array}{c} \pi_x \\ \pi_y \\ \pi_z \end{array}\right]=\left[\mathbf{s}^{c_1}\right]_{\times} \mathrm{e}^{c_1}, \quad \pi_w=\pi_x x_0+\pi_y y_0+\pi_z z_0
?πx?πy?πz??
?=[sc1?]×?ec1?,πw?=πx?x0?+πy?y0?+πz?z0?
给定相机坐标系
c
1
c_1
c1? 中的两个平面
π
1
\pi_1
π1? 和
π
2
\pi_2
π2?, 对偶普吕克矩阵
L
?
\mathrm{L}^*
L? 可以通过以下方式计算
L
?
=
[
[
d
]
x
n
?
n
?
0
]
=
π
1
π
2
?
?
π
2
π
1
?
∈
R
4
×
4
\mathbf{L}^*=\left[\begin{array}{cc} {[\mathbf{d}]_{\mathbf{x}}} & \mathbf{n} \\ -\mathbf{n}^{\top} & 0 \end{array}\right]=\pi_1 \pi_2^{\top}-\pi_2 \pi_1^{\top} \in \mathbb{R}^{4 \times 4}
L?=[[d]x??n??n0?]=π1?π2???π2?π1??∈R4×4
相机坐标系 c 1 c_1 c1? 中的 Plücker 坐标 L \mathcal{L} L 很容易从对偶 Plücker 矩阵 L ? L^* L? 中提取。可以看出, n \mathbf{n} n 和 d \mathrm{d} d 不需要是单位向量。
主体逻辑在void FeatureManager::triangulateLine(Vector3d Ps[], Vector3d tic[], Matrix3d ric[])
中:
- 根据线段起点终点相机原点求平面:
/*
三点确定一个平面 a(x-x0)+b(y-y0)+c(z-z0)=0 --> ax + by + cz + d = 0 d = -(ax0 + by0 + cz0)
平面通过点(x0,y0,z0)以及垂直于平面的法线(a,b,c)来得到
(a,b,c)^T = vector(AO) cross vector(BO)
d = O.dot(cross(AO,BO))
*/
Vector4d pi_from_ppp(Vector3d x1, Vector3d x2, Vector3d x3) {
Vector4d pi;
pi << ( x1 - x3 ).cross( x2 - x3 ), - x3.dot( x1.cross( x2 ) ); // d = - x3.dot( (x1-x3).cross( x2-x3 ) ) = - x3.dot( x1.cross( x2 ) )
return pi;
}
如果两平面夹角小于 15 度,不三角化。两个平面求交线:
// 两平面相交得到直线的plucker 坐标
Vector6d pipi_plk( Vector4d pi1, Vector4d pi2){
Vector6d plk;
Matrix4d dp = pi1 * pi2.transpose() - pi2 * pi1.transpose();
plk << dp(0,3), dp(1,3), dp(2,3), - dp(1,2), dp(0,2), - dp(0,1);
return plk;
}
Vector6d plk = pipi_plk(pii, pij);
Vector3d n = plk.head(3);
Vector3d v = plk.tail(3);
// ...plk坐标的矩阵形式
Vector3d pc, nc, vc;
nc = it_per_id.line_plucker.head(3);
vc = it_per_id.line_plucker.tail(3);
Matrix4d Lc;
Lc << skew_symmetric(nc), vc, -vc.transpose(), 0;
3.2.正交表示
由于
3
D
3 \mathrm{D}
3D 空间线只有四个自由度, 因此在优化过程中, 正交表示
(
U
,
W
)
∈
S
O
(
3
)
×
S
O
(
2
)
(\mathbf{U}, \mathbf{W}) \in S O(3) \times S O(2)
(U,W)∈SO(3)×SO(2) 比 Plücker 坐标更合适。此外,正交表示和普吕克坐标可以相互转换,这意味着我们可以在 SLAM 系统中出于不同的目的同时采用它们。在本节中,我们将介绍正交表示的详细信息。如图
2
a
2 \mathrm{a}
2a 所示,在 3D 线上定义了坐标系。归一化法向量和归一化方向向量是坐标系的两个轴。第三轴是通过与其他两个轴向量相交来确定的。我们可以将线坐标和相机框架之间的旋转矩阵定义为
U
\mathbf{U}
U :
U
=
R
(
ψ
)
=
[
n
∣
∣
n
∣
∣
d
∣
∣
d
∣
∣
n
×
d
∣
∣
n
×
d
∣
∣
]
\mathbf{U}=\mathbf{R}(\psi)=\left[\begin{array}{lll} \frac{\mathrm{n}}{||\mathrm{n}||} & \frac{\mathrm{d}}{||\mathrm{d}||} & \frac{\mathrm{n} \times \mathrm{d}}{||\mathrm{n} \times \mathrm{d}| \mid} \end{array}\right]
U=R(ψ)=[∣∣n∣∣n??∣∣d∣∣d??∣∣n×d∣∣n×d??]
其中
ψ
=
[
ψ
1
,
ψ
2
,
ψ
3
]
?
\psi=\left[\psi_1, \psi_2, \psi_3\right]^{\top}
ψ=[ψ1?,ψ2?,ψ3?]? 由相机框架绕
x
、
y
x 、 y
x、y 和
z
\mathrm{z}
z 轴的旋转角度组成。普吕克坐标与
U
\mathrm{U}
U 之间的关系为:
[
n
d
]
=
[
n
∥
n
∥
d
∥
d
∥
n
×
d
∥
n
×
d
∥
]
[
∥
n
∥
0
0
∥
?
d
∥
0
0
]
\left[\begin{array}{ll} \mathbf{n} & \mathbf{d} \end{array}\right]=\left[\begin{array}{lll} \frac{\mathrm{n}}{\|\mathrm{n}\|} & \frac{\mathrm{d}}{\|\mathrm{d}\|} & \frac{\mathrm{n} \times \mathrm{d}}{\|\mathrm{n} \times \mathrm{d}\|} \end{array}\right]\left[\begin{array}{cc} \|\mathbf{n}\| & 0 \\ 0 & \|\mathrm{~d}\| \\ 0 & 0 \end{array} \right]
[n?d?]=[∥n∥n??∥d∥d??∥n×d∥n×d??]
?∥n∥00?0∥?d∥0?
?
由于式(16)中
(
∥
n
∥
,
∥
d
∥
)
(\|\mathbf{n}\|,\|\mathrm{d}\|)
(∥n∥,∥d∥) 的组合只有一个DoF,我们可以用三角函数来表示:
W
=
[
cos
?
(
?
)
?
sin
?
(
?
)
sin
?
(
?
)
cos
?
(
?
)
]
=
1
(
∥
n
∥
2
+
∥
d
∥
2
)
[
∥
n
∥
?
∥
d
∥
∥
d
∥
∥
n
∥
]
\mathbf{W}=\left[\begin{array}{cc} \cos (\phi) & -\sin (\phi) \\ \sin (\phi) & \cos (\phi) \end{array}\right]=\frac{1}{\sqrt{\left(\|\mathbf{n}\|^2+\|\mathbf{d}\|^2\right)}}\left[\begin{array}{cc} \|\mathbf{n}\| & -\|\mathbf{d}\| \\ \|\mathbf{d}\| & \|\mathbf{n}\| \end{array}\right]
W=[cos(?)sin(?)??sin(?)cos(?)?]=(∥n∥2+∥d∥2)?1?[∥n∥∥d∥??∥d∥∥n∥?]
其中 ? \phi ? 是旋转角度。回想一下, 从坐标原点到 3 D 3 \mathrm{D} 3D 线的距离是 d = ∥ n ∥ ∥ d ∥ d=\frac{\|\mathrm{n}\|}{\|\mathrm{d}\|} d=∥d∥∥n∥?, 因此 W \mathbf{W} W 包含有关距离 d \mathrm{d} d 的信息。根据 U \mathrm{U} U 和 W \mathbf{W} W 的定义, 这四个 DoF 包括来自旋转矩阵 (将线坐标变换到相机坐标系) 的 3 个 DoF, 以及来自距离 d \mathrm{d} d 的 1 个 D o F \mathrm{DoF} DoF 。我们在优化过程中使用 O = [ ψ , ? ] ? O=[\psi, \phi]^{\top} O=[ψ,?]? 作为 3D 空间线的最小表示。
使用正交表示优化 3D 线
L
\mathcal{L}
L 后,可以通过以下方式计算该线相应的普吕克坐标:
L
′
=
[
w
1
u
1
T
,
w
2
u
2
T
]
T
=
1
(
∣
n
∥
2
+
∥
d
∥
2
)
L
\mathcal{L}^{\prime}=\left[w_1 \mathbf{u}_1^T, w_2 \mathbf{u}_2^T\right]^T=\frac{1}{\sqrt{\left(\mid \mathbf{n}\left\|^2+\right\| \mathbf{d} \|^2\right)}} \mathcal{L}
L′=[w1?u1T?,w2?u2T?]T=(∣n∥2+∥d∥2)?1?L
其中 u i \mathbf{u}_i ui? 是矩阵 U \mathbf{U} U 的第 i i i 列、 w 1 = cos ? ( ? ) w_1=\cos (\phi) w1?=cos(?) 和 w 2 = sin ? ( ? ) w_2=\sin (\phi) w2?=sin(?) 。 L ′ \mathcal{L}^{\prime} L′ 和 L \mathcal{L} L 之间存在比例因子,但它们代表相同的3D空间线。
-
plk转正交表示
Vector4d plk_to_orth(Vector6d plk) { Vector4d orth; Vector3d n = plk.head(3); Vector3d v = plk.tail(3); Vector3d u1 = n / n.norm(); Vector3d u2 = v / v.norm(); Vector3d u3 = u1.cross(u2); // todo:: use SO3 orth[0] = atan2(u2(2), u3(2)); orth[1] = asin(-u1(2)); orth[2] = atan2(u1(1), u1(0)); Vector2d w(n.norm(), v.norm()); w = w / w.norm(); orth[3] = asin(w(1)); return orth; }
-
正交表示转plk
Vector6d orth_to_plk(Vector4d orth) { Vector6d plk; Vector3d theta = orth.head(3); double phi = orth[3]; double s1 = sin(theta[0]); double c1 = cos(theta[0]); double s2 = sin(theta[1]); double c2 = cos(theta[1]); double s3 = sin(theta[2]); double c3 = cos(theta[2]); Matrix3d R; R << c2 * c3, s1 * s2 * c3 - c1 * s3, c1 * s2 * c3 + s1 * s3, c2 * s3, s1 * s2 * s3 + c1 * c3, c1 * s2 * s3 - s1 * c3, -s2, s1 * c2, c1 * c2; double w1 = cos(phi); double w2 = sin(phi); double d = w1 / w2; // 原点到直线的距离 Vector3d u1 = R.col(0); Vector3d u2 = R.col(1); Vector3d n = w1 * u1; Vector3d v = w2 * u2; plk.head(3) = n; plk.tail(3) = v; // Vector3d Q = -R.col(2) * d; // plk.head(3) = Q.cross(v); // plk.tail(3) = v; return plk; }
优化的时候使用正交表示。这部分操作位于line_geometry.h/cpp
4.线段对位姿的导数
- todo
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!