C++學習筆記(11) 矩陣Eigen庫使用(1)
如何安裝Eigen
下載和提取Eigen,進入官網任意選取一個版本下載,解壓之後,將壓縮包中的Eigen拷入程式目錄即可。這裡簡單說一下我所用的VScode所用的task配置。
{
“tasks”
:
[
{
“type”
:
“cppbuild”
,
“label”
:
“C/C++: g++ 生成活動檔案”
,
“command”
:
“/usr/bin/g++”
,
“args”
:
[
“-g”
,
//編譯而不連結
“${file}”
,
“-I”
,
“${workspaceFolder}/include”
,
//我自己寫的程式碼的標頭檔案位置
“-I”
,
“${workspaceFolder}”
,
//工作目錄,即Eigen拷入的位置
“${workspaceFolder}/src/**。cc”
,
//我自己寫的程式碼原始檔位置
“-o”
,
“${fileDirname}/${fileBasenameNoExtension}”
//產生的工程檔名,目前設定的含義是主函式的檔名
,
],
“options”
:
{
“cwd”
:
“${fileDirname}”
},
“problemMatcher”
:
[
“$gcc”
],
“group”
:
{
“kind”
:
“build”
,
“isDefault”
:
true
},
“detail”
:
“偵錯程式生成的任務。”
}
],
“version”
:
“2。0。0”
}
這裡還有一個小技巧,對於VScode而言,自動補全功能是需要手動配置庫的。開啟目錄下的。vscode檔案中的c_cpp_properties。json,在include裡面將之前所拷的Eigen新增進去。
{
“configurations”
:
[
{
“name”
:
“Linux”
,
“includePath”
:
[
“${workspaceFolder}/Eigen/**”
,
//這一行
“${workspaceFolder}/include/**”
,
],
“defines”
:
[],
“compilerPath”
:
“/usr/bin/gcc”
,
“cStandard”
:
“gnu17”
,
“cppStandard”
:
“gnu++14”
,
“intelliSenseMode”
:
“linux-gcc-x64”
}
],
“version”
:
4
}
簡單的例子
這裡有一個簡單的例子可以驗證看到簡單的Eigen程式碼。
#include
#include
using
Eigen
::
MatrixXd
;
int
main
()
{
MatrixXd
m
(
2
,
2
);
m
(
0
,
0
)
=
3
;
m
(
1
,
0
)
=
2。5
;
m
(
0
,
1
)
=
-
1
;
m
(
1
,
1
)
=
m
(
1
,
0
)
+
m
(
0
,
1
);
std
::
cout
<<
m
<<
std
::
endl
;
}
Eigen的形式與Matlab的形式非常相似,如果之前熟悉Matlab,可以很簡單的使用Eigen。
Eigen的標頭檔案定義了多種型別,但實際上可能使用MatrixXd型別就足夠了。這個型別性代表任一大小的矩陣,其中的每個元素都是double型別。可以檢視快速參考手冊瞭解能夠使用的各種不同型別的舉證。
下面一個例子將矩陣和向量結合起來使用。
#include
#include
using
namespace
Eigen
;
using
namespace
std
;
int
main
()
{
MatrixXd
m
=
MatrixXd
::
Random
(
3
,
3
);
m
=
(
m
+
MatrixXd
::
Constant
(
3
,
3
,
1。2
))
*
50
;
cout
<<
“m =”
<<
endl
<<
m
<<
endl
;
VectorXd
v
(
3
);
v
<<
1
,
2
,
3
;
cout
<<
“m * v =”
<<
endl
<<
m
*
v
<<
endl
;
}
另一個完全等價的例子:
#include
#include
using
namespace
Eigen
;
using
namespace
std
;
int
main
()
{
Matrix3d
m
=
Matrix3d
::
Random
();
m
=
(
m
+
Matrix3d
::
Constant
(
1。2
))
*
50
;
cout
<<
“m =”
<<
endl
<<
m
<<
endl
;
Vector3d
v
(
1
,
2
,
3
);
cout
<<
“m * v =”
<<
endl
<<
m
*
v
<<
endl
;
}
第二個例子和前一個例子不同,他事先宣告一個3$\times$3矩陣,然後呼叫Random()進行初始化,隨機值介於0~1之間。接下來一行則是使用線性對映,是的結果為介於10~110之間的數。對比上面那個例子中的MatrixXd::Constant(3,3,1。2),兩個的意思都是返回一個3$\times$3的矩陣表示式,其他都是標準算數。
接下來則是引入一個全新的型別,VectorXd表示任意大小的列向量。這裡例子1建立的向量包括三個未初始化的向量,然後之後再輸入三個的值,這是高階初始化。
最後,輸出矩陣m和向量v相乘之後的結果。
對比第一個和第二個例子,可以發現使用固定大小的矩陣和向量有兩個優點: 1。 編譯器能夠生成更好(更快)的程式碼。因此編譯器能夠知道矩陣和向量的大小。 2。 在型別中規定了矩陣的大小,能夠在編譯時進行更加嚴格的檢查。比如說,如果你試圖用一個Matri4d(一個4$\times$4矩陣)乘以一個Vector3d(一個3$\times$1的列向量),那麼編譯器就會報錯。但是,使用多種型別會增加編譯時間和可執行檔案的大小,編譯時也不知道矩陣的大小是什麼,就不好檢查了。因此,一般經驗上將,對於4$\times$4乃至更小的舉證可以使用固定大小的矩陣型別。
Matrix類
在Eigen中,所有的矩陣和向量都是Matrix模板類的物件。Vector是單行或者單列的矩陣。
Matrix的前三個模板引數
Matrix類有六個模板引數,但知道前三個就足夠了。剩下的三個是預設值,現在直接保持不變即可。接下來將討論:
Matrix的三個強制性模板引數是:
Matrix
<
typename
Scalar
,
int
RowsAtCompileTime
,
int
ColsAtCompileTime
>
Scalar是標量型別,即係數的型別。如果你想要的是浮點型舉證,這裡可以選擇float。檢視這,列出了所有支援的標量的型別以及怎麼擴展出一個支援的新型別。
RowsAtCompileTime和ColsAtCompileTime是矩陣的行數和列數。
標頭檔案中提供了簡單方便的預處理命名,比如一個浮點型的4$\times$4矩陣,可以用Matrix4f表示,而實際上在Eigen裡面的定義是:
typedef
Matrix
<
float
,
4
,
4
>
Matrix4f
;
向量
前面提到,在Eigen裡面,向量只不過是矩陣的一種特殊情況而已。一般來說,一列的向量是最普遍的,這也就是列向量。那如果是一行的話,就是行向量。
比如說我們現在需要一個含有三個浮點數的列向量,那麼我們可以用Vector3f表示,即:
typedef
Matrix
<
float
,
3
,
1
>
Vector3f
同樣,我們也可以呼叫一個行向量,如:
typedef
Matrix
<
int
,
1
,
2
>
RowVector2i
;
特殊變數Dynamic
當然,Eigen不僅限於在編譯時已知大小的舉證。在RowsAtCompileTime以及ColsAtCompileTime中都可以採用Dynamic表示大小在編譯中是未知的,所以必須作為執行中的變數處理。在Eigen術語中,這樣的大小成為動態大小,而如果知道了矩陣大小的則成為固定大小。比如說,MatixXd,就是帶有兩個dynamic引數的矩陣,即一下定義:
typedef
Matrix
<
double
,
Dynamic
,
Dynamic
>
MatrixXd
;
同樣,我們也有一個動態大小的向量:
typedef
Matrix
<
int
,
Dynamic
,
1
>
VectorXi
;
當然也完全可以自己定義一個三行未知列的矩陣:
Matrix
<
float
,
3
,
dynamic
>
myMatrix
;
建構函式
預設建構函式始終可用,不需要進行任何動態記憶體申請,也不需要初始化矩陣係數。可以這樣:
Matrix3f
a
;
MatrixXf
b
;
a是一個元素為float的3$\times$3矩陣
b是一個元素為float的未知大小舉證,目前是0$\times$0,其舉證的係數陣列尚未分配。
同樣,可以用建構函式規定尺寸。這裡,總是先寫行引數,在寫列引數。對於向量,則只輸出一個引數即可。他們會申請給定大小的係數矩陣,但不會初始化係數。
MatrixXf
a
(
10
,
15
);
//10乘15的動態大小矩陣,已申請分配記憶體但還沒有初始化。
VectorXf
b
(
30
);
//懂的都懂
為了提供一個統一的結構在固定大小的矩陣和動態大小的矩陣之間,在固定大小的矩陣中使用這些建構函式也是合法的,儘管輸入的引數一點軟用沒有,比如說:
Matrix3f
a
(
3
,
3
);
上面這個其實啥都沒執行。
矩陣和向量同樣能夠在係數列表中初始化,在C++11之前,這個功能還僅限於固定大小的小列或者最大為4的向量。
Vector2d
a
(
5。0
,
6。0
);
Vector3d
b
(
5。0
,
6。0
,
7。0
);
Vector4d
b
(
5。0
,
6。0
,
7。0
,
8。0
);
如果啟用C++11,那麼就可以透過傳遞任意數量的係數來初始化任一大小的固定大小的列或行向量:
Vector2i
a
(
1
,
2
);
矩陣
<
int
,
5
,
1
>
b
{
1
,
2
,
3
,
4
,
5
};
矩陣
<
int
,
1
,
5
>
c
=
{
1
,
2
,
3
,
4
,
5
}
在矩陣的情況下,係數必須按行分組作為初始化列表:
MatrixXi
a
{
// 構造一個 2x2 的矩陣
{
1
,
2
},
// 第一行
{
3
,
4
}
// 第二行
};
Matrix
<
double
,
2
,
3
>
b
{
{
2
,
3
,
4
},
{
5
,
6
,
7
},
};
對於行或者列向量,允許隱式轉置,即可以單行初始化列向量。
VectorXd
a
{{
1。5
,
2。5
,
3。5
}};
// 具有 3 個係數的列向量
RowVectorXd
b
{{
1。0
,
2。0
,
3。0
,
4。0
}};
// 一個有 4 個係數的行向量
係數儲存器
Eigen中主要的係數儲存期和修改器都是透過括號()過載的。直接看例子:
#include
#include
using
namespace
Eigen
;
int
main
()
{
MatrixXd
m
(
2
,
2
);
m
(
0
,
0
)
=
3
;
m
(
1
,
0
)
=
2。5
;
m
(
0
,
1
)
=
-
1
;
m
(
1
,
1
)
=
m
(
1
,
0
)
+
m
(
0
,
1
);
std
::
cout
<<
“Here is the matrix m:
\n
”
<<
m
<<
std
::
endl
;
VectorXd
v
(
2
);
v
(
0
)
=
4
;
v
(
1
)
=
v
(
0
)
-
1
;
std
::
cout
<<
“Here is the vector v:
\n
”
<<
v
<<
std
::
endl
;
}
請注意,m(index)不僅限於向量,同樣也可用於一般矩陣。但是這呼叫取決於矩陣的儲存順序,比如說所有Eigen裡面的矩陣都是列優先儲存的,但也可以修改為行優先。也就是定義的時候加入第四個引數ColMajor和RawMajor。
運算子[]也可以用於基於索引的訪問,但是C++不允許operator接受多個變數,所以就只能用括號比較安全,而 []最多隻能用在向量上。
逗號初始化
矩陣和向量的係數可以透過簡單的所謂逗號初始化進行設定。比如下面這個例子:
Matrix3f
m
;
m
<<
1
,
2
,
3
,
4
,
5
,
6
,
7
,
8
,
9
;
std
:
cout
<<
m
;
重組
一個矩陣當前的大小能夠被row(),cols(),size()檢索,他們分別返回行數、列數、總個數。而重組則是採用動態大小型別矩陣中的resize()。
#include
#include
using
namespace
Eigen
;
int
main
()
{
MatrixXd
m
(
2
,
5
);
m
。
resize
(
4
,
3
);
std
::
cout
<<
“The matrix m is of size ”
<<
m
。
rows
()
<<
“x”
<<
m
。
cols
()
<<
std
::
endl
;
std
::
cout
<<
“It has ”
<<
m
。
size
()
<<
“ coefficients”
<<
std
::
endl
;
VectorXd
v
(
2
);
v
。
resize
(
5
);
std
::
cout
<<
“The vector is of size ”
<<
v
。
size
()
<<
std
::
endl
;
std
::
cout
<<
“As a Matrix,v is of size ”
<<
v
。
rows
()
<<
“x”
<<
v
。
cols
()
<<
std
::
endl
;
}
如果實際的矩陣沒有發生變化,那麼resize()實際上是不起作用的。否則,他可能會改變係數的值。如果你想要在resize()中保持不變數,那麼可以採用conservativeResize()。
所有上述方法對於固定大小矩陣也是有效的,這是為了介面統一。當然,實際上並不能改變固定大小型別矩陣的大小。如果試著修改固定大小的矩陣的大小到另一個不同的大小,那麼就會引發一個插入失敗。
但下面的程式碼是合法的:
#include
#include
using
namespace
Eigen
;
int
main
()
{
Matrix4d
m
;
m
。
resize
(
4
,
4
);
//實際上不進行任何操作
std
::
cout
<<
“The matrix m is of size ”
<<
m
。
rows
()
<<
“x”
<<
m
。
cols
()
<<
std
::
endl
;
}
賦值與重組
賦值是將一個矩陣複製到另一個矩陣,使用運算子=。Eigen會自動重組左手邊的矩陣,從而讓其匹配右手邊的矩陣大小。比如說:
#include
#include
using
namespace
Eigen
;
int
main
()
{
MatrixXf
a
(
2
,
2
);
std
::
cout
<<
“At the begining,a is of size ”
<<
a
。
rows
()
<<
“x”
<<
a
。
cols
()
<<
std
::
endl
;
MatrixXf
b
(
3
,
3
);
a
=
b
;
std
::
cout
<<
“After assignment,a is of size ”
<<
a
。
rows
()
<<
“x”
<<
a
。
cols
()
<<
std
::
endl
;
}
當然,如果左右邊的矩陣是固定大小,那麼這個重組就是沒用的。
如果你不想要自動轉換大小,你可以將其取消掉。具體以後再說吧。
固定大小 vs。 動態大小
什麼更適合固定大小的型別,什麼更適合動態大小的型別?答案是:當矩陣比較小時儘可能使用固定大小,當矩陣比較大或者不得不這麼幹的時候再使用動態大小。對於較小的矩陣,尤其是個數小於16時(4$\times$4)的使用固定大小,這是能有非常良好的效能,他能讓Eigen避免申請動態記憶體並展開迴圈。一般來說,固定大小的矩陣就是一個數組,如果這麼寫:
Matrix4f
mymatrix
;
那實際上就是:
float
mymatrix
[
16
];
所以其幾乎沒有執行時間成本。相反,動態大小的陣列則總是在堆上動態申請記憶體,所以如果這麼些:
MatrixXf
mymatrix
(
rows
,
columns
);
那就相當於是:
float
*
mymatrix
=
new
float
[
rows
*
columns
];
除此之外,MatrixXf物件會把矩陣的行數和列數作為成員變數儲存。
使用固定大小的矩陣的唯一缺陷,就是可能需要在執行過程中獲得矩陣的大小。同樣,當大小超過32時,使用固定大小帶來的效能提升就顯得微乎其微了。更糟糕的是,當我們試圖在函式中建立一個非常大的固定大小的矩陣時有可能導致棧溢位,因此Eigen會試著自動呼叫這個陣列作為當地變數,這與在棧上做的一樣。最後,依賴於環境,Eigen也剋更加積極地艙室向量化(使用SIMD指令),具體參考官網向量化手冊。
可選模板引數
我們在一開始提到了矩陣類一共有六個模板引數,但目前位置我們只討論了前三個。剩下的三個時可選的。下面時模板引數的完整列表:
Matrix
<
typername
Scalar
,
int
RowsAtCompileTime
,
int
ColsAtCompileTime
,
int
Options
=
0
,
int
MaxRowsAtCompileTime
=
RowsAtCompileTime
,
int
MaxColsAtCompileTime
=
ColsAtCompileTime
,
>
Options 是位欄位,這裡我們僅僅討論以為:RowMajor。它規定了矩陣的儲存方式以行儲存為順序。而預設的,則是列儲存順序。比如說,我們需要一個3x3的以行儲存為順序的矩陣:
Matrix
<
float
,
3
,
3
,
RowMajor
>
;
MaxRowsAtCompileTime和MaxColsAtCompileTime只在你指定的時候生效,比如雖然你在編譯的過程中並不知道你的矩陣的確切大小,但是其在編譯過程中的固定上邊界是知道的。一般你這麼做的主要原因是避免申請動態記憶體。比如說接下來的矩陣型別就是用了一組12個浮點型的陣列,而不需要動態記憶體呼叫:
Matrix
<
float
,
Dynamic
,
Dynamic
,
0
,
3
,
4
>