基于OpenCV的图像编程基础
主要内容
- 完成图像旋转、平移、比例缩放。
- 设计实现用户UI交互方式下的图像几何变换。
- 实现图像的透视畸变矫正。
一、图像平移
平移操作是将图像的所有像素坐标经行水平或垂直方向移动,平移变换分为两种类型:图像大小变化和图像大小不变。前者图像的信息不会丢失,后者可能丢失部分图像。
因此可以得到变换逆矩阵为:
示例代码如下:
1. 平移,图像大小不变
cv::Mat imageTranslation1(cv::Mat& srcImage, int xOffset, int yOffset) {
int nRows = srcImage.rows;
int nCols = srcImage.cols;
cv::Mat resultImage(srcImage.size(), srcImage.type());
//遍历图像
for (int i = 0; i < nRows; ++i) {
for (int j = 0; j < nCols; ++j) {
//映射变换
int x = j - xOffset;
int y = i - yOffset;
//边界判断
if (x >= 0 && y >= 0 && x < nCols && y < nRows)
resultImage.at< cv::Vec3b >(i, j) = srcImage.ptr<cv::Vec3b>(y)[x];
}
}
return resultImage;
}
2. 平移,图像大小改变
cv::Mat imageTranslation2(cv::Mat & srcImage, int xOffset, int yOffset) {
//设置平移尺寸
int nRows = srcImage.rows + abs(yOffset);
int nCols = srcImage.cols + abs(xOffset);
cv::Mat resultImage(nRows, nCols, srcImage.type());
//图像遍历
for (int i = 0; i < nRows; ++i) {
for (int j = 0; j < nCols; ++j) {
//映射变换
int x = j - xOffset;
int y = i - yOffset;
//边界判断
if (x >= 0 && y >= 0 && x <= nCols && y < nRows)
resultImage.at<cv::Vec3b>(i, j) = srcImage.ptr<cv::Vec3b>(y)[x];
}
}
return resultImage;
}
二、图像旋转
图像旋转是指图像按照某个位置转动一定角度的过程,旋转中心图像任保持着原始尺寸。设图像旋转中心坐标为(Cx,Cy),源图像数据坐标为(x,y) ,旋转后目标图像坐标为(x’,y’) ,先将坐标原点由图像左上角变换到旋转中心,坐标转换矩阵形式为:
将其转换为图像矩阵表达式:
设旋转后图像的最左边点的横坐标,最右边点的横坐标,最高点的纵坐标,最低点的纵坐标非别为:lxmin,lxmax,lymin,lymax
最后需要将坐标原点变换到旋转后的图像的左上角坐标,转换矩阵表示如下:
示例代码如下:
cv::Mat angleRotate(cv::Mat& src, int angle) {
//角度转换
float alpha = angle * CV_PI / 180;
//构造旋转矩阵
float rotateMat[3][3] = {
{cos(alpha),-sin(alpha),0},
{sin(alpha),cos(alpha),0},
{0,0,1} };
int nSrcRows = src.rows;
int nSrcCols = src.cols;
//计算旋转后图像矩阵各个顶点的位置
float a1 = nSrcCols * rotateMat[0][0];
float b1 = nSrcCols * rotateMat[1][0];
float a2 = nSrcCols * rotateMat[0][0] + nSrcRows * rotateMat[0][1];
float b2 = nSrcCols * rotateMat[1][0] + nSrcRows * rotateMat[1][1];
float a3 = nSrcRows * rotateMat[0][1];
float b3 = nSrcRows * rotateMat[1][1];
//计算出极值点
float kxMin = min(min(min(0.0f, a1), a2), a3);
float kxMax = max(max(max(0.0f, a1), a2), a3);
float kyMin = min(min(min(0.0f, b1), b2), b3);
float kyMax = max(max(max(0.0f, b1), b2), b3);
//计算输出矩阵的尺寸
int nRows = abs(kxMax - kxMin);
int nCols = abs(kyMax - kyMin);
cv::Mat dst(nRows, nCols, src.type(), cv::Scalar::all(0));
for (int i = 0; i < nRows; ++i)
{
for (int j = 0; j < nCols; ++j)
{
//旋转坐标转换
int x = (j + kxMin) * rotateMat[0][0] - (i + kyMin) * rotateMat[0][1];
int y = -(j + kxMin) * rotateMat[1][0] + (i + kyMin) * rotateMat[1][1];
//区域旋转
if ((x >= 0) && (x < nSrcCols) && (y >= 0) && (y < nSrcCols) && (y >= 0) && (y < nSrcRows))
{
dst.at<cv::Vec3b>(i, j) = src.at<cv::Vec3b>(y, x);
}
}
}
return dst;
}
三、图像缩放
图像缩放是将图像的尺寸变小或者变大的过程,也就是减少或增加源图像数据像素的个数。对于源图像数据 f(x,y) ,分辨率为M×N,如果将其分辨率改为m×n,则比例因子为:k=M/N
则将 f(x, y) 变换成 g(x’, y’) 的矩阵变换关系如下:
示例代码如下:
//基于等间隔提取图像缩放
cv::Mat imageReduction(cv::Mat &srcImage, float kx, float ky) {
//获取输出图像分辨率
int nRows = cvRound(srcImage.rows * kx);
int nCols = cvRound(srcImage.cols * ky);
cv::Mat resultImage(nRows, nCols, srcImage.type());
for (int i = 0; i < nRows; ++i) {
for (int j = 0; j < nCols; ++j) {
//根据水平因子计算坐标
int x = static_cast<int>((i + 1) / kx + 0.5) - 1;
//更具垂直因子计算坐标
int y = static_cast<int>((j + 1) / ky + 0.5) - 1;
resultImage.at<cv::Vec3b>(i, j) = srcImage.at<cv::Vec3b>(x, y);
}
}
return resultImage;
}
四、用户UI交互设计
UI设计主要依靠MFC来实现,是基于对话框设计的程序。界面如下图所示:
图像一显示的原图片,图像二显示的是处理后的图像。
在MFC中,为了实现图像的显示,采用了CImage类的方式,显示图片是通过函数ShowImg()实现,其具体代码如下:
void CimageProcessDlg::ShowImg(cv::Mat mat, UINT ID)
{//显示mat在picture控件中
Mat temp;
//图片基本信息
int width = mat.cols;
int height = mat.rows;
int channels = mat.channels();
int depth = mat.depth();
int dims = mat.dims;
CWnd* m_pMyWnd = GetDlgItem(ID);
CDC *m_pDC = m_pMyWnd->GetDC();//获取窗口所拥有的设备上下文,用于显示图像
CLearwindow(ID);
m_pMyWnd->UpdateWindow();
ATL::CImage cImage;
cImage.Destroy();//这一步是防止重复利用造成内存问题
cImage.Create(width, height, 8 * channels);
// 如果是1个通道的图像(灰度图像) DIB格式才需要对调色板设置 。 CImage中内置了调色板,我们要对他进行赋值:
if (1 == channels)
{
RGBQUAD* ColorTable;
int MaxColors = 256;
//这里可以通过CI.GetMaxColorTableEntries()得到大小(如果你是CI.Load读入图像的话)
ColorTable = new RGBQUAD[MaxColors];
cImage.GetColorTable(0, MaxColors, ColorTable);//这里是取得指针
for (int i = 0; i < MaxColors; i++)
{
ColorTable[i].rgbBlue = (BYTE)i;
ColorTable[i].rgbGreen = (BYTE)i;
ColorTable[i].rgbRed = (BYTE)i;
}
cImage.SetColorTable(0, MaxColors, ColorTable);
delete[]ColorTable;
}
if (depth == 0) {//CV_8U
mat.copyTo(temp);
}
else if (depth == 1) {//CV_8S
mat.convertTo(temp, CV_8U, 255.0, 0);
}
else if (depth == 2) {//CV_16U
mat.convertTo(temp, CV_8U, 255.0, 0);
}
else if (depth == 3) {//CV_16S
mat.convertTo(temp, CV_8U, 255.0, 0);
}
else if (depth == 4) {//CV_32S
mat.convertTo(temp, CV_8U, 255.0, 0);
}
else if (depth == 5) {// CV_32F
mat.convertTo(temp, CV_8U, 255.0, 0);
}
else if (depth == 6) {//CV_64F
mat.convertTo(temp, CV_8U, 255.0, 0);
}
else if (depth == 7) {
AfxMessageBox(L"Format Error depth==7 Type = CV_USRTYPE1");
return;
}
else {
AfxMessageBox(L"Format Error. Type = Unknown");
return;
}
uchar* ps;
uchar* pimg = (uchar*)cImage.GetBits(); //获取CImage的像素存贮区的指针
int step = cImage.GetPitch();//每行的字节数,注意这个返回值有正有负
for (int i = 0; i < height; i++)
{
ps = temp.ptr<uchar>(i);
for (int j = 0; j < width; j++)
{
if (1 == channels)
{
*(pimg + i * step + j) = ps[j];
}
else if (3 == channels)
{
*(pimg + i * step + j * 3) = ps[j * 3];
*(pimg + i * step + j * 3 + 1) = ps[j * 3 + 1];
*(pimg + i * step + j * 3 + 2) = ps[j * 3 + 2];
}
}
}
CRect rc;
m_pMyWnd->GetWindowRect(&rc);
/*InvalidateRect(m_pMyWnd->m_hWnd,&rc,true);*/
int nwidth = rc.Width();
int nheight = rc.Height();
int fixed_width = min(cImage.GetWidth(), nwidth);
int fixed_height = min(cImage.GetHeight(), nheight);
double ratio_w = fixed_width / (double)cImage.GetWidth();
double ratio_h = fixed_height / (double)cImage.GetHeight();
double ratio = min(ratio_w, ratio_h);、
int show_width = (int)(cImage.GetWidth() * ratio);
int show_height = (int)(cImage.GetHeight() * ratio);
int offsetx = (nwidth - show_width) / 2;
int offsety = (nheight - show_height) / 2;
::SetStretchBltMode(m_pDC->GetSafeHdc(), COLORONCOLOR);//设置位图的伸缩模式
cImage.StretchBlt(m_pDC->GetSafeHdc(), offsetx, offsety, show_width, show_height,
0, 0, cImage.GetWidth(), cImage.GetHeight(), SRCCOPY);
}
其余平移,旋转,缩放功能的代码和上述示例近似。
五、图像的透视畸变矫正
在图像处理中,因为镜头角度等原因,容易导致图像出现倾斜、变形等情况,为了方便后续处理,常常需要进行图像矫正。
在opencv中,提供了一种可行的方法,即使用cv::perspectiveTransform()函数。输入原始图像和变换之后的图像的4组对应点,便可以得到变换矩阵。通过设置鼠标事件回调函数,来获取对应的四个点4个点。
#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
using namespace std;
using namespace cv;
const int N = 400;
const int M = 220;
Mat img;
Point p[5];
int flag = 1;
int cnt = 0;
static void mouse_callback(int event, int x, int y, int, void *) {
//当鼠标左键按下时,记录其坐标
if (event == EVENT_LBUTTONDOWN) {
p[cnt].x = x * 1.0; p[cnt++].y = y * 1.0;
cout << "p" << cnt << " is recorded at " << p[cnt - 1] << endl;
}
if (cnt == 4) {
cnt = 0;
//变换前图像四个点
vector<Point2f>src(4);
src[0] = p[0];
src[1] = p[1];
src[2] = p[2];
src[3] = p[3];
//变换后
vector<Point2f>dst(4);
dst[0] = Point2f(0, 0);//左上
dst[1] = Point2f(N, 0);//右上
dst[2] = Point2f(0, M);//左下
dst[3] = Point2f(N, M);//右下
//获取透视变换矩阵
Mat m = getPerspectiveTransform(src, dst);
Mat res;
warpPerspective(img, res, m, Size(N, M), INTER_LINEAR);//实现图像透视变换
namedWindow("PIC2", 1);
imshow("PIC2", res);
waitKey(0);
}
}
int main() {
img = imread("sample.jpg");
if (!img.data) { cout << "read image file wrong!" << endl; getchar(); return 0; }
cout << "height = " << img.size().height << ",width = " << img.size().width << endl;
getchar();
namedWindow("PIC");
imshow("PIC", img);
setMouseCallback("PIC", mouse_callback);//设置鼠标事件回调函数
while (char(waitKey(1)) != 'q') {}
return 0;
}
六、结果分析
1. 图像平移结果截图及分析
在主函数中分别调用平移处理函数,代码如下:
//图像左移不改变大小
cv::Mat resultImage1 = imageTranslation1(srcImage, 50, 80);
cv::imshow("resultImage1", resultImage1);
//图像左移改变大小
cv::Mat resultImage2 = imageTranslation2(srcImage, 50, 80);
cv::imshow("resultImage2", resultImage2);
//图像右移不改变大小
cv::Mat resultImage3 = imageTranslation1(srcImage, -50, -80);
结果如下图所示:
图像左移不改变大小
图像左移改变大小
图像右移不改变大小
平移操作是将图像的所有像素坐标经行水平或垂直方向移动,平移变换分为两种类型:图像大小变化和图像大小不变。前者图像的信息不会丢失,后者可能丢失部分图像。
2. 图像旋转截图及分析
在主函数中调用旋转函数,代码如下:
int angle = 30;
cv::Mat resultImage = angleRotate(srcImage, angle);
imshow("resultImage", resultImage);
结果如下图所示:
图像顺时针旋转30°
图像旋转是指图像按照某个位置转动一定角度的过程,旋转中心图像任保持着原始尺寸。但旋转过后要相应改变显示框的尺寸,避免使图像信息丢失。
3. 图像缩放截图及分析
在主函数中调用缩放函数,代码如下:
cv::Mat resultImage4 = imageReduction(srcImage, 0.5, 0.5);
cv::imshow("resultImage4", resultImage4);
结果如下图所示:
图像纵横变为原来的0.5倍
图像缩放是将图像的尺寸变小或者变大的过程,也就是减少或增加源图像数据像素的个数。缩放在一定程度上会造成信息的丢失,在此过程中如何进行图像插值和采样是一个非常重要的问题。
4. 人机交互设计截图及分析
在MFC界面,分别带年纪button按钮,可以得到不同的处理结果,如下图所示:
平移处理结果图
旋转处理结果图
缩放处理结果图
该程序还有很多方面需要进一步完善,可以加入自定义位移量,旋转角度,缩放比例的功能,使界面交互更加的人性化。
5. 透视畸变矫正截图及分析
在控制台运行程序,在先前存入的图片下选择四个点作为矫正的依据,如图所示,四个的坐标分别为[152,143],[417,142],[93,311],[451,344]。这些坐标通过鼠标事件来获得,在通过调用opencv处理畸变的函数来实现。处理结果如窗口PIC2所示