【數字影象處理】六.MFC空間幾何變換之影象平移、映象、旋轉、縮放詳解
本文主要講述基於VC++6.0 MFC影象處理的應用知識,主要結合自己大三所學課程《數字影象處理》及課件進行講解,主要通過MFC單文件檢視實現顯示BMP圖片空間幾何變換,包括影象平移、圖形旋轉、影象反轉倒置映象和影象縮放的知識。同時文章比較詳細基礎,沒有采用GDI+獲取矩陣,而是通過讀取BMP圖片資訊頭和矩陣畫素實現變換,希望該篇文章對你有所幫助,尤其是初學者和學習影象處理的學生。
【數字影象處理】一.MFC詳解顯示BMP格式圖片
【數字影象處理】二.MFC單文件分割視窗顯示圖片
【數字影象處理】三.MFC實現影象灰度、取樣和量化功能詳解
【數字影象處理】四.MFC對話方塊繪製灰度直方圖
【數字影象處理】五.MFC影象點運算之灰度線性變化、灰度非線性變化、閾值化和均衡化處理詳解
免費資源下載地址:
http://download.csdn.net/detail/eastmount/8772951
![](https://i.iter01.com/images/283f31fa73ba7bf4edbe6adce8ff5771c5285d39972c53b68ab95cd721dca26e.png)
![](https://i.iter01.com/images/658400b8ba303d6ffbff4be5b2e3eb2bae7f371884a320f5b215c2ec36651121.png)
2.垂直映象倒轉
其中變換矩陣如下:
X=X0
Y=height-Y0-1 (height為影象高度)
它相當於把原圖的畫素矩陣的最後一行畫素值賦值給第一行,首先找到(0,0)對應的(height-1,0)畫素值,然後依次賦值該行的畫素資料;最後當前行賦值結束,依次下一行。重點是找到每行的第一個畫素點即可。
程式碼中引用兩個變數:Place=(m_nWidth*3)*(m_nHeight-1-1)即是(height-1,0)最後一行的第一個畫素點;然後是迴圈中Place=(m_nWidth*3)*(m_nHeight-number-1)找到每行的第一個畫素點。
同樣通過類嚮導生成函式void CImageProcessingView::OnJhbhDz(),程式碼如下:
![](https://i.iter01.com/images/96ef600d4d8947d30e7f03996578f97c94d44406bbc5dc8c89d8cfee8e37e1ed.png)
![](https://i.iter01.com/images/97f336950de90415c0a77523bd4d3f344c3e2b85a3f6e3f86c278cdb6c2cddba.png)
![](https://i.iter01.com/images/99a8cccf50706819941ee244b8c1069b05ab74695fb26b1c8efca4a756083f64.png)
![](https://i.iter01.com/images/5f946736061562f64e638a433d57d44ea7949e8aeed838d2b5424b7b3215c6d7.png)
![](https://i.iter01.com/images/c533f7e7471aa374b8e7552b533b1b430be39a0a54f8516f3e552aa4a67ddba6.png)
換個通熟的說法,如下圖所示。採用最近鄰插值法就是P(x,y)畫素值採用四捨五入等於離它最近的輸入影象畫素值。分別計算它到四個頂點之間的距離,但是這樣會造成影象的馬賽克、鋸齒等現象。而採用雙線性插值法,主要通過該座標周圍的四個畫素值,按照比例混合計算器近似值。比例混合的依據是離哪個畫素近,哪個畫素的比例越大。
![](https://i.iter01.com/images/20b29c43c2868bf87bf16ba87fff22665eea8fff4b12ff2c4009a432072f2267.png)
【數字影象處理】一.MFC詳解顯示BMP格式圖片
【數字影象處理】二.MFC單文件分割視窗顯示圖片
【數字影象處理】三.MFC實現影象灰度、取樣和量化功能詳解
【數字影象處理】四.MFC對話方塊繪製灰度直方圖
【數字影象處理】五.MFC影象點運算之灰度線性變化、灰度非線性變化、閾值化和均衡化處理詳解
免費資源下載地址:
http://download.csdn.net/detail/eastmount/8772951
一. 影象平移
前一篇文章講述了影象點運算(基於畫素的影象變換),這篇文章講述的是影象幾何變換:在不改變影象內容的情況下對影象畫素進行空間幾何變換的處理方式。
點運算對單幅影象做處理,不改變畫素的空間位置;代數運算對多幅影象做處理,也不改變畫素的空間位置;幾何運算對單幅影象做處理,改變畫素的空間位置,幾何運算包括兩個獨立的演算法:空間變換演算法和灰度級插值演算法。
點運算對單幅影象做處理,不改變畫素的空間位置;代數運算對多幅影象做處理,也不改變畫素的空間位置;幾何運算對單幅影象做處理,改變畫素的空間位置,幾何運算包括兩個獨立的演算法:空間變換演算法和灰度級插值演算法。
空間變換操作包括簡單空間變換、多項式卷繞和幾何校正、控制柵格插值和影象卷繞,這裡主要講述簡單的空間變換,如影象平移、映象、縮放和旋轉。主要是通過線性代數中的齊次座標變換。
影象平移座標變換如下:
![](https://i.iter01.com/images/a60cd0683b837711838be206ceca789ac8f844548e10fe31fd3e635c01ebd98c.png)
影象平移座標變換如下:
![](https://i.iter01.com/images/a60cd0683b837711838be206ceca789ac8f844548e10fe31fd3e635c01ebd98c.png)
執行效果如下圖所示,其中BMP圖片(0,0)畫素點為左下角。
![](https://i.iter01.com/images/b613817cadc733dc640330afa06c12b4a1341cb138629f68b34f0caa2f3faf8d.png)
其程式碼核心演算法:
1.在對話方塊中輸入平移座標(x,y) m_xPY=x,m_yPY=y
2.定義Place=dlg.m_yPY*m_nWidth*3 表示當前m_yPY行需要填充為黑色
3.新建一個畫素矩陣 ImageSize=new unsigned char[m_nImage]
4.迴圈整個畫素矩陣處理
for(int i=0 ; i<m_nImage ; i++ ){
if(i<Place) {ImageSize[i]=black; continue;} //黑色填充底部 從小往上繪圖
else if(i>=Place && countWidth<dlg.m_xPY*3) {//黑色填充左部分
ImageSize[i]=black; countWidth++; continue;
}
else if(i>=Place && countWidth>=dlg.m_xPY*3) {//影象畫素平移區域
ImageSize[i]=m_pImage[m_pImagePlace];//原(0,0)畫素賦值過去
m_pImagePlace++; countWidth++;
if(countWidth==m_nWidth*3) { //一行填滿 m_pImagePlace走到(0,1)
number++; m_pImagePlace=number*m_nWidth*3;
}
}
}
5.寫檔案繪圖fwrite(ImageSize,m_nImage,1,fpw)
第一步:在ResourceView資源檢視中,新增Menu子選單如下:(注意ID號)
![](https://i.iter01.com/images/268c74c54bf30b41a5faf3e3b090b04144f5669f7946579b9667fb4833cb83f5.png)
![](https://i.iter01.com/images/e426481b03e5e6154c5ddde3600ca85f26242647cec49c9c962c9ba5a80f7655.png)
![](https://i.iter01.com/images/4a3d957f08e86d36cb62db0495796c0503e2d0fba995dcbd920c0fed04b067b4.png)
![](https://i.iter01.com/images/b613817cadc733dc640330afa06c12b4a1341cb138629f68b34f0caa2f3faf8d.png)
其程式碼核心演算法:
1.在對話方塊中輸入平移座標(x,y) m_xPY=x,m_yPY=y
2.定義Place=dlg.m_yPY*m_nWidth*3 表示當前m_yPY行需要填充為黑色
3.新建一個畫素矩陣 ImageSize=new unsigned char[m_nImage]
4.迴圈整個畫素矩陣處理
for(int i=0 ; i<m_nImage ; i++ ){
if(i<Place) {ImageSize[i]=black; continue;} //黑色填充底部 從小往上繪圖
else if(i>=Place && countWidth<dlg.m_xPY*3) {//黑色填充左部分
ImageSize[i]=black; countWidth++; continue;
}
else if(i>=Place && countWidth>=dlg.m_xPY*3) {//影象畫素平移區域
ImageSize[i]=m_pImage[m_pImagePlace];//原(0,0)畫素賦值過去
m_pImagePlace++; countWidth++;
if(countWidth==m_nWidth*3) { //一行填滿 m_pImagePlace走到(0,1)
number++; m_pImagePlace=number*m_nWidth*3;
}
}
}
5.寫檔案繪圖fwrite(ImageSize,m_nImage,1,fpw)
第一步:在ResourceView資源檢視中,新增Menu子選單如下:(注意ID號)
![](https://i.iter01.com/images/268c74c54bf30b41a5faf3e3b090b04144f5669f7946579b9667fb4833cb83f5.png)
第二步:設定平移對話方塊。將試圖切換到ResourceView介面--選中Dialog,右鍵滑鼠新建一個Dialog,並新建一個名為IDD_DIALOG_PY。編輯框(X)IDC_EDIT_PYX
和 (Y)IDC_EDIT_PYY,確定為預設按鈕。設定成下圖對話方塊:
![](https://i.iter01.com/images/e426481b03e5e6154c5ddde3600ca85f26242647cec49c9c962c9ba5a80f7655.png)
第三步:在對話方塊資源模板空白區域雙擊滑鼠—Create a new class建立一個新類--命名為CImagePYDlg。會自動生成它的.h和.cpp檔案。開啟類嚮導(Ctrl
W),選擇類名:CImagePYDlg新增成員變數如下圖所示,同時在Message Maps中生成ID_JHBH_PY實現函式。
![](https://i.iter01.com/images/748fe9a82094ce3dc4f974fa5a279f444ebbd28e7c523c9c0d877a5f309b8c9d.png)
![](https://i.iter01.com/images/4a3d957f08e86d36cb62db0495796c0503e2d0fba995dcbd920c0fed04b067b4.png)
第四步:在CImageProcessingView.cpp中新增標頭檔案#include "ImagePYDlg.h",並實現平移。
例如一行畫素為97位元組,我們就需要補3個位元組嗎,數值可以是0,但是我們在BMP格式的資訊頭裡說明了其寬度,所以補齊後對我們沒有影響,所以後面補若干個位元組的0即可直到被4整除。
![](https://i.iter01.com/images/42cf7424a9e852fc597e47d3b09056ef8d00fc9e3ecbf0b67e40099dd579fb14.png)
/********************************************************/
/* 影象空間幾何變換:影象平移 ID_JHBH_PY(幾何變換-平移)
/* 使用平移對話方塊:CImagePYDlg dlg
/* 演算法:f(x,y)=f(x+x0,y+y0)影象所有點平移,空的補黑'0'
/* 注意該影象平移方法只是從左上角(0,0)處開始平移
/* 其他方向原理相同 自己去實現
/********************************************************/
void CImageProcessingView::OnJhbhPy()
{
if(numPicture==0) {
AfxMessageBox("載入圖片後才能空間平移!",MB_OK,0);
return;
}
//定義取樣對話方塊也是用來空間變換平移的座標
CImagePYDlg dlg;
if( dlg.DoModal()==IDOK ) //顯示對話方塊
{
//取樣座標最初為圖片的自身畫素
if( dlg.m_xPY>m_nWidth || dlg.m_yPY>m_nHeight ) {
AfxMessageBox("圖片平移不能為超過原圖長寬!",MB_OK,0);
return;
}
AfxMessageBox("圖片空間變換-平移!",MB_OK,0);
//開啟臨時的圖片 讀寫檔案
FILE *fpo = fopen(BmpName,"rb");
FILE *fpw = fopen(BmpNameLin,"wb+");
fread(&bfh,sizeof(BITMAPFILEHEADER),1,fpo);
fread(&bih,sizeof(BITMAPINFOHEADER),1,fpo);
fwrite(&bfh,sizeof(BITMAPFILEHEADER),1,fpw);
fwrite(&bih,sizeof(BITMAPINFOHEADER),1,fpw);
fread(m_pImage,m_nImage,1,fpo);
/************************************************************/
/* 圖片空間變換-平移
/* 座標(dlg.m_xPY,dlg.m_yPY)表示影象平移的座標
/* 先用Plave計算出平移後的起始座標,其他的座標賦值為'0'黑色
/* 然後依次平移座標,空的賦為黑色,否則填充
/************************************************************/
/******************************************************************/
/* 嚴重錯誤1:陣列變數賦值相等
/* 在View.h中定義變數 BYTE *m_pImage 讀入圖片資料後的指標
/* 建立臨時變數陣列,讓它平移變換 unsigned char *ImageSize
/* ImageSize=m_pImage(錯誤)
/* 會導致ImageSize賦值變換時m_pImage也產生了變換,所以輸出全為黑色
/* 因為它倆指向了相同的陣列地址
/* 解決方法:使用下面C++的new方法動態分配或for迴圈i=m_nImage賦值
/******************************************************************/
/*臨時變數儲存的畫素與m_pImage相同,便於處理影象*/
unsigned char *ImageSize;
ImageSize=new unsigned char[m_nImage]; //new和delete有效的進行動態記憶體的分配和釋放
int Place; //建立臨時座標 記錄起始座標(0,0)平移過來的位置
int m_pImagePlace; //原始影象平移為(0,0) 影象把它平移到Place位置
unsigned char black; //填充黑色='0'
/************************************************************/
/* for(int i=0 ; i<m_nHeight ; i++ )
/* for(int j=0 ; j<m_nWidth ; j++ )
/* 不能使用的上面的因為可能影象的最後一行沒有完整的一行畫素
/* 這樣會出現exe報錯,使用m_nImage讀寫所有畫素比較正確
/************************************************************/
Place=dlg.m_yPY*m_nWidth*3; //前m_yPY行都要填充為黑色
black=0; //顏色為黑色
m_pImagePlace=0; //影象處事位置為(0,0),把該點畫素平移過去
int countWidth=0; //記錄每行的畫素個數,滿行時變回0
int number=0; //數字記錄使用的畫素行數,平移時使用
for(int i=0 ; i<m_nImage ; i++ )
{
/*如果每行的畫素填滿時清為0*/
if(countWidth==m_nWidth*3) {
countWidth=0;
}
/*第一部分:到平移後畫素位置前面的所有畫素點賦值為黑色*/
if(i<Place) {
ImageSize[i]=black; //賦值為黑色
continue;
}
/*第二部分:平移區域的左邊部分賦值為黑色*/
else if(i>=Place && countWidth<dlg.m_xPY*3) { //RGB乘3
ImageSize[i]=black; //賦值為黑色
countWidth++;
continue;
}
/****************************/
/* 各部分如圖所示:
/* 000000000000000000000000
/* 000000000000000000000000
/* 0000000.................
/* 0000000.................
/* 0000000.................
/* 0000000.................
/* 點表示畫素部分,0為黑色
/****************************/
/* 重點錯誤提示:由於bmp影象顯示是從左下角開始儲存(0,0)點所以輸出影象為 */
/* bmp影象是從左下角到右上角排列的 */
/****************************/
/* 各部分如圖所示:
/* 0000000.................
/* 0000000.................
/* 0000000.................
/* 0000000.................
/* 000000000000000000000000
/* 000000000000000000000000
/* 點表示畫素部分,0為黑色
/****************************/
/*第三部分:影象畫素平移區域*/
else if(i>=Place && countWidth>=dlg.m_xPY*3)
{
ImageSize[i]=m_pImage[m_pImagePlace];
m_pImagePlace++;
countWidth++;
if(countWidth==m_nWidth*3)
{
number++;
m_pImagePlace=number*m_nWidth*3;
}
}
}
fwrite(ImageSize,m_nImage,1,fpw);
fclose(fpo);
fclose(fpw);
numPicture = 2;
level=200; //200表示幾何變換
Invalidate();
}
}
同時在ShowBitmap中新增level標記重新繪製圖片,程式碼如下:else //影象幾何變換
if(level=200)
{
m_hBitmapChange = (HBITMAP) LoadImage(NULL,BmpNameLin,IMAGE_BITMAP,0,0,
LR_LOADFROMFILE|LR_DEFAULTSIZE|LR_CREATEDIBSECTION);
}
執行時需要注意一點:BMP影象在處理過程中可能會出現一些斜線,而平移(40,60)位移量時可能出現如下。他是因為BMP格式有個非常重要的規定,要求每一掃描的位元組資料必須能被4整除,也就是Dword對齊(長度4位元組),如果影象的一行位元組數不能被4整除,就需要在每行末尾不起0達到標準。例如一行畫素為97位元組,我們就需要補3個位元組嗎,數值可以是0,但是我們在BMP格式的資訊頭裡說明了其寬度,所以補齊後對我們沒有影響,所以後面補若干個位元組的0即可直到被4整除。
![](https://i.iter01.com/images/dd0178a6b471a5e93e8a59cc90da421a1f7a5e94b6134cb29d7e36dd7bd44e1e.png)
![](https://i.iter01.com/images/42cf7424a9e852fc597e47d3b09056ef8d00fc9e3ecbf0b67e40099dd579fb14.png)
通過後面的影象縮放後,我從學做了一遍這個補齊的縮放。程式碼如下,能夠實現完美平移。nice啊~
void CImageProcessingView::OnJhbhPy()
{
if(numPicture==0) {
AfxMessageBox("載入圖片後才能空間平移!",MB_OK,0);
return;
}
//定義取樣對話方塊也是用來空間變換平移的座標
CImagePYDlg dlg;
if( dlg.DoModal()==IDOK ) //顯示對話方塊
{
//取樣座標最初為圖片的自身畫素
if( dlg.m_xPY>m_nWidth || dlg.m_yPY>m_nHeight ) {
AfxMessageBox("圖片平移不能為超過原圖長寬!",MB_OK,0);
return;
}
AfxMessageBox("圖片空間變換-平移!",MB_OK,0);
//開啟臨時的圖片 讀寫檔案
FILE *fpo = fopen(BmpName,"rb");
FILE *fpw = fopen(BmpNameLin,"wb+");
fread(&bfh,sizeof(BITMAPFILEHEADER),1,fpo);
fread(&bih,sizeof(BITMAPINFOHEADER),1,fpo);
int num; //記錄每行多餘的影象素數個數
int sfSize; //補齊後的影象大小
//重點:影象的每行畫素都必須是4的倍數:1*1的影象為 r g b 00H
if(m_nWidth*3%4!=0)
{
num=(4-m_nWidth*3%4);
sfSize=(m_nWidth*3+num)*m_nHeight; //每行多number個
}
else
{
num=0;
sfSize=m_nWidth*m_nHeight*3;
}
//注意:假如最後一行畫素不足,我預設處理為完整的一行,不足補00H
//總之處理後的影象總是m*n且為4倍數,每行都完整存在
/*更改檔案頭資訊 定義臨時檔案頭結構變數*/
BITMAPFILEHEADER bfhsf;
BITMAPINFOHEADER bihsf;
bfhsf=bfh;
bihsf=bih;
bfhsf.bfSize=sfSize+54;
fwrite(&bfhsf,sizeof(BITMAPFILEHEADER),1,fpw);
fwrite(&bihsf,sizeof(BITMAPINFOHEADER),1,fpw);
fread(m_pImage,m_nImage,1,fpo);
CString str;
str.Format("補齊=%d",num);
AfxMessageBox(str);
/*臨時變數儲存的畫素與sfSize相同 new和delete有效的進行動態記憶體的分配和釋放*/
unsigned char *ImageSize;
ImageSize=new unsigned char[sfSize];
int Place; //建立臨時座標 記錄起始座標(0,0)平移過來的位置
int m_pImagePlace; //原始影象平移為(0,0) 影象把它平移到Place位置
unsigned char black=0; //填充黑色='0'
unsigned char other=0; //補碼00H='\0'
Place=dlg.m_yPY*(m_nWidth*3+num); //前m_yPY行都要填充為黑色
m_pImagePlace=0; //影象處事位置為(0,0),把該點畫素平移過去
int countWidth=0; //記錄每行的畫素個數,滿行時變回0
int number=0; //數字記錄使用的畫素行數,平移時使用
for(int i=0 ; i<sfSize ; i++ )
{
/*第一部分:到平移後畫素位置前面的所有畫素點賦值為黑色*/
if(i<Place)
{
ImageSize[i]=black; //賦值為黑色
continue;
}
/*第二部分:平移區域的左邊部分賦值為黑色*/
else if(i>=Place && countWidth<dlg.m_xPY*3) //RGB乘3
{
ImageSize[i]=black; //賦值為黑色
countWidth++;
continue;
}
/*第三部分:影象畫素平移區域*/
else if(i>=Place && countWidth>=dlg.m_xPY*3)
{
ImageSize[i]=m_pImage[m_pImagePlace];
m_pImagePlace++;
countWidth++;
if(countWidth==m_nWidth*3)
{
if(num==0)
{
countWidth=0;
number++;
m_pImagePlace=number*m_nWidth*3;
}
else //num為補0
{
for(int j=0;j<num;j++)
{
i++;
ImageSize[i]=other;
}
countWidth=0;
number++;
m_pImagePlace=number*(m_nWidth*3+num); //重點:新增Num
}
}
}
}
fwrite(ImageSize,sfSize,1,fpw);
fclose(fpo);
fclose(fpw);
numPicture = 2;
level=200; //200表示幾何變換
Invalidate();
}
}
執行效果如下圖所示,完美平移,其他演算法遇到斜線問題類似補齊即可。![](https://i.iter01.com/images/9d3c6c4eb0a84d4b812c4cd126723dab1e32443d2224f3d6f0afb4f20c422277.png)
二. 影象映象
1.水平映象翻轉
其變換矩陣如下:
X=width-X0-1 (width為影象寬度)
Y=Y0
開啟類嚮導,在CImageProcessingView中新增IDs為ID_JHBH_FZ,生成函式,程式碼如下:
/* 幾何變換 影象翻轉:自己對這個功能比較感興趣,做個影象反轉 */
void CImageProcessingView::OnJhbhFz()
{
if(numPicture==0) {
AfxMessageBox("載入圖片後才能空間反轉!",MB_OK,0);
return;
}
AfxMessageBox("圖片空間變換-反轉影象!",MB_OK,0);
//開啟臨時的圖片
FILE *fpo = fopen(BmpName,"rb");
FILE *fpw = fopen(BmpNameLin,"wb+");
fread(&bfh,sizeof(BITMAPFILEHEADER),1,fpo);
fread(&bih,sizeof(BITMAPINFOHEADER),1,fpo);
fwrite(&bfh,sizeof(BITMAPFILEHEADER),1,fpw);
fwrite(&bih,sizeof(BITMAPINFOHEADER),1,fpw);
fread(m_pImage,m_nImage,1,fpo);
/*new和delete有效的進行動態記憶體的分配和釋放*/
unsigned char *ImageSize;
ImageSize=new unsigned char[m_nImage];
int countWidth=0; //記錄每行的畫素個數,滿行時變回0
int Place; //記錄影象每行的位置,便於影象反轉
int number=0; //數字記錄使用的畫素行數
Place=m_nWidth*3-1;
//翻轉矩陣: y=y0 x=width-x0-1
for(int i=0 ; i<m_nImage ; i++ )
{
if(countWidth==m_nWidth*3)
{
countWidth=0;
}
ImageSize[i]=m_pImage[Place]; //(0,0)賦值(0,width*3-1)畫素
Place--;
countWidth++;
if(countWidth==m_nWidth*3)
{
number++;
Place=number*m_nWidth*3-1;
}
}
fwrite(ImageSize,m_nImage,1,fpw);
fclose(fpo);
fclose(fpw);
numPicture = 2;
level=200;
Invalidate();
}
執行效果如下圖所示,其中還是存在一些小BUG,如前面的BMP圖補0湊齊4整數倍寬度或顏色失幀。![](https://i.iter01.com/images/283f31fa73ba7bf4edbe6adce8ff5771c5285d39972c53b68ab95cd721dca26e.png)
![](https://i.iter01.com/images/658400b8ba303d6ffbff4be5b2e3eb2bae7f371884a320f5b215c2ec36651121.png)
2.垂直映象倒轉
其中變換矩陣如下:
X=X0
Y=height-Y0-1 (height為影象高度)
它相當於把原圖的畫素矩陣的最後一行畫素值賦值給第一行,首先找到(0,0)對應的(height-1,0)畫素值,然後依次賦值該行的畫素資料;最後當前行賦值結束,依次下一行。重點是找到每行的第一個畫素點即可。
程式碼中引用兩個變數:Place=(m_nWidth*3)*(m_nHeight-1-1)即是(height-1,0)最後一行的第一個畫素點;然後是迴圈中Place=(m_nWidth*3)*(m_nHeight-number-1)找到每行的第一個畫素點。
![](https://i.iter01.com/images/9b4ffe758caa42bdb63e1eb8924bdc7ade8fcf78bef369fdf80f125279f3a23f.png)
/* 幾何變換 影象倒轉 */
void CImageProcessingView::OnJhbhDz()
{
if(numPicture==0) {
AfxMessageBox("載入圖片後才能空間反轉!",MB_OK,0);
return;
}
AfxMessageBox("圖片空間變換-反轉影象!",MB_OK,0);
//開啟臨時的圖片
FILE *fpo = fopen(BmpName,"rb");
FILE *fpw = fopen(BmpNameLin,"wb+");
fread(&bfh,sizeof(BITMAPFILEHEADER),1,fpo);
fread(&bih,sizeof(BITMAPINFOHEADER),1,fpo);
fwrite(&bfh,sizeof(BITMAPFILEHEADER),1,fpw);
fwrite(&bih,sizeof(BITMAPINFOHEADER),1,fpw);
fread(m_pImage,m_nImage,1,fpo);
/*new和delete有效的進行動態記憶體的分配和釋放*/
unsigned char *ImageSize;
ImageSize=new unsigned char[m_nImage];
int countWidth=0; //記錄每行畫素個數,滿行時變回0
int Place; //每列位置
int number=0; //畫素行數
Place=(m_nWidth*3)*(m_nHeight-1-1); //0行儲存
//翻轉矩陣: x=x0 y=height-y0-1
for(int i=0 ; i<m_nImage ; i++ )
{
ImageSize[i]=m_pImage[Place]; //(0,0)賦值(0,0)畫素
Place++;
countWidth++;
if(countWidth==m_nWidth*3)
{
countWidth=0;
number++;
Place=(m_nWidth*3)*(m_nHeight-number-1);
}
}
fwrite(ImageSize,m_nImage,1,fpw);
fclose(fpo);
fclose(fpw);
numPicture = 2;
level=200;
Invalidate();
}
執行結果如下圖所示,第二張圖顏色沒有失幀或變灰,這完全可以懷疑在翻轉過程中RGB畫素程式設計BGR後導致的結果,最終實現了翻轉影象,但灰度存在一定;所以如果改為RBG順序不變化即可原圖顏色顯示。![](https://i.iter01.com/images/96ef600d4d8947d30e7f03996578f97c94d44406bbc5dc8c89d8cfee8e37e1ed.png)
![](https://i.iter01.com/images/97f336950de90415c0a77523bd4d3f344c3e2b85a3f6e3f86c278cdb6c2cddba.png)
三. 影象旋轉
影象饒原點旋轉順時針theta角矩陣變換如下:注意BMP影象(0,0)左下角
![](https://i.iter01.com/images/99a8cccf50706819941ee244b8c1069b05ab74695fb26b1c8efca4a756083f64.png)
寫到這裡真心覺得寫底層的程式碼非常困難啊!尤其是以為畫素轉換二維畫素,同時也覺得當時的自己演算法部分還是很強大的,也感覺到如果採用GDI+操作畫素矩陣Matrix或ColorMatrix是多麼的方便,因為它定義好了X和Y向量,這就是為什麼Android前面寫的影象處理要容易得多。但是效率高~
好像利用GDI+旋轉通過幾句程式碼即可:
matrix.Rotate(15); //矩陣旋轉15度
graph.SetTransform(&matrix);
graph.DrawImage(&image,points,3);
下面這部分程式碼是實現Android旋轉的:參考我的部落格
![](https://i.iter01.com/images/5b7f2a6c45fd1408888c3bf8267e9c4c366ba1431efb4ba74bd6c9cce8b1713b.jpg)
好像利用GDI+旋轉通過幾句程式碼即可:
matrix.Rotate(15); //矩陣旋轉15度
graph.SetTransform(&matrix);
graph.DrawImage(&image,points,3);
下面這部分程式碼是實現Android旋轉的:參考我的部落格
//旋轉圖片
private void TurnPicture() {
Matrix matrix = new Matrix();
turnRotate=turnRotate+15;
//選擇角度 饒(0,0)點選擇 正數順時針 負數逆時針 中心旋轉
matrix.setRotate(turnRotate,bmp.getWidth()/2,bmp.getHeight()/2);
Bitmap createBmp = Bitmap.createBitmap(bmp.getWidth(), bmp.getHeight(), bmp.getConfig());
Canvas canvas = new Canvas(createBmp);
Paint paint = new Paint();
canvas.drawBitmap(bmp, matrix, paint);
imageCreate.setBackgroundColor(Color.RED);
imageCreate.setImageBitmap(createBmp);
textview2.setVisibility(View.VISIBLE);
}
實現效果如下圖所示:![](https://i.iter01.com/images/5b7f2a6c45fd1408888c3bf8267e9c4c366ba1431efb4ba74bd6c9cce8b1713b.jpg)
言歸正傳,新建Dialog如下圖所示,設定ID_DIALOG_XZ和變數:
![](https://i.iter01.com/images/25260bef0e358298eb1e0d688a71077469b957022fb38a55424ba1ce94185dfc.png)
![](https://i.iter01.com/images/25260bef0e358298eb1e0d688a71077469b957022fb38a55424ba1ce94185dfc.png)
再點選空白處建立CImageXZDlg類(旋轉),它會自動生成.h和.cpp檔案。開啟類嚮導生成CImageXZDlg類的成員變數m_xzds(旋轉度數),並設定其為int型(最大值360 最小值0)。
在類嚮導(Ctrl+W)選擇類CImageProcessingView,為ID_JHBH_TXXZ(影象旋轉)新增函式,同時新增標頭檔案#include "ImageXZDlg.h"
![](https://i.iter01.com/images/12ed94a42f9e699725b59514b829dd6682fe1c0fcbc33dcf71f00bf07129672f.png)
![](https://i.iter01.com/images/450fc1247ab2d20eeaaf4c5a86a06358bde6d2067c9a148bf9b083cdd60e1e05.png)
在類嚮導(Ctrl+W)選擇類CImageProcessingView,為ID_JHBH_TXXZ(影象旋轉)新增函式,同時新增標頭檔案#include "ImageXZDlg.h"
/**********************************************************/
/* 幾何變換:圖片旋轉
/* 先新增對話方塊:IDD_JHBH_TXXZ(影象旋轉),建立新類CImageXZDlg
/* 建立輸入度數的:m_xzds Member variables 為int 0-360間
/**********************************************************/
void CImageProcessingView::OnJhbhTxxz()
{
if(numPicture==0) {
AfxMessageBox("載入圖片後才能空間旋轉!",MB_OK,0);
return;
}
//定義對話方塊並呼叫對話方塊
CImageXZDlg dlg;
if( dlg.DoModal()==IDOK ) //顯示對話方塊
{
AfxMessageBox("圖片空間變換-旋轉影象!",MB_OK,0);
//讀寫檔案
FILE *fpo = fopen(BmpName,"rb");
FILE *fpw = fopen(BmpNameLin,"wb+");
fread(&bfh,sizeof(BITMAPFILEHEADER),1,fpo);
fread(&bih,sizeof(BITMAPINFOHEADER),1,fpo);
fwrite(&bfh,sizeof(BITMAPFILEHEADER),1,fpw);
fwrite(&bih,sizeof(BITMAPINFOHEADER),1,fpw);
fread(m_pImage,m_nImage,1,fpo);
/*new和delete有效的進行動態記憶體的分配和釋放*/
unsigned char *ImageSize;
ImageSize=new unsigned char[m_nImage];
int Place; //記錄影象每行的位置,便於影象旋轉
/*定義PA=3.14時使用的方法是arcsin(1.0/2)*6即為π*/
double PA;
PA=asin(0.5)*6;
/*把輸入的0-360的正整數度數轉換為角度,30度=π/6*/
double degree;
degree=PA*dlg.m_xzds/180; //呼叫dlg.m_xzds(旋轉度數)
//對應的二維矩陣 注意影象矩陣從左下角開始處理 它最終要轉換成一維儲存
int X,Y; //影象變換前通過一維矩陣轉換為二維
int XPlace,YPlace;
//輸出轉換為的角度
CString str;
str.Format("轉換後的角度=%f",degree);
AfxMessageBox(str);
//影象旋轉處理
for(int i=0 ; i<m_nImage ; i++ )
{
//原圖:一維矩陣轉換為二維矩陣
X=(i/3)%m_nWidth;
Y=(i/3)/m_nWidth;
//注意錯誤:X=i/m_nHeight Y=i%m_nWidth; 只輸出最後1/3
//影象旋轉為:a(x,y)=x*cos-y*sin b(x,y)=x*sin+y*cos
XPlace=(int)(X*cos(degree)-Y*sin(degree));
YPlace=(int)(X*sin(degree)+Y*cos(degree));
//在轉換為一維圖想輸出
if( (XPlace>=0 && XPlace<=m_nWidth) && (YPlace>=0 && YPlace<=m_nHeight) )
{
Place=YPlace*m_nWidth*3+XPlace*3;
//在影象範圍內賦值為該畫素
if(Place+2<m_nImage)
{
ImageSize[i]=m_pImage[Place];
i++;
ImageSize[i]=m_pImage[Place+1];
i++;
ImageSize[i]=m_pImage[Place+2];
}
//否則賦值為黑色
else
{
ImageSize[i]=0;
i++;
ImageSize[i]=0;
i++;
ImageSize[i]=0;
}
}
//否則賦值為黑色
else
{
ImageSize[i]=0;
i++;
ImageSize[i]=0;
i++;
ImageSize[i]=0;
}
}
fwrite(ImageSize,m_nImage,1,fpw);
fclose(fpo);
fclose(fpw);
numPicture = 2;
level=200; //幾何變換
Invalidate();
}
}
執行效果如下圖所示,中心旋轉太難了!找到中心那個位置就不太容易,我做不下去了,fuck~同時旋轉過程中,由於是饒左下角(0,0)實現,故有的角度會到介面外顯示全黑。下圖分別旋轉15度和355度。![](https://i.iter01.com/images/12ed94a42f9e699725b59514b829dd6682fe1c0fcbc33dcf71f00bf07129672f.png)
![](https://i.iter01.com/images/450fc1247ab2d20eeaaf4c5a86a06358bde6d2067c9a148bf9b083cdd60e1e05.png)
四. 影象縮放
影象縮放主要有兩種方法:
1.最近鄰插值:向後對映時,輸出影象的灰度等於離它所對映位置最近的輸入影象的灰度值。其中向前對映和向後對映如下:
![](https://i.iter01.com/images/00f2fb46fd6347a2007e25df3ce044f3feb81b01ac8e4a15f9a646c4dbf94860.png)
![](https://i.iter01.com/images/5f946736061562f64e638a433d57d44ea7949e8aeed838d2b5424b7b3215c6d7.png)
對於向前對映每個輸出影象的灰度要經過多次運算,對於向後對映,每個輸出影象的灰度只經過一次運算。在實際應用中,更多的是採用向後對映法,其中根據四個相鄰畫素灰度值計算某個位置的畫素灰度值即為灰度級插值。
2.雙線性插值:四點確定一個平面函式,屬於過約束問題。即單位正方形頂點已知,求正方形內任一點的f(x,y)值。
![](https://i.iter01.com/images/c533f7e7471aa374b8e7552b533b1b430be39a0a54f8516f3e552aa4a67ddba6.png)
換個通熟的說法,如下圖所示。採用最近鄰插值法就是P(x,y)畫素值採用四捨五入等於離它最近的輸入影象畫素值。分別計算它到四個頂點之間的距離,但是這樣會造成影象的馬賽克、鋸齒等現象。而採用雙線性插值法,主要通過該座標周圍的四個畫素值,按照比例混合計算器近似值。比例混合的依據是離哪個畫素近,哪個畫素的比例越大。
![](https://i.iter01.com/images/20b29c43c2868bf87bf16ba87fff22665eea8fff4b12ff2c4009a432072f2267.png)
下面是採用最近鄰插值法的過程,注意BMP圖縮放還需修改標頭檔案資訊。
第一步:在資源檢視中新增“影象縮放”Dialog
![](https://i.iter01.com/images/15635047a7f6218b8adfbbf690cd67235aab3884fac73516bf44586d6f945fa4.png)
第一步:在資源檢視中新增“影象縮放”Dialog
![](https://i.iter01.com/images/15635047a7f6218b8adfbbf690cd67235aab3884fac73516bf44586d6f945fa4.png)
第二步:點選空白處建立對話方塊的類CImageSFDlg,同時開啟類嚮導為其新增成員變數m_sfbs(縮放倍數),其為int型在0-200之間。
![](https://i.iter01.com/images/73fd1609932a53a3c7da559d3b0b43d4d69535de2a70940b40bcf1fc41cc5eba.png)
![](https://i.iter01.com/images/da4f79764b7f9e272f3f8352e287806ac862715cfd5cc4c42cc76e134902ea77.png)
![](https://i.iter01.com/images/73fd1609932a53a3c7da559d3b0b43d4d69535de2a70940b40bcf1fc41cc5eba.png)
![](https://i.iter01.com/images/da4f79764b7f9e272f3f8352e287806ac862715cfd5cc4c42cc76e134902ea77.png)
第三步:開啟類嚮導為其新增成員函式void CImageProcessingView::OnJhbhSf() 並實現縮放。同時新增標頭檔案#include "ImageSFDlg.h"。
![](https://i.iter01.com/images/e6c585b87be5ac49be5651f7e43e9c22d447c79ec9d1ee4bfb38b7392b90ed12.png)
![](https://i.iter01.com/images/65674c75aadedf5110fd7eb26d17eff0e035f899c07ce77d91192a5ffc475fe2.png)
/*******************************************************************/
/* ID_JHBH_SF: 幾何運算-縮放-最近鄰插值演算法
/* 演算法思想:輸出影象的灰度等於離它所對映位置最近的輸入影象的灰度值
/* 先計算出放大縮小後的長寬,根據它計算找原圖中的點灰度,四捨五入
/*******************************************************************/
void CImageProcessingView::OnJhbhSf()
{
if(numPicture==0) {
AfxMessageBox("載入圖片後才能幾何縮放影象!",MB_OK,0);
return;
}
CImageSFDlg dlg; //定義縮放對話方塊
if( dlg.DoModal()==IDOK )
{
//取樣座標最初為圖片的自身畫素 m_sfbs(縮放倍數)
if( dlg.m_sfbs==0 ) {
AfxMessageBox("輸入圖片縮放倍數不能為0!",MB_OK,0);
return;
}
FILE *fpo = fopen(BmpName,"rb");
FILE *fpw = fopen(BmpNameLin,"wb+");
fread(&bfh,sizeof(BITMAPFILEHEADER),1,fpo);
fread(&bih,sizeof(BITMAPINFOHEADER),1,fpo);
/*先求縮放後的長寬*/
int sfWidth,sfHeight; //縮放後的長寬
int sfSize; //縮放後的影象大小
sfWidth=(int)(m_nWidth*(dlg.m_sfbs*1.0)/100); //24點陣圖像RGB必須是3倍數 迴圈讀取時為RGB
sfHeight=(int)(m_nHeight*(dlg.m_sfbs*1.0)/100);
int number; //記錄每行多餘的影象素數個數
//重點:影象的每行畫素都必須是4的倍數:1*1的影象為 r g b 00H
if(sfWidth*3%4!=0) {
number=(4-sfWidth*3%4);
sfSize=(sfWidth*3+(4-sfWidth*3%4))*sfHeight;
}
else {
number=0;
sfSize=sfWidth*sfHeight*3;
}
//注意:假如最後一行畫素不足,我預設處理為完整的一行,不足補00H
//總之處理後的影象總是m*n且為4倍數,每行都完整存在
/*更改檔案頭資訊 定義臨時檔案頭結構變數*/
BITMAPFILEHEADER bfhsf;
BITMAPINFOHEADER bihsf; //縮放(sf)
bfhsf=bfh;
bihsf=bih;
bfhsf.bfSize=sfSize+54;
bihsf.biWidth=sfWidth;
bihsf.biHeight=sfHeight;
//顯示部分m_nDrawWidth<650顯示原圖,否則顯示
flagSF=1; //影象縮放為1標識變數
m_nDrawWidthSF=sfWidth;
m_nDrawHeightSF=sfHeight;
fwrite(&bfhsf,sizeof(BITMAPFILEHEADER),1,fpw);
fwrite(&bihsf,sizeof(BITMAPINFOHEADER),1,fpw);
fread(m_pImage,m_nImage,1,fpo);
unsigned char red,green,blue;
unsigned char other=0; //補碼00H='\0'
int placeX; //記錄在原圖中的第幾行的位置
int placeY; //記錄在原圖中的位置(x,y)
int placeBH; //記錄變換後在變換圖中的位置
/*new和delete有效的進行動態記憶體的分配和釋放*/
unsigned char *ImageSize;
ImageSize=new unsigned char[sfSize];
/*讀取檔案畫素資訊 縮放注意:1.找最近灰度 2.四捨五入法(演算法+0.5)*/
for(int i=0; i<sfHeight ; i++ ) //行
{
placeX=(int)(i/(dlg.m_sfbs*1.0/100)+0.5)*bih.biWidth*3;
for(int j=0; j<sfWidth ; j++ ) //列
{
red=green=blue=0;
//放大倍數為(dlg.m_sfbs*1.0/100)
placeY=placeX+(int)(j/(dlg.m_sfbs*1.0/100)+0.5)*3;
//重點是:number*i補充00H,如果是numer影象會被切成2塊
placeBH=(i*sfWidth*3+number*i)+j*3;
if(placeY+2<m_nImage)
{
ImageSize[placeBH]=m_pImage[placeY];
ImageSize[placeBH+1]=m_pImage[placeY+1];
ImageSize[placeBH+2]=m_pImage[placeY+2];
}
else
{
ImageSize[placeBH]=0;
ImageSize[placeBH+1]=0;
ImageSize[placeBH+2]=0;
}
}
}
fwrite(ImageSize,sfSize,1,fpw);
fclose(fpo);
fclose(fpw);
numPicture = 2;
level=200;
Invalidate();
}
}
第四步:因為影象縮放修改BMP圖片頭資訊,所以需要修改ShowBitmap中的顯示第二張圖片時的部分程式碼。如下所示:新增變數flagSF、m_nDrawWidthSF和m_nDrawHeightSF。/*定義顯示影象縮放時的長寬與標記*/
int flagSF=0; //影象幾何變換縮放變換
int m_nDrawWidthSF=0; //影象顯示寬度縮放後
int m_nDrawHeightSF=0; //影象顯示高度縮放後
//****************顯示BMP格式圖片****************//
void CImageProcessingView::ShowBitmap(CDC *pDC, CString BmpName)
{
......
else //影象幾何變換
if(level=200)
{
m_hBitmapChange = (HBITMAP) LoadImage(NULL,BmpNameLin,IMAGE_BITMAP,0,0,
LR_LOADFROMFILE|LR_DEFAULTSIZE|LR_CREATEDIBSECTION);
}
if( m_bitmap.m_hObject ) {
m_bitmap.Detach(); //m_bitmap為建立的點陣圖物件
}
m_bitmap.Attach(m_hBitmapChange);
//定義並建立一個記憶體裝置環境
CDC dcBmp;
if( !dcBmp.CreateCompatibleDC(pDC) ) //建立相容性的DC
return;
BITMAP m_bmp; //臨時bmp圖片變數
m_bitmap.GetBitmap(&m_bmp); //將圖片載入點陣圖中
CBitmap *pbmpOld = NULL;
dcBmp.SelectObject(&m_bitmap); //將點陣圖選入臨時記憶體裝置環境
//圖片顯示呼叫函式StretchBlt
if(flagSF==1)
{
CString str;
str.Format("縮放長=%d 寬%d 原圖長=%d 寬=%d",m_nDrawWidthSF,
m_nDrawHeightSF,m_nWidth,m_nHeight);
AfxMessageBox(str);
flagSF=0;
//m_nDrawWidthSF縮放此存見函式最近鄰插值法中賦值
if(m_nDrawWidthSF<650 && m_nDrawHeightSF<650)
pDC->StretchBlt(m_nWindowWidth-m_nDrawWidthSF,0,
m_nDrawWidthSF,m_nDrawHeightSF,&dcBmp,0,0,m_bmp.bmWidth,m_bmp.bmHeight,SRCCOPY);
else
pDC->StretchBlt(m_nWindowWidth-640,0,640,640,&dcBmp,0,0,
m_bmp.bmWidth,m_bmp.bmHeight,SRCCOPY); //顯示大小為640*640
}
else {
//如果圖片太大顯示大小為固定640*640 否則顯示原圖大小
if(m_nDrawWidth<650 && m_nDrawHeight<650)
pDC->StretchBlt(m_nWindowWidth-m_nDrawWidth,0,
m_nDrawWidth,m_nDrawHeight,&dcBmp,0,0,m_bmp.bmWidth,m_bmp.bmHeight,SRCCOPY);
else
pDC->StretchBlt(m_nWindowWidth-640,0,640,640,&dcBmp,0,0,
m_bmp.bmWidth,m_bmp.bmHeight,SRCCOPY);
}
//恢復臨時DC的點陣圖
dcBmp.SelectObject(pbmpOld);
}
執行效果如下圖所示,採用最近鄰插值法縮放大了會出現失幀。![](https://i.iter01.com/images/e6c585b87be5ac49be5651f7e43e9c22d447c79ec9d1ee4bfb38b7392b90ed12.png)
![](https://i.iter01.com/images/65674c75aadedf5110fd7eb26d17eff0e035f899c07ce77d91192a5ffc475fe2.png)
但是同時當圖片縮小是總是報錯,圖片縮放確實有點難,因為畫素需要補齊4整數倍,同時需要修改訊息頭,同時畫素矩陣的變換都非常複雜。
![](https://i.iter01.com/images/a4957d474b67296adb71697a452b1fe8d2ec952832435760010908cf347c342f.png)
![](https://i.iter01.com/images/a4957d474b67296adb71697a452b1fe8d2ec952832435760010908cf347c342f.png)
最後還是希望文章對你有所幫助,如果文章有不足或錯誤之處,請海涵。自己給自己點個贊,挺不容易的,但還會繼續寫完~
(By:Eastmount 2015-06-04 下午5點 http://blog.csdn.net/eastmount/)
相關文章
- [Python影象處理] 六.影象縮放、影象旋轉、影象翻轉與影象平移Python
- OpenCV計算機視覺學習(11)——影像空間幾何變換(影像縮放,影像旋轉,影像翻轉,影像平移,仿射變換,映象變換)OpenCV計算機視覺
- CGAffineTransform二維檢視旋轉、縮放、平移變換詳解ORM
- 數字影象處理DIP
- 影像縮放、旋轉、翻轉、平移
- [Python影象處理] 五.影象融合、加法運算及影象型別轉換Python型別
- 數字影象處理-第一節
- ARFoundation - 實現物體旋轉, 平移,縮放
- opencv 圖片幾何變換-縮放OpenCV
- 影象處理之影象增強
- 影象處理1--傅立葉變換(Fourier Transform )ORM
- 【筆記】基於Python的數字影象處理筆記Python
- [Python影象處理] 八.影象腐蝕與影象膨脹Python
- 第四個OpenGL程式,vector 向量 (矩陣變換之 旋轉,縮放)矩陣
- Qt 從 QTransform 逆向解出 Translate/Scale/Rotate(平移/縮放/旋轉)分析QTORM
- [Python影象處理] 七.影象閾值化處理及演算法對比Python演算法
- C# 簡易影像處理(包括平移,旋轉,翻轉, 裁切)C#
- 三維空間座標系變換-旋轉矩陣矩陣
- [Python影象處理] 一.影象處理基礎知識及OpenCV入門函式PythonOpenCV函式
- 影象中的畫素處理
- [Python影象處理] 三.獲取影象屬性、興趣ROI區域及通道處理Python
- Python 影像處理 OpenCV (5):影像的幾何變換PythonOpenCV
- 影象處理庫GPUImage簡單使用GPUUI
- [Python影象處理] 四.影象平滑之均值濾波、方框濾波、高斯濾波及中值濾波Python
- [work] 影象縮放——雙線性插值演算法演算法
- 15分鐘理解數字影象中的二維傅立葉變換語義
- 影象處理的濾鏡演算法演算法
- 實戰 | 用Python做影象處理(一)Python
- matlab中將RGB影象轉化為灰度影象Matlab
- ffmpeg-圖片壓縮旋轉等處理
- 影象傅立葉變換,幅度譜,相位譜
- 該演算法利用區域性的平移、縮放以及旋轉的方式來不失真的進行影像纖瘦化處理。演算法
- Luminar 4 for MacOS影象後期處理軟體Mac
- blender python api -修改骨架中特定骨骼的變換,包括沿不同軸的旋轉、位置和縮放(旋轉為四元數運算WXYZ)PythonAPI
- [Python影象處理] 九.形態學之影象開運算、閉運算、梯度運算Python梯度
- [Python影象處理] 十.形態學之影象頂帽運算和黑帽運算Python
- 第四個OpenGL程式,vector 向量 (矩陣變換之 旋轉,縮放)後續 繪製多個 圖形矩陣
- 【Go】IP地址轉換:數字與字串之間高效轉換Go字串
- 三維座標系旋轉——旋轉矩陣到旋轉角之間的換算矩陣