# Task02 幾何變換
2.1 簡介
該部分將對基本的幾何變換進行學習,幾何變換的原理大多都是相似,只是變換矩陣不同,因此,我們以最常用的平移和旋轉爲例進行學習。在深度學習領域,我們常用平移、旋轉、鏡像等操作進行數據增廣;在傳統CV領域,由於某些拍攝角度的問題,我們需要對圖像進行矯正處理,而幾何變換正是這個處理過程的基礎,因此瞭解和學習幾何變換也是有必要的。
這次我們帶着幾個問題進行,以旋轉爲例:
-
1:變換的形式(公式)是什麼?
-
2:旋轉中心是什麼?畢竟以不同位置爲旋轉中心得到的結果是不一樣的。
-
3:採用前向映射還是反向映射?(反向映射更爲有效)
-
4:採用反向映射後,採用何種插值算法?最常用的的是雙線性插值,OpenCV也是默認如此。
2.2 學習目標
-
瞭解幾何變換的概念與應用
-
理解平移、旋轉的原理
-
掌握在OpenCV框架下實現平移、旋轉操作
2.3 內容介紹
1、平移、旋轉的原理
2、OpenCV代碼實踐
3、動手實踐並打卡(讀者完成)
2.4 算法理論介紹
變換形式
先看第一個問題,變換的形式。與OpencV不同的是這裏採取岡薩雷斯的《數字圖像處理_第三版》的變換矩陣方式,關於OpenCV的策略可以看它的官方文檔。根據岡薩雷斯書中的描述,仿射變換的一般形式如下:
式中的T就是變換矩陣,其中 (v,w)爲原座標,(x,y) 爲變換後的座標,不同的變換對應不同的矩陣,這裏也貼出來吧,一些常見的變換矩陣及作用如下表:
也就是說,我們根據自己的目的選擇不同變換矩陣就可以了。
座標系變換
再看第二個問題,變換中心,對於縮放、平移可以以圖像座標原點(圖像左上角爲原點)爲中心變換,這不用座標系變換,直接按照一般形式計算即可。而對於旋轉和偏移,一般是以圖像中心爲原點,那麼這就涉及座標系轉換了。
我們都知道,圖像座標的原點在圖像左上角,水平向右爲 X 軸,垂直向下爲 Y 軸。數學課本中常見的座標系是以圖像中心爲原點,水平向右爲 X 軸,垂直向上爲 Y 軸,稱爲笛卡爾座標系。看下圖:
因此,對於旋轉和偏移,就需要3步(3次變換):
- 將輸入原圖圖像座標轉換爲笛卡爾座標系;
- 進行旋轉計算。旋轉矩陣前面已經給出了;
- 將旋轉後的圖像的笛卡爾座標轉回圖像座標。
圖像座標系與笛卡爾座標系轉換關係:
先看下圖:
在圖像中我們的座標系通常是AB和AC方向的,原點爲A,而笛卡爾直角座標系是DE和DF方向的,原點爲D。
令圖像表示爲M×N的矩陣,對於點A而言,兩座標系中的座標分別是(0,0)和(-N/2,M/2),則圖像某像素點(x’,y’)轉換爲笛卡爾座標(x,y)轉換關係爲,x爲列,y爲行:
逆變換爲:
於是,根據前面說的3個步驟(3次變換),旋轉(順時針旋轉)的變換形式就爲,3次變換就有3個矩陣:
反向映射
看第3個問題,在岡薩雷斯的《數字圖像處理_第三版》中說的很清楚,前向映射就是根據原圖用變換公式直接算出輸出圖像相應像素的空間位置,那麼這會導致一個問題:可能會有多個像素座標映射到輸出圖像的同一位置,也可能輸出圖像的某些位置完全沒有相應的輸入圖像像素與它匹配,也就是沒有被映射到,造成有規律的空洞(黑色的蜂窩狀)。更好的一種方式是採用 反向映射(Inverse Mapping):掃描輸出圖像的位置(x,y),通過
(爲T的逆矩陣)計算輸入圖像對應的位置 (v,w),通過插值方法決定輸出圖像該位置的灰度值。
插值
第4個問題,採用反向映射後,需通過插值方法決定輸出圖像該位置的值,因此需要選擇插值算法。通常有最近鄰插值、雙線性插值,雙三次插值等,OpencV默認採用雙線性插值,我們也就採用雙線性插值。
2.5 基於OpenCV的實現
- 工具:OpenCV3.1.0+VS2013
- 平臺:WIN10
函數原型(c++)
OpenCV仿射變換相關的函數一般涉及到warpAffine和getRotationMatrix2D這兩個:
- 使用OpenCV函數warpAffine 來實現一些簡單的重映射.
- OpenCV函數getRotationMatrix2D 來獲得旋轉矩陣。
1、warpAffined函數詳解
void boxFilter( InputArray src, OutputArray dst,
int ddepth,
Size ksize,
Point anchor = Point(-1,-1),
bool normalize = true,
int borderType = BORDER_DEFAULT );
- 第一個參數,InputArray類型的src,輸入圖像,即源圖像,填Mat類的對象即可。
- 第二個參數,OutputArray類型的dst,函數調用後的運算結果存在這裏,需和源圖片有一樣的尺寸和類型。
- 第三個參數,InputArray類型的M,2×3的變換矩陣。
- 第四個參數,Size類型的dsize,表示輸出圖像的尺寸。
- 第五個參數,int類型的flags,插值方法的標識符。此參數有默認值INTER_LINEAR(線性插值),可選的插值方式如下:
INTER_NEAREST - 最近鄰插值
INTER_LINEAR - 線性插值(默認值)
INTER_AREA - 區域插值
INTER_CUBIC –三次樣條插值
INTER_LANCZOS4 -Lanczos插值
CV_WARP_FILL_OUTLIERS - 填充所有輸出圖像的象素。如果部分象素落在輸入圖像的邊界外,那麼它們的值設定爲 fillval.
CV_WARP_INVERSE_MAP –表示M爲輸出圖像到輸入圖像的反變換,即 。因此可以直接用來做象素插值。否則, warpAffine函數從M矩陣得到反變換。 - 第六個參數,int類型的borderMode,邊界像素模式,默認值爲BORDER_CONSTANT。
- 第七個參數,const Scalar&類型的borderValue,在恆定的邊界情況下取的值,默認值爲Scalar(),即0。
2、getRotationMatrix2D函數詳解
C++: Mat getRotationMatrix2D(Point2f center, double angle, double scale)
參數:
- 第一個參數,Point2f類型的center,表示源圖像的旋轉中心。
- 第二個參數,double類型的angle,旋轉角度。角度爲正值表示向逆時針旋轉(座標原點是左上角)。
- 第三個參數,double類型的scale,縮放係數。## 2.6 總結
實現示例(c++)
1、旋轉
cv::Mat src = cv::imread("lenna.jpg");
cv::Mat dst;
//旋轉角度
double angle = 45;
cv::Size src_sz = src.size();
cv::Size dst_sz(src_sz.height, src_sz.width);
int len = std::max(src.cols, src.rows);
//指定旋轉中心(圖像中點)
cv::Point2f center(len / 2., len / 2.);
//獲取旋轉矩陣(2x3矩陣)
cv::Mat rot_mat = cv::getRotationMatrix2D(center, angle, 1.0);
//根據旋轉矩陣進行仿射變換
cv::warpAffine(src, dst, rot_mat, dst_sz);
//顯示旋轉效果
cv::imshow("image", src);
cv::imshow("result", dst);
cv::waitKey(0);
return 0;
2、平移
cv::Mat src = cv::imread("lenna.jpg");
cv::Mat dst;
cv::Size dst_sz = src.size();
//定義平移矩陣
cv::Mat t_mat =cv::Mat::zeros(2, 3, CV_32FC1);
t_mat.at<float>(0, 0) = 1;
t_mat.at<float>(0, 2) = 20; //水平平移量
t_mat.at<float>(1, 1) = 1;
t_mat.at<float>(1, 2) = 10; //豎直平移量
//根據平移矩陣進行仿射變換
cv::warpAffine(src, dst, t_mat, dst_sz);
//顯示平移效果
cv::imshow("image", src);
cv::imshow("result", dst);
cv::waitKey(0);
return 0;
進階實現(根據原理自己實現)
1、旋轉
/*圖像旋轉(以圖像中心爲旋轉中心)*/
void affine_trans_rotate(cv::Mat& src, cv::Mat& dst, double Angle){
double angle = Angle*CV_PI / 180.0;
//構造輸出圖像
int dst_rows = round(fabs(src.rows * cos(angle)) + fabs(src.cols * sin(angle)));//圖像高度
int dst_cols = round(fabs(src.cols * cos(angle)) + fabs(src.rows * sin(angle)));//圖像寬度
if (src.channels() == 1) {
dst = cv::Mat::zeros(dst_rows, dst_cols, CV_8UC1); //灰度圖初始
}
else {
dst = cv::Mat::zeros(dst_rows, dst_cols, CV_8UC3); //RGB圖初始
}
cv::Mat T1 = (cv::Mat_<double>(3,3) << 1.0,0.0,0.0 , 0.0,-1.0,0.0, -0.5*src.cols , 0.5*src.rows , 1.0); // 將原圖像座標映射到數學笛卡爾座標
cv::Mat T2 = (cv::Mat_<double>(3,3) << cos(angle),-sin(angle),0.0 , sin(angle), cos(angle),0.0, 0.0,0.0,1.0); //數學笛卡爾座標下順時針旋轉的變換矩陣
double t3[3][3] = { { 1.0, 0.0, 0.0 }, { 0.0, -1.0, 0.0 }, { 0.5*dst.cols, 0.5*dst.rows ,1.0} }; // 將數學笛卡爾座標映射到旋轉後的圖像座標
cv::Mat T3 = cv::Mat(3.0,3.0,CV_64FC1,t3);
cv::Mat T = T1*T2*T3;
cv::Mat T_inv = T.inv(); // 求逆矩陣
for (double i = 0.0; i < dst.rows; i++){
for (double j = 0.0; j < dst.cols; j++){
cv::Mat dst_coordinate = (cv::Mat_<double>(1, 3) << j, i, 1.0);
cv::Mat src_coordinate = dst_coordinate * T_inv;
double v = src_coordinate.at<double>(0, 0); // 原圖像的橫座標,列,寬
double w = src_coordinate.at<double>(0, 1); // 原圖像的縱座標,行,高
// std::cout << v << std::endl;
/*雙線性插值*/
// 判斷是否越界
if (int(Angle) % 90 == 0) {
if (v < 0) v = 0; if (v > src.cols - 1) v = src.cols - 1;
if (w < 0) w = 0; if (w > src.rows - 1) w = src.rows - 1; //必須要加上,否則會出現邊界問題
}
if (v >= 0 && w >= 0 && v <= src.cols - 1 && w <= src.rows - 1){
int top = floor(w), bottom = ceil(w), left = floor(v), right = ceil(v); //與映射到原圖座標相鄰的四個像素點的座標
double pw = w - top ; //pw爲座標 行 的小數部分(座標偏差)
double pv = v - left; //pv爲座標 列 的小數部分(座標偏差)
if (src.channels() == 1){
//灰度圖像
dst.at<uchar>(i, j) = (1 - pw)*(1 - pv)*src.at<uchar>(top, left) + (1 - pw)*pv*src.at<uchar>(top, right) + pw*(1 - pv)*src.at<uchar>(bottom, left) + pw*pv*src.at<uchar>(bottom, right);
}
else{
//彩色圖像
dst.at<cv::Vec3b>(i, j)[0] = (1 - pw)*(1 - pv)*src.at<cv::Vec3b>(top, left)[0] + (1 - pw)*pv*src.at<cv::Vec3b>(top, right)[0] + pw*(1 - pv)*src.at<cv::Vec3b>(bottom, left)[0] + pw*pv*src.at<cv::Vec3b>(bottom, right)[0];
dst.at<cv::Vec3b>(i, j)[1] = (1 - pw)*(1 - pv)*src.at<cv::Vec3b>(top, left)[1] + (1 - pw)*pv*src.at<cv::Vec3b>(top, right)[1] + pw*(1 - pv)*src.at<cv::Vec3b>(bottom, left)[1] + pw*pv*src.at<cv::Vec3b>(bottom, right)[1];
dst.at<cv::Vec3b>(i, j)[2] = (1 - pw)*(1 - pv)*src.at<cv::Vec3b>(top, left)[2] + (1 - pw)*pv*src.at<cv::Vec3b>(top, right)[2] + pw*(1 - pv)*src.at<cv::Vec3b>(bottom, left)[2] + pw*pv*src.at<cv::Vec3b>(bottom, right)[2];
}
}
}
}
}
2、平移
/*平移變換*(以圖像左頂點爲原點)/
/****************************************
tx: 水平平移距離 正數向右移動 負數向左移動
ty: 垂直平移距離 正數向下移動 負數向上移動
*****************************************/
void affine_trans_translation(cv::Mat& src, cv::Mat& dst, double tx, double ty){
//構造輸出圖像
int dst_rows = src.rows;//圖像高度
int dst_cols = src.cols;//圖像寬度
if (src.channels() == 1) {
dst = cv::Mat::zeros(dst_rows, dst_cols, CV_8UC1); //灰度圖初始
}
else {
dst = cv::Mat::zeros(dst_rows, dst_cols, CV_8UC3); //RGB圖初始
}
cv::Mat T = (cv::Mat_<double>(3, 3) << 1,0,0 , 0,1,0 , tx,ty,1); //平移變換矩陣
cv::Mat T_inv = T.inv(); // 求逆矩陣
for (int i = 0; i < dst.rows; i++){
for (int j = 0; j < dst.cols; j++){
cv::Mat dst_coordinate = (cv::Mat_<double>(1, 3) << j, i, 1);
cv::Mat src_coordinate = dst_coordinate * T_inv;
double v = src_coordinate.at<double>(0, 0); // 原圖像的橫座標,列,寬
double w = src_coordinate.at<double>(0, 1); // 原圖像的縱座標,行,高
/*雙線性插值*/
// 判斷是否越界
if (v >= 0 && w >= 0 && v <= src.cols - 1 && w <= src.rows - 1){
int top = floor(w), bottom = ceil(w), left = floor(v), right = ceil(v); //與映射到原圖座標相鄰的四個像素點的座標
double pw = w - top; //pw爲座標 行 的小數部分(座標偏差)
double pv = v - left; //pv爲座標 列 的小數部分(座標偏差)
if (src.channels() == 1){
//灰度圖像
dst.at<uchar>(i, j) = (1 - pw)*(1 - pv)*src.at<uchar>(top, left) + (1 - pw)*pv*src.at<uchar>(top, right) + pw*(1 - pv)*src.at<uchar>(bottom, left) + pw*pv*src.at<uchar>(bottom, right);
}
else{
//彩色圖像
dst.at<cv::Vec3b>(i, j)[0] = (1 - pw)*(1 - pv)*src.at<cv::Vec3b>(top, left)[0] + (1 - pw)*pv*src.at<cv::Vec3b>(top, right)[0] + pw*(1 - pv)*src.at<cv::Vec3b>(bottom, left)[0] + pw*pv*src.at<cv::Vec3b>(bottom, right)[0];
dst.at<cv::Vec3b>(i, j)[1] = (1 - pw)*(1 - pv)*src.at<cv::Vec3b>(top, left)[1] + (1 - pw)*pv*src.at<cv::Vec3b>(top, right)[1] + pw*(1 - pv)*src.at<cv::Vec3b>(bottom, left)[1] + pw*pv*src.at<cv::Vec3b>(bottom, right)[1];
dst.at<cv::Vec3b>(i, j)[2] = (1 - pw)*(1 - pv)*src.at<cv::Vec3b>(top, left)[2] + (1 - pw)*pv*src.at<cv::Vec3b>(top, right)[2] + pw*(1 - pv)*src.at<cv::Vec3b>(bottom, left)[2] + pw*pv*src.at<cv::Vec3b>(bottom, right)[2];
}
}
}
}
}
效果
1、旋轉45度
2、平移
相關技術文檔、博客、教材、項目推薦
opencv文檔: https://docs.opencv.org/3.1.0/da/d54/group__imgproc__transform.html#ga0203d9ee5fcd28d40dbc4a1ea4451983
博客:https://blog.csdn.net/weixin_40647819/article/details/87912122
https://www.jianshu.com/p/18cd12e776e1
https://blog.csdn.net/whuhan2013/article/details/53814026
python版本:https://blog.csdn.net/g11d111/article/details/79978582
https://www.kancloud.cn/aollo/aolloopencv/264331 http://www.woshicver.com/FifthSection/4_2_%E5%9B%BE%E5%83%8F%E5%87%A0%E4%BD%95%E5%8F%98%E6%8D%A2/
2.6 總結
該部分對幾何變換的平移和旋轉進行了介紹,讀者可根據提供的資料對相關原理進行學習,然後參考示例代碼自行實現。另外讀者可以嘗試學習並實現其他幾何變換,如偏移。