OpenMVG 的功能模組由若干核心庫組成,本文主要介紹 Image 和 Numeric 兩個庫
1 Image
Image 庫包含影象容器 Image<T>、影象IO讀寫函式 ReadImage() 和 WriteImage()、基本繪圖操作 DrawLine()、DrawCircle() 和 DrawEllipse() 等
1.1 影象容器
Image<T> 是一個影象類泛型容器,T 代表畫素型別,可以是單通道的灰度圖
// 8bit and 32bit gray images
Image<unsigned char> gray_img_8bit;
Image<double> gray_img_32bit;
也可以是 RGB 和 RGBA 等多通道的彩色圖
Image<Rgb<unsigned char>> rgb_img_8bit; // 8bit RGB
Image<Rgb<double> > rgb_img_32bit; // 32bit RGB Image<Rgba<unsigned char> > rgba_img_8bit; // 8bit RGBA
Image<T> 也是一個模板類,繼承自 Eigen 中的“行優先”模板類 Matrix<T, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>,所謂“行優先”,指的是矩陣內元素的儲存順序
以 $A=\begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{bmatrix}$ 為例,行優先時元素在記憶體中的儲存順序為 1-2-3-4-5-6,列優先為 1-4-2-5-3-6
template <typename T>
class Image : public Eigen::Matrix<T, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>
{
// ...
};
Image<T> 的完整類檢視如下,包含建構函式、解構函式、運算子過載函式、獲取高度(行)函式等
1.2 讀寫操作
影象的 IO 讀寫函式,使用比較簡單,如下:
// Read a grayscale image
Image<unit8_t> gray_img;
bool bRet = ReadImage("Foo.imgExtension", &gray_img); // Read a color image
Image<RGBColor> rgb_img;
bool bRet = ReadImage("Foo.imgExtension", &rgb_img);
影象 IO 讀寫函式的實現,稍微複雜,要根據不同的影象格式 (如 jpeg、tiff、png等),呼叫各自的庫來實現 (如 libjpeg、libpng、libtiff 等),ReadImage() -> ReadJpg() -> ReadJpgStream() -> libjpeg
筆者剛接觸影象處理時,並不知道 libjpeg 等庫的存在,曾花了不少時間,嘗試用 c 語言讀寫 jpeg 圖片,現在看來是浪費了時間,並無多大的實際用處
在此摘錄 OpenMVG 中 ReadJpgStream() 的實現程式碼,僅供閱讀參考,希望不要投入過多精力
int ReadJpgStream(FILE * file, std::vector<unsigned char> * ptr, int * w, int * h, int * depth)
{
jpeg_decompress_struct cinfo;
struct my_error_mgr jerr;
cinfo.err = jpeg_std_error(&jerr.pub);
jerr.pub.error_exit = &jpeg_error; if (setjmp(jerr.setjmp_buffer)) {
std::cerr << "Error JPG: Failed to decompress.";
jpeg_destroy_decompress(&cinfo);
return 0;
} jpeg_create_decompress(&cinfo);
jpeg_stdio_src(&cinfo, file);
jpeg_read_header(&cinfo, TRUE);
jpeg_start_decompress(&cinfo); int row_stride = cinfo.output_width * cinfo.output_components; *h = cinfo.output_height;
*w = cinfo.output_width;
*depth = cinfo.output_components;
ptr->resize((*h)*(*w)*(*depth)); unsigned char *ptrCpy = &(*ptr)[0]; while (cinfo.output_scanline < cinfo.output_height) {
JSAMPROW scanline[1] = { ptrCpy };
jpeg_read_scanlines(&cinfo, scanline, 1);
ptrCpy += row_stride;
} jpeg_finish_decompress(&cinfo);
jpeg_destroy_decompress(&cinfo);
return 1;
}
2 Numeric
Numeric 的實現,主要是基於一個開源的 C++ 模板庫 Eigen,它包含了線性代數的基本運算:向量、矩陣、矩陣運算等
2.1 向量和矩陣
Vec2f 和 Vec2 分別表示型別為 float 和 double 的 2d 點 (x, y)
// 2d vector using float internal format
using Vec2f = Eigen::Vector2f; // 2d vector using double internal format
using Vec2 = Eigen::Vector2d;
Vec3f 和 Vec3 分別表示型別為 float 和 double 的 3d 點 (x, y, z)
// 3d vector using float internal format
using Vec3f =Eigen::Vector3f; // 3d vector using double internal format
using Vec3 = Eigen::Vector3d;
Mat 表示通用的一個矩陣;Mat2X 是列儲存形式的一組 2d 點;Mat3X 則是列儲存形式的一組 3d 點
// Unconstrained matrix using double internal format
using Mat = Eigen::MatrixXd; // 2xN matrix using double internal format
using Mat2X = Eigen::Matrix<double, 2, Eigen::Dynamic>;
// 3xN matrix using double internal format
using Mat3X = Eigen::Matrix<double, 3, Eigen::Dynamic>;
2.2 奇異值分解 - SVD
SVD 將一個矩陣分解成三個矩陣的乘積 $ A_{m \times n} = UDV^T$,其中,$U_{m\times m}$ 和 $V_{n \times n}$ 都是正交矩陣, $D_{m \times n}$ 是對角矩陣
在影象的幾何變換中,仿射變換可視為一個奇異值分解的過程,參見博文 OpenCV 之 影象幾何變換
變換過程如下:
$\begin{bmatrix} a_{11} & a_{12} \\ a_{21} & a_{22} \end{bmatrix} = \begin{bmatrix} \cos \theta & -\sin \theta \\ \sin \theta & \cos \theta \end{bmatrix} \begin{bmatrix} \sigma_{1} & \\ & \sigma_2 \end{bmatrix} \begin{bmatrix} \cos \phi & sin \phi \\ -\sin \phi & \cos \phi \end{bmatrix} = UDV^T$
更為形象的描述:第1個圓旋轉 $V^T$得到第2個圓,再經過 $D$ 的拉伸得到第3個橢圓,最後旋轉 $U$ 得到第4個橢圓
2.3 程式碼示例
SVD 的經典應用:求線性方程組 Ax=b 的最小二乘解
MatrixXf A = MatrixXf(3, 2);
A << -1, -0.0827, -0.737, 0.0655, 0.511, -0.562;
cout << "The matrix m:" << endl << A << endl; // SVD decomposition
JacobiSVD<MatrixXf> svd(A, ComputeThinU | ComputeThinV);
cout << "Singular values are:" << endl << svd.singularValues() << endl;
cout << "Left singular vectors U :" << endl << svd.matrixU() << endl;
cout << "Right singular vectors V :" << endl << svd.matrixV() << endl; // solve Ax=b
Vector3f b(1, 0, 0);
cout << "A least-squares solution of m*x = rhs is:" << endl << svd.solve(b) << endl;
OpenCV 中也有求解 Ax=b 最小二乘解的函式 solve(InputArray src1, InputArray src2, OutpuArray dst, int flags = DECOMP_LU)
cv::Mat A = (cv::Mat_<float>(3, 2) << -1, - 0.0827, -0.737, 0.0655, 0.511, -0.562);
cv::Mat b = (cv::Mat_<float>(3, 1) << 1.0, 0.0, 0.0);
cv::Mat x; // solve Ax=b
cv::solve(A, b, x, cv::DECOMP_SVD);
cout << "An OpenCV solution of Ax=b is: " << endl << x << endl;
從結果來看,Eigen 和 OpenCV 的求解基本一致
3 與 OpenCV 的轉換
OpenCV 中也有一個表示影象容器的模板類 Mat,參見博文 OpenCV 之 Mat 類,二者的轉換關係如下:
1)cv::Mat 轉換為 Image (灰度圖)
// cv Mat -> mvg Image
cv::Mat img_cv = cv::imread("messi.jpg", cv::IMREAD_GRAYSCALE); Image<uint8_t> img_mvg;
img_mvg.resize(img_cv.cols, img_cv.rows); // convert and save
cv::cv2eigen(img_cv, *(Image<uint8_t>::Base*) &img_mvg);
WriteImage("messi_mvg.jpg", img_mvg);
2)cv::Mat 轉換為 Image (彩色圖)
cv::Mat img_cv;
img_cv = cv::imread("messi.jpg"); Image<RGBColor> img_mvg;
img_mvg.resize(img_cv.cols, img_cv.rows);
cv::cvtColor(img_cv, img_cv, cv::COLOR_BGR2RGB); // convert and save
memcpy(img_mvg.data(), static_cast<unsigned char*>(img_cv.data), img_cv.cols * img_cv.rows * 3);
WriteImage("messi_mvg.jpg", img_mvg);
3)Image 轉換為 cv::Mat
// Read a grayscale image
Image<unsigned char> img_mvg;
bool bRet = ReadImage("messi.jpg", &img_mvg); // mvg Image -> cv Mat
cv::Mat img_cv;
cv::eigen2cv(img_mvg.GetMat(), img_cv); // show image
cv::imshow("messi", img_cv);
cv::waitKey();
轉換後的圖片結果:
參考資料
《Introduction to Linear Algebra》 7.4 The Geometry of the SVD