1.掩膜(mask)的定义
用选定的图像,图形或物体,对处理的图像(全部或局部)进行遮挡,来控制图像处理的区域或处理过程。用于覆盖的特定图像或物体称为掩模或模板。光学图像处理中,掩模可以足胶片,滤光片等。
掩模是由0和1组成的一个二进制图像。当在某一功能中应用掩模时,1值区域被处理,被屏蔽的0值区域不被包括在计算中。通过指定的数据值,数据范围,有限或无限值,感兴趣区和注释文件来定义图像掩模,也可以应用上述选项的任意组合作为输入来建立掩模。
掩膜是一种图像滤镜的模板,实用掩膜经常处理的是遥感图像。当提取道路或者河流,或者房屋时,通过一个N * N的矩阵来对图像进行像素过滤,然后将我们需要的地物或者标志突出显示出来。这个矩阵就是一种掩膜。
在OpenCV的中,掩模操作是相对简单的。大致的意思是,通过一个掩模矩阵,重新计算图像中的每一个像素值。掩模矩阵控制了旧图像当前位置以及周围位置像素对新图像当前位置像素值的影响力度。用数学术语讲,即我们自定义一个权重表。
2.掩膜的用法
2.1 提取感兴趣区:用预先制作的感兴趣区掩膜与待处理图像相乘,得到感兴趣区图像,感兴趣区内图像值保持不变,而区外图像值都为0;
2.2 屏蔽作用:用掩膜对图像上某些区域作屏蔽,使其不参加处理或不参加处理参数的计算,或仅对屏蔽区作处理或统计;
2.3 结构特征提取:用相似性变量或图像匹配方法检测和提取图像中与掩膜相似的结构特征;
2.4 特殊形状图像的制作。
3.掩膜运算的一个小实例
以图和掩膜的与运算为例:
原图中的每个像素和掩膜中的每个对应像素进行与运算。比如1 & 1 = 1;1 & 0 = 0;
比如一个3 * 3的图像与3 * 3的掩膜进行运算,得到的结果图像就是:
掩膜操作实现图像对比度调整
矩阵的掩膜操作十分简单,根据掩膜来重新计算每个像素的像素值,掩膜(mask)也被称为内核。
通过掩膜操作实现图像对比度提高。I(i,j) = 5*I(i,j) - [I(i-1,j) + I(i+1,j) + I(i,j-1) + I(i,j+1)]
Mat kern = (Mat_<char>(3,3) << 0, -1, 0,
-1, 5, -1,
0, -1, 0);
红色是中心像素,从上到下,从左到右对每个像素做同样的处理操作,得到最终结果就是对比度提高之后的输出图像垫对象。
实例代码:
#include <opencv2\opencv.hpp>
#include <iostream>
#include <math.h>
using namespace std;
using namespace cv;
int main()
{
Mat src, dst;
src = imread("E:\\VS2015Opencv\\vs2015\\project\\picture\\cat.jpg");
if (!src.data)
{
cout << "could not load image..." << endl;
return -1;
}
namedWindow("source image", CV_WINDOW_AUTOSIZE);
imshow("source image", src);
//1).empty() 判断文件读取是否正确
//2).rows 获取图像行数(高度)
//3).cols 获取图像列数(长度)
//4).channels() 获取图像通道数
//5).depth() 获取图像位深度
//【1】记录程序开始点timeStart
double timeStart = (double)getTickCount();//计算时间语句
//进行矩阵的掩膜操作
int cols = (src.cols - 1)*src.channels();//837 //获取图像的列数,一定不要忘记图像的通道数
int offsetx = src.channels();//图像的通道数 3
int rows = src.rows;//220
dst = Mat::zeros(src.size(), src.type());//返回指定的大小和类型的数组 创建一个跟src一样大小 类型的图像矩阵
for (int row = 1; row < (rows - 1); row++)
{
//Mat.ptr<uchar>(int i=0) 获取像素矩阵的指针,索引i表示第几行,从0开始计行数。
//获得当前行指针const uchar* current= myImage.ptr<uchar>(row );
//获取当前像素点P(row, col)的像素值 p(row, col) =current[col]
//Mat.ptr<uchar>(row):获取第row行的图像像素指针。图像的行数从0开始计数
//获取点P(row, col)的像素值:P(row.col) = Mat.ptr<uchar>(row)[col]
const uchar *previous = src.ptr<uchar>(row - 1);//获取上一行指针
const uchar *current = src.ptr<uchar>(row);//获取当前行的指针
const uchar *next = src.ptr<uchar>(row + 1);//获取下一行的指针
uchar *output = dst.ptr<uchar>(row);//目标对象像素
for (int col = offsetx; col < cols; col++)
{
//current[col-offsetx]是当前的像素点的左边那个像素点的位置,因为一个像素点有三个通道
//current[col+offsetx]是当前的像素点的右边那个像素点的位置,因为一个像素点有三个通道
//previous[col]表示当前像素点对应的上一行的那个像素点
//next[col]表示当前像素点对应的下一行的那个像素点
output[col] = 5 * current[col] - (current[col - offsetx] + current[col + offsetx] + previous[col] + next[col]);
}
}
//OpenCV提高了函数filter2D来实现掩膜操作:
//Mat kernel = (Mat_<char>(3, 3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);//定义掩膜
//调用filter2D
//filter2D(src, dst, src.depth(), kernel);
double timeconsume = ((double)getTickCount() - timeStart) / getTickFrequency();
cout << "运行上面程序共耗时: " << timeconsume << endl;
//输出 掩膜操作后的图像
namedWindow("contrast image", CV_WINDOW_AUTOSIZE);
imshow("contrast image", dst);
waitKey(0);
return 0;
}
我们可以看见掩膜操作后的图像对比度明显提高了,但是美中不足的是出现了一些不好的小斑点。这是因为这项像素点的值的范围不在0~255之间了。
解决方法:
使用函数saturate_cast(像素值)
这个函数的作用就是确保RGB的值在0~255之间。
像素范围处理saturate_cast <typename _Tp>()
- saturate_cast <UCHAR>( - 100),返回0
- saturate_cast <UCHAR>(288),返回255
- saturate_cast <UCHAR>(100),返回100
这个函数的功能是确保RGB值范围在0〜255之间。
添加上:
#include <opencv2\opencv.hpp>
#include <iostream>
#include <math.h>
using namespace std;
using namespace cv;
int main()
{
Mat src, dst;
src = imread("E:\\VS2015Opencv\\vs2015\\project\\picture\\cat.jpg");
if (!src.data)
{
cout << "could not load image..." << endl;
return -1;
}
namedWindow("source image", CV_WINDOW_AUTOSIZE);
imshow("source image", src);
//1).empty() 判断文件读取是否正确
//2).rows 获取图像行数(高度)
//3).cols 获取图像列数(长度)
//4).channels() 获取图像通道数
//5).depth() 获取图像位深度
//【1】记录程序开始点timeStart
double timeStart = (double)getTickCount();//计算时间语句
//进行矩阵的掩膜操作
int cols = (src.cols - 1)*src.channels();//837 //获取图像的列数,一定不要忘记图像的通道数
int offsetx = src.channels();//图像的通道数 3
int rows = src.rows;//220
dst = Mat::zeros(src.size(), src.type());//返回指定的大小和类型的数组 创建一个跟src一样大小 类型的图像矩阵
for (int row = 1; row < (rows - 1); row++)
{
//Mat.ptr<uchar>(int i=0) 获取像素矩阵的指针,索引i表示第几行,从0开始计行数。
//获得当前行指针const uchar* current= myImage.ptr<uchar>(row );
//获取当前像素点P(row, col)的像素值 p(row, col) =current[col]
//Mat.ptr<uchar>(row):获取第row行的图像像素指针。图像的行数从0开始计数
//获取点P(row, col)的像素值:P(row.col) = Mat.ptr<uchar>(row)[col]
const uchar *previous = src.ptr<uchar>(row - 1);//获取上一行指针
const uchar *current = src.ptr<uchar>(row);//获取当前行的指针
const uchar *next = src.ptr<uchar>(row + 1);//获取下一行的指针
uchar *output = dst.ptr<uchar>(row);//目标对象像素
for (int col = offsetx; col < cols; col++)
{
//像素范围处理saturate_cast<uchar>
output[col] = saturate_cast<uchar>(5 * current[col] - (current[col - offsetx] + current[col + offsetx] + previous[col] + next[col]));
//current[col-offsetx]是当前的像素点的左边那个像素点的位置,因为一个像素点有三个通道
//current[col+offsetx]是当前的像素点的右边那个像素点的位置,因为一个像素点有三个通道
//previous[col]表示当前像素点对应的上一行的那个像素点
//next[col]表示当前像素点对应的下一行的那个像素点
//output[col] = 5 * current[col] - (current[col - offsetx] + current[col + offsetx] + previous[col] + next[col]);
}
}
double timeconsume = ((double)getTickCount() - timeStart) / getTickFrequency();
cout << "运行上面程序共耗时: " << timeconsume << endl;
//输出 掩膜操作后的图像
namedWindow("contrast image", CV_WINDOW_AUTOSIZE);
imshow("contrast image", dst);
waitKey(0);
return 0;
}
函数调用filter2D功能
定义掩膜:Mat kernel = (Mat_(3,3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
filter2D( src, dst, src.depth(), kernel ); 其中src与dst是Mat类型变量、src.depth表示位图深度,有32、24、8等。
#include <opencv2\opencv.hpp>
#include <iostream>
#include <math.h>
using namespace std;
using namespace cv;
int main()
{
Mat src, dst;
src = imread("E:\\VS2015Opencv\\vs2015\\project\\picture\\cat.jpg");
if (!src.data)
{
cout << "could not load image..." << endl;
return -1;
}
namedWindow("source image", CV_WINDOW_AUTOSIZE);
imshow("source image", src);
//1).empty() 判断文件读取是否正确
//2).rows 获取图像行数(高度)
//3).cols 获取图像列数(长度)
//4).channels() 获取图像通道数
//5).depth() 获取图像位深度
//【1】记录程序开始点timeStart
double timeStart = (double)getTickCount();//计算时间语句
//进行矩阵的掩膜操作
int cols = (src.cols - 1)*src.channels();//837 //获取图像的列数,一定不要忘记图像的通道数
int offsetx = src.channels();//图像的通道数 3
int rows = src.rows;//220
dst = Mat::zeros(src.size(), src.type());//返回指定的大小和类型的数组 创建一个跟src一样大小 类型的图像矩阵
for (int row = 1; row < (rows - 1); row++)
{
//Mat.ptr<uchar>(int i=0) 获取像素矩阵的指针,索引i表示第几行,从0开始计行数。
//获得当前行指针const uchar* current= myImage.ptr<uchar>(row );
//获取当前像素点P(row, col)的像素值 p(row, col) =current[col]
//Mat.ptr<uchar>(row):获取第row行的图像像素指针。图像的行数从0开始计数
//获取点P(row, col)的像素值:P(row.col) = Mat.ptr<uchar>(row)[col]
const uchar *previous = src.ptr<uchar>(row - 1);//获取上一行指针
const uchar *current = src.ptr<uchar>(row);//获取当前行的指针
const uchar *next = src.ptr<uchar>(row + 1);//获取下一行的指针
uchar *output = dst.ptr<uchar>(row);//目标对象像素
for (int col = offsetx; col < cols; col++)
{
//像素范围处理saturate_cast<uchar>
output[col] = saturate_cast<uchar>(5 * current[col] - (current[col - offsetx] + current[col + offsetx] + previous[col] + next[col]));
//current[col-offsetx]是当前的像素点的左边那个像素点的位置,因为一个像素点有三个通道
//current[col+offsetx]是当前的像素点的右边那个像素点的位置,因为一个像素点有三个通道
//previous[col]表示当前像素点对应的上一行的那个像素点
//next[col]表示当前像素点对应的下一行的那个像素点
//output[col] = 5 * current[col] - (current[col - offsetx] + current[col + offsetx] + previous[col] + next[col]);
}
}
//OpenCV提高了函数filter2D来实现掩膜操作:
Mat kernel = (Mat_<char>(3, 3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);//定义掩膜
//调用filter2D
filter2D(src, dst, src.depth(), kernel);
double timeconsume = ((double)getTickCount() - timeStart) / getTickFrequency();
cout << "运行上面程序共耗时: " << timeconsume << endl;
//输出 掩膜操作后的图像
namedWindow("contrast image", CV_WINDOW_AUTOSIZE);
imshow("contrast image", dst);
waitKey(0);
return 0;
}
与前面没有区别;
小结
1.图像中,各种位运算,比如与、或、非运算与普通的位运算类似。
2.如果用一句话总结,掩膜就是两幅图像之间进行的各种位运算操作。
可以看看下面代码:
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include <iostream>
#include <stdio.h>
using namespace std;
using namespace cv;
int main()
{
Mat image, mask;
Rect r1(100, 100, 250, 300);
Mat img1, img2, img3, img4;
image = imread("E:\\VS2015Opencv\\vs2015\\project\\picture\\cat.jpg");
mask = Mat::zeros(image.size(), CV_8UC1);
mask(r1).setTo(255);
img1 = image(r1);
image.copyTo(img2, mask);
image.copyTo(img3);
img3.setTo(0, mask);
imshow("Image sequence", image);
imshow("img1", img1);
imshow("img2", img2);
imshow("img3", img3);
imshow("mask", mask);
waitKey();
return 0;
}
注意程序中的这两句关于Mask的操作。
mask = Mat::zeros(image.size(), CV_8UC1);
mask(r1).setTo(255); //r1是已经设置好的兴趣区域
解释一下上面两句的操作。
- 第一步建立与原图一样大小的mask图像,并将所有像素初始化为0,因此全图成了一张全黑色图。
- 第二步将mask图中的r1区域的所有像素值设置为255,也就是整个r1区域变成了白色。
这样就能得到Mask图像了。
image.copyTo(img2, mask);
这句是原始图image拷贝到目的图img2上。
其实拷贝的动作完整版本是这样的:
原图(image)与掩膜(mask)进行与运算后得到了结果图(img2)。
说白了,mask就是位图啊,来选择哪个像素允许拷贝,哪个像素不允许拷贝。如果mask像素的值是非0的,我就拷贝它,否则不拷贝。
因为我们上面得到的mask中,感兴趣的区域是白色的,表明感兴趣区域的像素都是非0,而非感兴趣区域都是黑色,表明那些区域的像素都是0。一旦原图与mask图进行与运算后,得到的结果图只留下原始图感兴趣区域的图像了。也正如下图所示。
如果想要直接抠出目标区域,直接这样写就OK了:
img1 = image(r1);
image.copyTo(img3);
img3.setTo(0, mask);
上面两句代码所做的事情是首先将原始图image拷贝一份给img3,然后img3将mask白色区域设置为0(黑色),好比如果mask中像素非0的,我就把我图像对应的那个点的像素值设置为0,否则啥也不做。伪代码是 if mask(i,j)>0 then img3(i,j)=0。
也就是说mask为1的位置设置为0,如下图