-
Notifications
You must be signed in to change notification settings - Fork 97
/
Copy pathInitializer.cc
1627 lines (1417 loc) · 66.5 KB
/
Initializer.cc
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
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* This file is part of ORB-SLAM2.
*
* Copyright (C) 2014-2016 Raúl Mur-Artal <raulmur at unizar dot es> (University of Zaragoza)
* For more information see <https://github.com/raulmur/ORB_SLAM2>
*
* ORB-SLAM2 is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ORB-SLAM2 is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ORB-SLAM2. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Initializer.h"
#include "Thirdparty/DBoW2/DUtils/Random.h"
#include "Optimizer.h"
#include "ORBmatcher.h"
#include<thread>
namespace ORB_SLAM2
{
/**
* @brief 给定参考帧构造Initializer
*
* 用reference frame来初始化,这个reference frame就是SLAM正式开始的第一帧
* @param ReferenceFrame I 参考帧
* @param sigma I TODO 测量误差,但是我到现在也还是不知道这个是不是外部给定的,又是谁给定的
* @param iterations I RANSAC迭代次数,外部给定
*/
Initializer::Initializer(const Frame &ReferenceFrame, float sigma, int iterations)
{
//获取相机的内参数矩阵
mK = ReferenceFrame.mK.clone();
//获取保存参考帧特征点的向量
mvKeys1 = ReferenceFrame.mvKeysUn;
//获取估计误差
mSigma = sigma;
//并计算其平方
mSigma2 = sigma*sigma;
//保存最大迭代次数
mMaxIterations = iterations;
}
/**
* @brief 并行地计算基础矩阵和单应性矩阵,选取其中一个来恢复出最开始两帧之间的相对姿态,并进行三角化测量得到最初两帧的点云
* @param CurrentFrame I 当前帧,也就是SLAM意义上的第二帧
* @param vMatches12 I 当前帧和参考帧图像中特征点的匹配关系
* @param R21 O 相机从参考帧到当前帧所发生的旋转
* @param t21 O 相机从参考帧到当前帧所发生的平移
* @param vP3D O 三角化测量之后的特征点的空间坐标
* @param vbTriangulated O 基于特征点匹配关系,标记某对特征点是否被三角化测量
* @return 初始化是否成功
*/
bool Initializer::Initialize(const Frame &CurrentFrame, const vector<int> &vMatches12, cv::Mat &R21, cv::Mat &t21,
vector<cv::Point3f> &vP3D, vector<bool> &vbTriangulated)
{
// Fill structures with current keypoints and matches with reference frame
// Reference Frame: 1, Current Frame: 2
// Frame2 特征点
mvKeys2 = CurrentFrame.mvKeysUn;
// mvMatches12记录匹配上的特征点对
mvMatches12.clear();
// 预分配空间
//分配mvKeys2.size()这么多的空间的原因是,后面的for操作是按照vMatches12这个向量来遍历的,而根据下文对程序代码的猜测,这个向量的长度
//应该是和mvKeys2相同的。由于这个只保存参考帧和当前帧的特征点对应关系,所以不一定最后就能够有mvKeys2.size()个匹配关系,因此使用的是
//std::vector::reserve()这个函数
mvMatches12.reserve(mvKeys2.size());
// mvbMatched1记录参考帧中的每个特征点是否有匹配的特征点,
//这个成员变量后面没有用到,后面只关心匹配上的特征点
mvbMatched1.resize(mvKeys1.size());
// 步骤1:组织特征点对
for(size_t i=0, iend=vMatches12.size();i<iend; i++)
{
//TODO 猜测,这里vMatches12的下标是参考帧中特征点在其向量中的下标,其内容则是对应的当前帧中特征点在其向量中的下标
if(vMatches12[i]>=0)
{
//所以,保存这个匹配关系,实际上就是保存了特征点在各自的向量中的下标
mvMatches12.push_back(make_pair(i,vMatches12[i]));
//并且标记参考帧中的这个特征点有匹配关系
mvbMatched1[i]=true;
}
else
//否则,就没有
mvbMatched1[i]=false;
}
// 匹配上的特征点的对数
const int N = mvMatches12.size();
// Indices for minimum set selection
// 新建一个容器vAllIndices,生成0到N-1的数作为特征点的索引
//所有特征点对的索引
vector<size_t> vAllIndices;
//预分配空间
vAllIndices.reserve(N);
//在RANSAC的某次迭代中,还可以被抽取来作为数据样本的特征点对的索引,所以这里起的名字叫做可用的索引
vector<size_t> vAvailableIndices;
//初始化所有特征点对的索引
for(int i=0; i<N; i++)
{
vAllIndices.push_back(i);
}
// Generate sets of 8 points for each RANSAC iteration
// 步骤2:在所有匹配特征点对中随机选择8对匹配特征点为一组,共选择mMaxIterations组
// 用于FindHomography和FindFundamental求解
// mMaxIterations:200(默认值)
//这个变量用于保存每次迭代时所使用的向量which was used to 保存八对点 to 进行单应矩阵和基础矩阵估计
mvSets = vector< vector<size_t> >(mMaxIterations, //最大的RANSAC迭代次数
vector<size_t>(8,0)); //这个则是第二维元素的初始值,也就是第一维。这里其实也是一个第一维的构造函数,第一维vector有8项,每项的初始值为0.
//用于进行随机数据样本采样,设置随机数种子
DUtils::Random::SeedRandOnce(0);
//开始每一次的迭代
for(int it=0; it<mMaxIterations; it++)
{
//迭代开始的时候,所有的点都是可用的
vAvailableIndices = vAllIndices;
// Select a minimum set
//选择最小的数据样本集,这里我们希望求出相机的位姿,因此最少需要八个点,所以这里就循环了八次
for(size_t j=0; j<8; j++)
{
// 产生0到N-1的随机数,对应的是要抽取的特征点对的索引
int randi = DUtils::Random::RandomInt(0,vAvailableIndices.size()-1);
// idx表示哪一个索引对应的特征点对被选中
int idx = vAvailableIndices[randi];
//将本次迭代这个选中的第j个特征点对的索引添加到mvSets中
mvSets[it][j] = idx;
// randi对应的索引已经被选过了,从容器中删除
// randi对应的索引用最后一个元素替换,并删掉最后一个元素
// 所以首先把已经抽走的点的索引,替换为vector尾的索引
vAvailableIndices[randi] = vAvailableIndices.back();
//然后删除尾
vAvailableIndices.pop_back();
}//依次提取出8个特征点对
}//迭代mMaxIterations次,选取各自迭代时需要用到的最小数据集
// Launch threads to compute in parallel a fundamental matrix and a homography
// 步骤3:调用多线程分别用于计算fundamental matrix和homography
//这两个变量用于标记在H和F的计算中哪些特征点对被认为是Inlier
vector<bool> vbMatchesInliersH, vbMatchesInliersF;
//计算出来的单应矩阵和基础矩阵的RANSAC评分,这里其实是采用重投影误差来计算的
float SH, SF; // score for H and F
//这两个是经过RANSAC算法后计算出来的单应矩阵和基础矩阵
cv::Mat H, F; // H and F
// ref是引用的功能:http://en.cppreference.com/w/cpp/utility/functional/ref
// 计算homograpy并打分
thread threadH(&Initializer::FindHomography, //该线程的主函数
this, //由于主函数为类的成员函数,所以第一个参数就应该是当前对象的this指针
ref(vbMatchesInliersH), //输出,特征点对的Inlier标记
ref(SH), //输出,计算的单应矩阵的RANSAC评分
ref(H)); //输出,计算的单应矩阵结果
// 计算fundamental matrix并打分,参数定义是一样的,这里不再赘述
thread threadF(&Initializer::FindFundamental,this,ref(vbMatchesInliersF), ref(SF), ref(F));
// Wait until both threads have finished
//等待两个计算线程结束
threadH.join();
threadF.join();
// Compute ratio of scores
// 步骤4:计算得分比例,选取某个模型
//通过这个规则来判断谁的评分占比更多一些,注意不是简单的评分大,而是看评分的占比
float RH = SH/(SH+SF); //RH=Ratio of Homography?
// Try to reconstruct from homography or fundamental depending on the ratio (0.40-0.45)
// 步骤5:从H矩阵或F矩阵中恢复R,t
if(RH>0.40)
//此时从单应矩阵恢复,函数ReconstructH返回bool型结果
return ReconstructH(vbMatchesInliersH, //输入,匹配成功的特征点对Inliers标记
H, //输入,前面RANSAC计算后的单应矩阵
mK, //输入,相机的内参数矩阵
R21,t21, //输出,计算出来的相机从参考帧到当前帧所发生的旋转变换和位移变换
vP3D, //特征点对经过三角测量之后的空间坐标
vbTriangulated, //特征点对是否被三角化的标记
1.0, //这个对应的形参为minParallax,即认为某对特征点的三角化测量中,认为其测量有效时
//需要满足的最小视差角(如果视差角过小则会引起非常大的观测误差)
//并且这个角度的单位还是弧度
50); //为了进行运动恢复,所需要的最少的三角化测量成功的点个数
else //if(pF_HF>0.6)
//NOTICE 注意这里有一段注释掉的代码 作者原来的想法是,必须两个矩阵的评分都要“足够优秀”才会分别选取,如果这个比例在0.5附近,
//说明使用两种矩阵进行相机运动恢复的效果差不多,所以……那个时候干脆就不恢复了
//此时从基础矩阵恢复
return ReconstructF(vbMatchesInliersF,F,mK,R21,t21,vP3D,vbTriangulated,1.0,50);
//一般地程序不应该执行到这里,如果执行到这里说明程序跑飞了
//其实也有可能是前面注释掉的代码的补充
return false;
}
/**
* @brief 计算单应矩阵,假设场景为平面情况下通过前两帧求取Homography矩阵(current frame 2 到 reference frame 1),并得到该模型的评分
* @param vbMatchesInliers O 匹配的特征点对属于inliers的标记
* @param score O 这个单应矩阵的RANSAC评分
* @param H21 O 单应矩阵计算结果
*/
void Initializer::FindHomography(vector<bool> &vbMatchesInliers, float &score, cv::Mat &H21)
{
// Number of putative matches
//匹配的特征点对总数
const int N = mvMatches12.size();
// Normalize coordinates
// 将mvKeys1和mvKey2归一化到均值为0,一阶绝对矩为1,归一化矩阵分别为T1、T2
//这里所谓的一阶绝对矩其实就是随机变量到取值的中心的绝对值的平均值
//而特征点坐标向量乘某矩阵可以得到归一化后的坐标向量,那么这个矩阵称之为归一化矩阵,它表示了点集在进行归一化的时候
//所进行的平移变换和旋转变换
//归一化后的参考帧和当前帧中的特征点坐标
vector<cv::Point2f> vPn1, vPn2;
//各自的归一化矩阵
//NOTE 其实这里的矩阵归一化操作主要是为了在单目初始化过程中,固定场景的尺度,原理可以参考SLAM十四讲P152
cv::Mat T1, T2;
Normalize(mvKeys1,vPn1, T1);
Normalize(mvKeys2,vPn2, T2);
//这里求的逆在后面的代码中要用到,辅助进行原始尺度的恢复
cv::Mat T2inv = T2.inv();
// Best Results variables
// F:最终最佳的MatchesInliers与得分
//历史最佳评分
score = 0.0;
//历史最佳评分所对应的特征点对的inliers标记
vbMatchesInliers = vector<bool>(N,false);
// Iteration variables
//迭代过程中使用到的变量
//某次迭代中,参考帧的特征点坐标
vector<cv::Point2f> vPn1i(8);
//某次迭代中,当前帧的特征点坐标
vector<cv::Point2f> vPn2i(8);
//以及计算出来的单应矩阵、以及其逆
cv::Mat H21i, H12i;
// 每次RANSAC的MatchesInliers与得分
vector<bool> vbCurrentInliers(N,false);
float currentScore;
// Perform all RANSAC iterations and save the solution with highest score
//下面进行每次的RANSAC迭代
for(int it=0; it<mMaxIterations; it++)
{
// Select a minimum set
//每次迭代首先需要做的还是生成最小数据集
for(size_t j=0; j<8; j++)
{
//从mvSets中获取当前次迭代的某个特征点对的索引信息
int idx = mvSets[it][j];
// vPn1i和vPn2i为匹配的特征点对的坐标
//首先根据这个特征点对的索引信息分别找到两个特征点在各自图像特征点向量中的索引,然后读取其归一化之后的特征点坐标
vPn1i[j] = vPn1[mvMatches12[idx].first];
vPn2i[j] = vPn2[mvMatches12[idx].second];
}//读取8对特征点的归一化之后的坐标
//八点法计算单应矩阵
cv::Mat Hn = ComputeH21(vPn1i,vPn2i);
// NOTICE 恢复原始的均值和尺度
//关于这里为什么之前要对特征点进行归一化,后面又恢复这个矩阵的尺度,可以在《计算机视觉中的多视图几何》这本书中找到
//答案:P193页中讲到的归一化8点算法
H21i = T2inv*Hn*T1;
//然后计算逆
H12i = H21i.inv();
// 利用重投影误差为当次RANSAC的结果评分
currentScore = CheckHomography(H21i, H12i, //输入,单应矩阵的计算结果
vbCurrentInliers, //输出,特征点对的Inliers标记
mSigma); //TODO 测量误差,在Initializer类对象构造的时候,由外部给定的
// 得到最优的vbMatchesInliers与score
if(currentScore>score)
{
//如果当前的计算结果是历史最高,那么就要保存计算结果
H21 = H21i.clone();
//保存匹配好的特征点对的Inliers标记
vbMatchesInliers = vbCurrentInliers;
//更新历史最优评分
score = currentScore;
}//得到最优的计算结果
}//进行指定次数的RANSAC迭代
}//计算单应矩阵(函数)
/**
* @brief 计算基础矩阵,假设场景为非平面情况下通过前两帧求取Fundamental矩阵(current frame 2 到 reference frame 1),并得到该模型的评分
* @param vbMatchesInliers O 匹配好的特征点对的Inliers标记
* @param score O 当前计算好的基础矩阵的RANSAC评分
* @param F21 O 基础矩阵的计算结果
*/
void Initializer::FindFundamental(vector<bool> &vbMatchesInliers, float &score, cv::Mat &F21)
{
// Number of putative matches
// 获取总的匹配好的特征点对数
const int N = vbMatchesInliers.size();
// Normalize coordinates
//归一化坐标
vector<cv::Point2f> vPn1, vPn2;
cv::Mat T1, T2;
Normalize(mvKeys1,vPn1, T1);
Normalize(mvKeys2,vPn2, T2);
// NOTICE 注意这里取的是归一化矩阵T2的转置,因为基础矩阵的定义和单应矩阵不同,两者去归一化的计算也不相同
cv::Mat T2t = T2.t();
// Best Results variables
//最优结果
score = 0.0;
vbMatchesInliers = vector<bool>(N,false);
// Iteration variables
//迭代过程中使用到的变量
vector<cv::Point2f> vPn1i(8);
vector<cv::Point2f> vPn2i(8);
cv::Mat F21i;
vector<bool> vbCurrentInliers(N,false);
float currentScore;
// Perform all RANSAC iterations and save the solution with highest score
//开始RANSAC迭代
for(int it=0; it<mMaxIterations; it++)
{
// Select a minimum set
//选取最小数据集
for(int j=0; j<8; j++)
{
int idx = mvSets[it][j];
vPn1i[j] = vPn1[mvMatches12[idx].first];
vPn2i[j] = vPn2[mvMatches12[idx].second];
}//选取最小数据集
//根据最小数据集,计算基础矩阵
cv::Mat Fn = ComputeF21(vPn1i,vPn2i);
F21i = T2t*Fn*T1;
// 利用重投影误差为当次RANSAC的结果评分
currentScore = CheckFundamental(F21i, vbCurrentInliers, mSigma);
//得到最优结果
if(currentScore>score)
{
F21 = F21i.clone();
vbMatchesInliers = vbCurrentInliers;
score = currentScore;
}//得到最优的计算结果
}//进行RANSAC迭代
}//计算基础矩阵(函数)
// |x'| | h1 h2 h3 ||x|
// |y'| = a | h4 h5 h6 ||y| 简写: x' = a H x, a为一个尺度因子
// |1 | | h7 h8 h9 ||1|
// 使用DLT(direct linear tranform)求解该模型
// x' = a H x
// ---> (x') 叉乘 (H x) = 0
// ---> Ah = 0
// A = | 0 0 0 -x -y -1 xy' yy' y'| h = | h1 h2 h3 h4 h5 h6 h7 h8 h9 |
// |-x -y -1 0 0 0 xx' yx' x'|
// 通过SVD求解Ah = 0,A^T*A最小特征值对应的特征向量即为解
// 其实也就是右奇异值矩阵的最后一列
/**
* @brief 从特征点匹配求homography(normalized DLT),其实这里最少用四对点就能够求出来,不过这里为了和基础矩阵统一还是使用了8对点求最小二乘解
* @param vP1 I 归一化后的点, in reference frame
* @param vP2 I 归一化后的点, in current frame
* @return 单应矩阵
* @see Multiple View Geometry in Computer Vision - Algorithm 4.2 p109
*/
cv::Mat Initializer::ComputeH21(const vector<cv::Point2f> &vP1, const vector<cv::Point2f> &vP2)
{
//获取参与计算的特征点的数目
const int N = vP1.size();
//构造用于计算的矩阵A
cv::Mat A(2*N, //行,注意每一个点的数据对应两行
9, //列
CV_32F); // 2N*9 //数据类型
//开始构造矩阵A,对于每一个点
for(int i=0; i<N; i++)
{
//获取特征点对的像素坐标
const float u1 = vP1[i].x;
const float v1 = vP1[i].y;
const float u2 = vP2[i].x;
const float v2 = vP2[i].y;
//生成这个点的第一行
A.at<float>(2*i,0) = 0.0;
A.at<float>(2*i,1) = 0.0;
A.at<float>(2*i,2) = 0.0;
A.at<float>(2*i,3) = -u1;
A.at<float>(2*i,4) = -v1;
A.at<float>(2*i,5) = -1;
A.at<float>(2*i,6) = v2*u1;
A.at<float>(2*i,7) = v2*v1;
A.at<float>(2*i,8) = v2;
//生成这个点的第二行
A.at<float>(2*i+1,0) = u1;
A.at<float>(2*i+1,1) = v1;
A.at<float>(2*i+1,2) = 1;
A.at<float>(2*i+1,3) = 0.0;
A.at<float>(2*i+1,4) = 0.0;
A.at<float>(2*i+1,5) = 0.0;
A.at<float>(2*i+1,6) = -u2*u1;
A.at<float>(2*i+1,7) = -u2*v1;
A.at<float>(2*i+1,8) = -u2;
}//对于每一个点构造矩阵A
//保存计算结果,vt中的t表示是转置
cv::Mat u,w,vt;
//opencv提供的进行奇异值分解的函数
cv::SVDecomp(A, //输入,待进行奇异值分解的矩阵
w, //输出,奇异值矩阵
u, //输出,矩阵U
vt, //输出,矩阵V^T
cv::SVD::MODIFY_A | //输入,MODIFY_A是指允许计算函数可以修改待分解的矩阵,官方文档上说这样可以加快计算速度、节省内存
cv::SVD::FULL_UV); //FULL_UV=把U和VT补充成单位正交方阵
//注意前面说的是右奇异值矩阵的最后一列,但是在这里因为是vt,转置后了,所以是行;由于A有9列数据,故最后一列的下标为8
//
return vt.row(8).reshape(0, //转换后的通道数,这里设置为0表示是与前面相同
3); //转换后的行数
// v的最后一列
}//计算单应矩阵H(函数)
// x'Fx = 0 整理可得:Af = 0
// A = | x'x x'y x' y'x y'y y' x y 1 |, f = | f1 f2 f3 f4 f5 f6 f7 f8 f9 |
// 通过SVD求解Af = 0,A'A最小特征值对应的特征向量即为解
//其实也是运用DLT解法,由于点是三维的因此使用DLT法构造出来的A矩阵和前面的都有些不一样;但是其他的操作和上面计算单应矩阵
//都是基本相似的
/**
* @brief 从特征点匹配求fundamental matrix(normalized 8点法)
* @param vP1 I 归一化后的点, in reference frame
* @param vP2 I 归一化后的点, in current frame
* @return 基础矩阵
* @see Multiple View Geometry in Computer Vision - Algorithm 11.1 p282 (中文版 p191)
*/
cv::Mat Initializer::ComputeF21(const vector<cv::Point2f> &vP1,const vector<cv::Point2f> &vP2)
{
//获取参与计算的特征点对数
const int N = vP1.size();
//初始化A矩阵
cv::Mat A(N,9,CV_32F); // N*9
//对于每对特征点,生成A矩阵的内容
for(int i=0; i<N; i++)
{
const float u1 = vP1[i].x;
const float v1 = vP1[i].y;
const float u2 = vP2[i].x;
const float v2 = vP2[i].y;
A.at<float>(i,0) = u2*u1;
A.at<float>(i,1) = u2*v1;
A.at<float>(i,2) = u2;
A.at<float>(i,3) = v2*u1;
A.at<float>(i,4) = v2*v1;
A.at<float>(i,5) = v2;
A.at<float>(i,6) = u1;
A.at<float>(i,7) = v1;
A.at<float>(i,8) = 1;
}
//存储奇异值分解结果的变量
cv::Mat u,w,vt;
//进行奇异值分解
cv::SVDecomp(A,w,u,vt,cv::SVD::MODIFY_A | cv::SVD::FULL_UV);
//转换成基础矩阵的形式
cv::Mat Fpre = vt.row(8).reshape(0, 3); // v的最后一列
//对初步得来的基础矩阵继续进行一次奇异值分解
cv::SVDecomp(Fpre,w,u,vt,cv::SVD::MODIFY_A | cv::SVD::FULL_UV);
//NOTICE 注意基础矩阵的定义中,由于乘了一个t^,这个矩阵因为是从三维向量拓展出来的因此它的秩为2;
//所以正确的基础矩阵的秩应当小于等于2,这里就是进行的这个处理
w.at<float>(2)=0; // 秩2约束,将第3个奇异值设为0
//重新组合好满足秩约束的基础矩阵,作为最终计算结果返回
return u*cv::Mat::diag(w)*vt;
}//计算基础矩阵
/**
* @brief 对给定的homography matrix打分
* @param H21 I 从参考帧到当前帧的单应矩阵
* @param H12 I 从当前帧到参考帧的单应矩阵
* @param vbMatchesInliers O 匹配好的特征点对的Inliers标记
* @param sigma I 估计误差
* @see
* - Author's paper - IV. AUTOMATIC MAP INITIALIZATION (2)
* - Multiple View Geometry in Computer Vision - symmetric transfer errors: 4.2.2 Geometric distance
* - Multiple View Geometry in Computer Vision - model selection 4.7.1 RANSAC
*/
float Initializer::CheckHomography(const cv::Mat &H21, const cv::Mat &H12, vector<bool> &vbMatchesInliers, float sigma)
{
//获取特征点对的总大小
const int N = mvMatches12.size();
//获取从参考帧到当前帧的单应矩阵的各个元素
// |h11 h12 h13|
// |h21 h22 h23|
// |h31 h32 h33|
const float h11 = H21.at<float>(0,0);
const float h12 = H21.at<float>(0,1);
const float h13 = H21.at<float>(0,2);
const float h21 = H21.at<float>(1,0);
const float h22 = H21.at<float>(1,1);
const float h23 = H21.at<float>(1,2);
const float h31 = H21.at<float>(2,0);
const float h32 = H21.at<float>(2,1);
const float h33 = H21.at<float>(2,2);
//然后获取它的逆的各个元素
// |h11inv h12inv h13inv|
// |h21inv h22inv h23inv|
// |h31inv h32inv h33inv|
const float h11inv = H12.at<float>(0,0);
const float h12inv = H12.at<float>(0,1);
const float h13inv = H12.at<float>(0,2);
const float h21inv = H12.at<float>(1,0);
const float h22inv = H12.at<float>(1,1);
const float h23inv = H12.at<float>(1,2);
const float h31inv = H12.at<float>(2,0);
const float h32inv = H12.at<float>(2,1);
const float h33inv = H12.at<float>(2,2);
//给特征点对的Inliers标记预分配空间
vbMatchesInliers.resize(N);
//初始化RANSAC评分
float score = 0;
// 基于卡方检验计算出的阈值(假设测量有一个像素的偏差)
// 自由度为2的卡方分布,误差项有95%的概率不符合正态分布时的阈值
const float th = 5.991;
//信息矩阵,方差平方的倒数
//TODO 还不明白为什么泡泡机器人给出的注释说下面的这个是信息矩阵
//NOTE 不是有一个类成员变量mSigma2吗。。。为什么不直接用那个呢——我猜是程序员忘记了
const float invSigmaSquare = 1.0/(sigma*sigma);
// N对特征匹配点
for(int i=0; i<N; i++)
{
//一开始都默认为Inlier
bool bIn = true;
//根据索引获取这一对特征点
const cv::KeyPoint &kp1 = mvKeys1[mvMatches12[i].first];
const cv::KeyPoint &kp2 = mvKeys2[mvMatches12[i].second];
//提取特征点的坐标
const float u1 = kp1.pt.x;
const float v1 = kp1.pt.y;
const float u2 = kp2.pt.x;
const float v2 = kp2.pt.y;
// Reprojection error in first image
// x2in1 = H12*x2
// 将图像2中的特征点单应到图像1中
// |u1| |h11inv h12inv h13inv||u2|
// |v1| = |h21inv h22inv h23inv||v2|
// |1 | |h31inv h32inv h33inv||1 |
//这个是一个归一化系数,因为我们希望单应到图像1中的这个点的齐次坐标,最后一维为1
const float w2in1inv = 1.0/(h31inv*u2+h32inv*v2+h33inv); //为了计算方便加了一个倒数
//计算两个坐标
const float u2in1 = (h11inv*u2+h12inv*v2+h13inv)*w2in1inv; //u2_in_image_1
const float v2in1 = (h21inv*u2+h22inv*v2+h23inv)*w2in1inv; //v2_in_image_1
// 计算重投影误差
const float squareDist1 = (u1-u2in1)*(u1-u2in1)+(v1-v2in1)*(v1-v2in1);
// 根据方差归一化误差
//这里的误差只有归一化之后,后面的RANSAC评分在不同点和不同的矩阵(主要是这个)中的比较才会有意义
const float chiSquare1 = squareDist1*invSigmaSquare;
//如果这个点的归一化后的重投影误差超过了给定的阈值
//其实就是如果这个误差的平方和超过了阈值后说明观测的点的误差有95%的概率不符合正态分布
if(chiSquare1>th)
//那么说明就是Outliers
bIn = false;
else
//在阈值内才算是Inliers,保持默认设置不变;然后累计对当前使用的单应矩阵的RANSAC评分
//因为在卡方检验中,那个和越小说明数据越符合正态分布,因此用th-来取反,这样数据越好score越大
score += th - chiSquare1;
//为了使这个计算能够比较好地反应矩阵的实际计算效果,因此这里还要再反方向进行一次重投影误差的计算
// Reprojection error in second image
// x1in2 = H21*x1
// 将图像1中的特征点单应到图像2中
//缩放因子
const float w1in2inv = 1.0/(h31*u1+h32*v1+h33);
//两个坐标
const float u1in2 = (h11*u1+h12*v1+h13)*w1in2inv;
const float v1in2 = (h21*u1+h22*v1+h23)*w1in2inv;
//计算重投影误差
const float squareDist2 = (u2-u1in2)*(u2-u1in2)+(v2-v1in2)*(v2-v1in2);
//重投影误差的归一化
const float chiSquare2 = squareDist2*invSigmaSquare;
//比较归一化后的重投影误差是否大于阈值
if(chiSquare2>th)
//大于阈值说明是Outlier
bIn = false;
else
//反之则是Inlier,保持标志不变;同时累计评分
score += th - chiSquare2;
//注意,只有两个点都是Inlier才会认为这对匹配关系是Inlier;
//只要有一个特征点是Outlier那么就认为这对特征点的匹配关系是Outlier
if(bIn)
vbMatchesInliers[i]=true;
else
vbMatchesInliers[i]=false;
}//对于每对匹配好的特征点
//返回当前给出的单应矩阵的评分
return score;
}//计算给出的单应矩阵的RANSAC评分
/**
* @brief 对给定的fundamental matrix打分
* @param F21 I 从当前帧到参考帧的基础矩阵
* @param vbMatchesInliers O 匹配的特征点对属于inliers的标记
* @param sigma I TODO 估计误差?
* @see
* - Author's paper - IV. AUTOMATIC MAP INITIALIZATION (2)
* - Multiple View Geometry in Computer Vision - symmetric transfer errors: 4.2.2 Geometric distance
* - Multiple View Geometry in Computer Vision - model selection 4.7.1 RANSAC
*/
float Initializer::CheckFundamental(const cv::Mat &F21, vector<bool> &vbMatchesInliers, float sigma)
{
//获取匹配的特征点对的总对数
const int N = mvMatches12.size();
//然后提取基础矩阵中的元素数据
const float f11 = F21.at<float>(0,0);
const float f12 = F21.at<float>(0,1);
const float f13 = F21.at<float>(0,2);
const float f21 = F21.at<float>(1,0);
const float f22 = F21.at<float>(1,1);
const float f23 = F21.at<float>(1,2);
const float f31 = F21.at<float>(2,0);
const float f32 = F21.at<float>(2,1);
const float f33 = F21.at<float>(2,2);
//预分配空间
vbMatchesInliers.resize(N);
//设置评分初始值(因为后面需要进行这个数值的累计)
float score = 0;
// 基于卡方检验计算出的阈值(假设测量有一个像素的偏差?好像不是这样呢)
// 自由度为1的卡方分布,当平方和有95%的概率不符合正态分布时的阈值
const float th = 3.841;
//TODO 这里还增加了一个自由度为2的卡方分布的阈值,但是还不清楚为什么使用这个参与评分,目测应该是和单应矩阵的评分部分统一
const float thScore = 5.991;
//计算这个逆,后面计算卡方的时候会用到
const float invSigmaSquare = 1.0/(sigma*sigma);
//对于每一对匹配的特征点对
for(int i=0; i<N; i++)
{
//默认为这对特征点是Inliers
bool bIn = true;
//从匹配关系中获得索引并且拿到特点数据
const cv::KeyPoint &kp1 = mvKeys1[mvMatches12[i].first];
const cv::KeyPoint &kp2 = mvKeys2[mvMatches12[i].second];
//提取出特征点的坐标
const float u1 = kp1.pt.x;
const float v1 = kp1.pt.y;
const float u2 = kp2.pt.x;
const float v2 = kp2.pt.y;
// Reprojection error in second image
// l2=F21x1=(a2,b2,c2)
// F21x1可以算出x1在图像中x2对应的线l
//将参考帧中的特征点以给出的基础矩阵投影到当前帧上,下面的计算完完全全就是矩阵计算的展开
//注意为了方便计算,这里投影所得到的向量的形式正好是一条2D直线,三个参数对应这直线方程的三个参数
const float a2 = f11*u1+f12*v1+f13;
const float b2 = f21*u1+f22*v1+f23;
const float c2 = f31*u1+f32*v1+f33;
//理想状态下:x2应该在l这条线上:x2点乘l = 0
//计算点到直线距离,这里是分子
const float num2 = a2*u2+b2*v2+c2;
//计算重投影误差,这里的重投影误差其实是这样子定义的
//注意这里计算的只有一个平方项
const float squareDist1 = num2*num2/(a2*a2+b2*b2); // 点到线的几何距离 的平方
//归一化误差
const float chiSquare1 = squareDist1*invSigmaSquare;
//判断归一化误差是否大于阈值
//因为上面计算的只有一个平方项,所以这里的阈值也是选择的服从自由度为1的卡方分布的0.95的阈值
if(chiSquare1>th)
//大于就说明这个点是Outlier
bIn = false;
else
//只有小于的时候才认为是Inlier,然后累计对当前使用的基础矩阵的RANSAC评分
//不过在这里累加的时候使用的阈值还是自由度为2的那个卡方的阈值
//这样最终计算的结果会使得基础矩阵的比单应矩阵的高(因为前面的那个阈值小)
score += thScore - chiSquare1;
// Reprojection error in second image
// l1 =x2tF21=(a1,b1,c1)
//然后反过来进行相同操作,求解直线
const float a1 = f11*u2+f21*v2+f31;
const float b1 = f12*u2+f22*v2+f32;
const float c1 = f13*u2+f23*v2+f33;
//计算分子
const float num1 = a1*u1+b1*v1+c1;
//计算重投影误差
const float squareDist2 = num1*num1/(a1*a1+b1*b1);
//归一化
const float chiSquare2 = squareDist2*invSigmaSquare;
//判断阈值
if(chiSquare2>th)
bIn = false;
else
score += thScore - chiSquare2;
//然后就是对点的标记处理
if(bIn)
vbMatchesInliers[i]=true;
else
vbMatchesInliers[i]=false;
}//对于每对匹配的特征点
//返回评分
return score;
}
//注意下文中的符号“'”表示矩阵的转置
// |0 -1 0|
// E = U Sigma V' let W = |1 0 0|
// |0 0 1|
// 得到4个解 E = [R|t]
// R1 = UWV' R2 = UW'V' t1 = U3 t2 = -U3
/**
* @brief 从F恢复R t
*
* 度量重构
* 1. 由Fundamental矩阵结合相机内参K,得到Essential矩阵: \f$ E = k'^T F k \f$
* 2. SVD分解得到R t
* 3. 进行cheirality check, 从四个解中找出最合适的解
*
* @param vbMatchesInliers I 匹配好的特征点对的Inliers标记
* @param F21 I 从参考帧到当前帧的基础矩阵
* @param K I 相机的内参数矩阵
* @param R21 O 计算好的相机从参考帧到当前帧的旋转
* @param t21 O 计算好的相机从参考帧到当前帧的平移
* @param vP3D O 三角化测量之后的特征点的空间坐标
* @param vbTriangulated O 某个特征点是否被三角化了的标记
* @param minParallax I 认为三角化测量有效的最小视差角
* @param minTriangulated I 认为使用三角化测量进行数据判断的最小测量点数量
* @return 是否解析成功
*
* @see Multiple View Geometry in Computer Vision - Result 9.19 p259
*/
bool Initializer::ReconstructF(vector<bool> &vbMatchesInliers, cv::Mat &F21, cv::Mat &K,
cv::Mat &R21, cv::Mat &t21, vector<cv::Point3f> &vP3D, vector<bool> &vbTriangulated,
float minParallax, int minTriangulated)
{
//计数器,统计被标记为Inlier的特征点对数
int N=0;
//开始遍历
for(size_t i=0, iend = vbMatchesInliers.size() ; i<iend; i++)
//如果当前被遍历的特征点对被标记,
if(vbMatchesInliers[i])
//那么计数器++
N++;
// Compute Essential Matrix from Fundamental Matrix
//根据基础矩阵和相机的内参数矩阵计算本质矩阵
cv::Mat E21 = K.t()*F21*K;
//emmm过会儿存放计算结果要用到的
cv::Mat R1, R2, t;
// Recover the 4 motion hypotheses
// 虽然这个函数对t有归一化,但并没有决定单目整个SLAM过程的尺度
// NOTICE 因为CreateInitialMapMonocular函数对3D点深度会缩放,然后反过来对 t 有改变
//调用自己建立的解析函数,求解两个R解和两个t解,不过由于两个t解互为相反数,因此这里先只获取一个
DecomposeE(E21,R1,R2,t);
//这里计算另外一个t解
cv::Mat t1=t;
cv::Mat t2=-t;
// Reconstruct with the 4 hyphoteses and check
//验证
//这四个向量对应着解的四种组合情况,分别清楚各自情况下三角化测量之后的特征点空间坐标
vector<cv::Point3f> vP3D1, vP3D2, vP3D3, vP3D4;
//这四个标记用的向量则保存了哪些点能够被三角化测量的标记
vector<bool> vbTriangulated1,vbTriangulated2,vbTriangulated3, vbTriangulated4;
//每种解的情况对应的比较大的特征点对视差角
float parallax1,parallax2, parallax3, parallax4;
//检查每种解,会返回一个数值,这个数值是3D点在摄像头前方且投影误差小于阈值的3D点个数,下文我们称之为good点吧
int nGood1 = CheckRT(R1,t1, //当前组解
mvKeys1,mvKeys2, //参考帧和当前帧中的特征点
mvMatches12, vbMatchesInliers, //特征点的匹配关系和Inliers标记
K, //相机的内参数矩阵
vP3D1, //存储三角化以后特征点的空间坐标
4.0*mSigma2, //三角化测量过程中允许的最大重投影误差
vbTriangulated1, //参考帧中被成功进行三角化测量的特征点的标记
parallax1); //认为某对特征点三角化测量有效的最小视差角
int nGood2 = CheckRT(R2,t1,mvKeys1,mvKeys2,mvMatches12,vbMatchesInliers,K, vP3D2, 4.0*mSigma2, vbTriangulated2, parallax2);
int nGood3 = CheckRT(R1,t2,mvKeys1,mvKeys2,mvMatches12,vbMatchesInliers,K, vP3D3, 4.0*mSigma2, vbTriangulated3, parallax3);
int nGood4 = CheckRT(R2,t2,mvKeys1,mvKeys2,mvMatches12,vbMatchesInliers,K, vP3D4, 4.0*mSigma2, vbTriangulated4, parallax4);
//选取最多的good点数
int maxGood = max(nGood1,max(nGood2,max(nGood3,nGood4)));
//清空函数的参数,我们要准备进行输出了
R21 = cv::Mat();
t21 = cv::Mat();
// minTriangulated为可以三角化恢复三维点的个数
//从这里可以看出minTriangulated变量应该是作为输入的变量
//然后。。。有这样一个认为应该选取的最少的good点的确定操作
int nMinGood = max(static_cast<int>(0.9*N),minTriangulated);
//统计有多少组可行解的,这里暂时称之为“可行解计数变量”吧
int nsimilar = 0;
//如果在某种情况下观测到的3D点占到了绝大多数,那么“可行解计数”变量++
if(nGood1>0.7*maxGood)
nsimilar++;
if(nGood2>0.7*maxGood)
nsimilar++;
if(nGood3>0.7*maxGood)
nsimilar++;
if(nGood4>0.7*maxGood)
nsimilar++;
// If there is not a clear winner or not enough triangulated points reject initialization
// 四个结果中如果没有明显的最优结果,则返回失败
if(maxGood<nMinGood || //如果最好的解中没有足够的good点
nsimilar>1) //或者是存在两种及以上的解的good点都占了绝大多数,说明没有明显的最优结果
{
//认为这次的解算是失败的
return false;
}
// If best reconstruction has enough parallax initialize
// 比较大的视差角
//根据后面代码的大概意思,貌似是确定解的时候,必须要有足够的可以被三角化的空间点才行。其实是这样的:
//下面程序确定解的思想是找大部分空间点在相机前面的解,这个“大部分”是按照0.7*maxGood定义的(当然程序作者
//考虑问题更加细致,它们还考虑了两种解的大部分空间点都在相机前面的情况),但是如果这里的maxGood本来就不大,那么
//这个方法其实就没有什么意义了。为了衡量变量maxGood的好坏,这里对每种解都使用了视差角parallax进行描述(因为过小的
//视差角会带来比较大的观测误差);然后有函数入口有一个给定的最小值minParallax,如果很幸运某种解的good点占了大多数
//(其实一般地也就是nGoodx==maxGood了),也要保证parallaxx>minParallax这个条件满足,才能够被认为是真正的解。
//看看最好的good点是在哪种解的条件下发生的
if(maxGood==nGood1)
{
//如果该种解下的parallax大于函数参数中给定的最小值
if(parallax1>minParallax)
{
//那么就它了
//获取三角测量后的特征点的空间坐标
vP3D = vP3D1;
//获取特征点向量的三角化测量标记
vbTriangulated = vbTriangulated1;
//另存一份对应解情况下的相机位姿
R1.copyTo(R21);
t1.copyTo(t21);
//返回true表示由给定的基础矩阵求解相机R,t成功
return true;
}
}else if(maxGood==nGood2) //接下来就是对其他情况的判断了,步骤都是一样的,这里就不加备注了
{
if(parallax2>minParallax)
{
vP3D = vP3D2;
vbTriangulated = vbTriangulated2;
R2.copyTo(R21);
t1.copyTo(t21);
return true;
}
}else if(maxGood==nGood3)
{
if(parallax3>minParallax)
{
vP3D = vP3D3;
vbTriangulated = vbTriangulated3;
R1.copyTo(R21);
t2.copyTo(t21);
return true;
}
}else if(maxGood==nGood4)
{
if(parallax4>minParallax)
{
vP3D = vP3D4;
vbTriangulated = vbTriangulated4;
R2.copyTo(R21);
t2.copyTo(t21);
return true;
}
}
//如果有最优解但是不满足对应的parallax>minParallax,或者是其他的原因导致的无法求出相机R,t,那么返回false表示求解失败
return false;
}
// H矩阵分解常见有两种方法:Faugeras SVD-based decomposition 和 Zhang SVD-based decomposition
// 参考文献:Motion and structure from motion in a piecewise plannar environment
// 这篇参考文献和下面的代码使用了Faugeras SVD-based decomposition算法
/**
* @brief 从H恢复R t
* @param vbMatchesInliers I 匹配点对的内点标记
* @param H21 I 从参考帧到当前帧的单应矩阵
* @param K I 相机的内参数矩阵
* @param R21 O 计算出来的相机旋转
* @param t21 O 计算出来的相机平移
* @param vP3D O 世界坐标系下,三角化测量特征点对之后得到的特征点的空间坐标
* @param vbTriangulated O 特征点对被三角化测量的标记
* @param minParallax I 在进行三角化测量时,观测正常所允许的最小视差角
* @param minTriangulated I 最少被三角化的点对数(其实也是点个数)
* @return 数据解算是否成功的
* @see
* - Faugeras et al, Motion and structure from motion in a piecewise planar environment. International Journal of Pattern Recognition and Artificial Intelligence, 1988.
* - Deeper understanding of the homography decomposition for vision-based control
*/
bool Initializer::ReconstructH(vector<bool> &vbMatchesInliers, cv::Mat &H21, cv::Mat &K,
cv::Mat &R21, cv::Mat &t21, vector<cv::Point3f> &vP3D, vector<bool> &vbTriangulated,
float minParallax, int minTriangulated)
{
//匹配的特征点对中属于Inlier的个数
int N=0;
//遍历
for(size_t i=0, iend = vbMatchesInliers.size() ; i<iend; i++)
//如果被遍历到的这个点属于Inlier
if(vbMatchesInliers[i])
//计数变量++
N++;
// We recover 8 motion hypotheses using the method of Faugeras et al.
// Motion and structure from motion in a piecewise planar environment.
// International Journal of Pattern Recognition and Artificial Intelligence, 1988
// 因为特征点是图像坐标系,所以将H矩阵由相机坐标系换算到图像坐标系
cv::Mat invK = K.inv();
//这个部分不要看PPT,对不太上,可以结合着视觉SLAM十四讲P146页中单应矩阵的推导来看
//TODO 不过感觉最后还是缺少了一个因子d
cv::Mat A = invK*H21*K;
//存储进行奇异值分解的结果
cv::Mat U,w,Vt,V;
//进行奇异值分解
cv::SVD::compute(A, //等待被进行奇异值分解的矩阵
w, //奇异值矩阵
U, //奇异值分解左矩阵
Vt, //奇异值分解右矩阵,注意函数返回的是转置
cv::SVD::FULL_UV); //全部分解
//得到奇异值分解的右矩阵本身