在开始之前先说一下RGB颜色空间,常用一个三维数组来表示一种颜色,在OpenCV中常用一个向量Vec××来表示,例如表示蓝色使用Vec3b(255,0,0),OpenCV中是BGR,顺序有些不同,某个分量越大,则数值越大。数值越大就会导致图像的颜色越浅,
上一张图就明白了
再提一下灰度图0~255,0代表黑色,255代表白色,和RGB色彩空间有点不同
1、策略设计模式
是指将算法封装到类当中,可以组合多个算法,然后通过创建类的实例来部署算法。下面展示的这个类当中的算法可以实现通过比较颜色差距来生成一张二值图像。
#include<opencv2/highgui.hpp>
#include<opencv2/core.hpp>
using namespace cv;
class ColorDetector
{
public:
ColorDetector() :maxDist(100), target(0, 0, 0) {}//构造函数的初始化参数列表(这是黑色)
void setColorDistanceThreshold(int distance) //用户设置公差
{
if (distance < 0)
distance = 0;
maxDist = distance;
}
int getColorDistanceThreshold () const //用来获取公差,也就是颜色差距的阈值
{ //若添加const,便是常量成员函数,不修改对象的数据(保护机制)
return maxDist; //用来保护内部数据
}
void setTargetColor(uchar blue, uchar green, uchar red)//设置要检查的颜色
{
target = Vec3b(blue, green, red);
}
void setTargetColor(Vec3b color)//设置要检查的颜色函数的重载
{
target = color;
}
Vec3b getTargetColor() //获取设置的颜色
{
return target;
}
int getDistanceToTargetColor(const Vec3b& color) const //返回某颜色与要检查的颜色间的差距
{
return getColorDistance(color, target);
}
int getColorDistance(const Vec3b& color1, const Vec3b& color2) const//范围任意两个颜色间差距
{
return abs(color1[0] - color2[0]) + abs(color1[1] - color2[1]) + abs(color1[2] - color2[2]);
}
Mat process(const Mat& image)
{
result.create(image.size(), CV_8U);
Mat_<Vec3b>::const_iterator it = image.begin<Vec3b>();
Mat_<Vec3b>::const_iterator itend = image.end<Vec3b>();
Mat_<uchar>::iterator itout = result.begin<uchar>(); //这是嵌套的命名空间
for (; it != itend; it++, itout++)
{
if (getDistanceToTargetColor(*it) <= maxDist)
*itout = 255;
else
*itout = 0;
}
return result;
}
private:
int maxDist;
Vec3b target;
Mat result;
};
int main(void)
{
ColorDetector cdect;
Mat image = imread("1.jpg");
if (image.empty())
return 0;
cdect.setTargetColor(250, 130,130);
Mat result = cdect.process(image);
imshow("result", result);
waitKey(0);
}
cdect.setTargetColor(250, 130,130);
怎么就认为这样设置是挑选蓝色那?因为绿色分量和红色分量的设置值相对于0~255这个范围来说处在区间中间,因此不管对具有任何绿红分布的颜色求abs(color1[i] - color2[i])得到的值都比较小,所以在return abs(color1[0] - color2[0]) + abs(color1[1] - color2[1]) + abs(color1[2] - color2[2]);这句代码中影响return值得是蓝色分量对应的那个绝对值,当蓝色分量比较小时,差值比较大,就会导致return值增大,当蓝色分量比较大时,差值比较小,会导致return值比较小(因为设定蓝色值比较大),又因为判断条件是小于,所以就把比较小的return值选择出来了,也就是蓝色分量比较多的。
上面计算两个颜色三维向量之间的距离时使用了取绝对值的方式,OpenCV中还可以使用函数计算两个向量之间的范数,从而求这两个三维向量之间的距离。这里涉及的欧几里得范数类型是2范数:
N维向量v的欧几里得范数定义为
其实就是这求个N维向量的长度。
在代码中求的是(color1[0] - color2[0], color1[1] - color2[1],color1[2] - color2[2])这个三维向量的范数(长度),其实就是求color1向量减去color2向量之后得到的那个向量的长度,请联系三角形法则理解。
return static_cast<int>(
norm<int,3> (Vec3i(color1[0] - color2[0],
color1[1] - color2[1],
color1[2] - color2[2])))
//norm函数是一个模板函数,用来计算一个向量的欧几里得2范数,在形参列表内指定向量的元素类型以及向量维数,然后填写向量参数
//使用int型是因为减法的结果是整型,运算时自动发生了类型转换,这是C++语言自己的机制问题。
这段代码中static_cast<type-id>(expression),这个函数是用来实现显式类型转换的,也可以叫它是是运算符,类似sizeof运算符。
还有一个函数长得有点像它,是saturate_cast<Typename>(),是防止越界的。
2、函数介绍
void absdiff(InputArray src1, InputArray src2, OutputArray dst);这个函数是用来计算两个输入图像各个元素之间的绝对值之差,最终结果是一个数组。(Calculates the per-element absolute difference between two arrays or between an array and a scalar.)
Scalar sum(InputArray src);这个函数计算得到的是输入图像各元素之和,结果是一个颜色向量,也就是各个通道的和是独立的
3、使用OpenCV函数来检测
Mat process(const Mat& image)
{
Mat output;
absdiff(image, Scalar(target), output);
vector<Mat> images;
split(output, images); //分离
output = images[0] + images[1] + images[2];
/*param src Source 8-bit single-channel image.
因为函数要求输入图像是单通道,因此把三个通道叠加到一起*/
threshold(output, output, maxDist, 255, THRESH_BINARY_INV);
}
threshold函数创建的是二值图像。
还有一个函数floodFill,它也是对像素逐一检查,但是它还会检查旁边的元素,所以它检查出的是一个大连通域,细小的部分不会检查到。 floodFill( InputOutputArray image, InputOutputArray mask,
Point seedPoint, Scalar newVal, CV_OUT Rect* rect=0,
Scalar loDiff = Scalar(), Scalar upDiff = Scalar(),
int flags = 4 );其中CV_OUT Rect* rect=0参数是所要检查的最小周边区域,设置为零,意为对周边区域进行地毯式搜索,一个像素也不放过。
4、仿函数或者函数对象
通过C++的运算符重载让类对象表现得像函数,这种类的实例称为函数对象或者仿函数。
//这个是构造函数重载,使用参数列表初始化时必须是对类的数据成员操作
ColorDetector(uchar blue, uchar green, uchar red, int Dmaxist = 100) : maxDist(maxDist)
{
setTargetColor(blue, green, red);
}
Mat operator() (const Mat& image) //实现运算符的重载
{
result.create(image.size(), CV_8U);
Mat_<Vec3b>::const_iterator it = image.begin<Vec3b>();
Mat_<Vec3b>::const_iterator itend = image.end<Vec3b>();
Mat_<uchar>::iterator itout = result.begin<uchar>(); //这是嵌套的命名空间
for (; it != itend; it++, itout++)
{
if (getDistanceToTargetColor(*it) <= maxDist)
*itout = 255;
else
*itout = 0;
}
return result;
}
//这样调用
ColorDetector colordetector(250,130,130);
result = colordetector(image);
5、grubCut算法
//I have to say this algorithm is so slow!
#include<opencv2/core.hpp>
#include<opencv2/highgui.hpp>
#include<opencv2/imgproc.hpp>
#include<iostream>
using namespace cv;
/*这里加入flag_down的原因是:在鼠标移动的过程中会一直调用鼠标事件函数,会一直执行啊这一句代码
imshow("这是选好的区域", tempImage);为了避免加入了flag_down*/
bool flag_move,flag_down = 1;
Point start;
Mat image;
Mat tempImage;
Rect rect;
void grabCutInitial()
{
}
void onMouse(int event, int x, int y, int flags, void* param)
{
switch (event)
{
case EVENT_LBUTTONDOWN://鼠标左键按下
if (flag_down)
{
flag_move = true;//flag置为true
start = Point(x, y);//记录左键按下时的坐标点
//std::cout << "123";
break;
}
case EVENT_MOUSEMOVE://鼠标移动
if (flag_move)//如果flag为true
{
flag_down = false;
/*如果不加这句代码,会让在移动过程中绘制的所有矩形都显示出来,
只有这样不断覆盖才可以最后得到想要的矩形*/
tempImage = image.clone();//将tempImg置为原图的拷贝copyImg
rectangle(tempImage, start, Point(x, y), Scalar(0, 255, 0), 1, 8);//画出矩形
rect = Rect(start, Point(x, y));//定义Rect区域
//std::cout << "213";
}
break;
case EVENT_LBUTTONUP://鼠标左键抬起
flag_move = false;//flag置为false
flag_down = true;
//std::cout << "321";
break;
default:
break;
}
//std::cout << "为什么我还没有按鼠标,你就开始执行鼠标事件函数那?还不止一次";
if((flag_move == false) && (flag_down == true))
imshow("这是选好的区域", tempImage);//显示绘制上矩形的tempImg
}
int main(void)
{
namedWindow("前景图", 0); //大小可调
image = imread("1.jpg", 1);
image.copyTo(tempImage);//这个必须在注册回调函数前拷贝,否则会导致第52行代码出错,为空
imshow("前景图", image);
setMouseCallback("前景图", onMouse, 0);
if (waitKey(0) == 32) //这个可以判断是否空格键按下了,waitKey函数返回按键的编码
{
Mat result;//定义用来存储结果的图像矩阵
Mat bgModle, fgModle; //定义两个内部矩阵
//调用grabCut函数,迭代5次,最后一个参数指明使用带边框的矩形模型
grabCut(image, result, rect, bgModle, fgModle, 5, GC_INIT_WITH_RECT);
//以CMP_EQ模式比较result与GC_PR_FGD(可能属于前景的像素),将结果存储在result中
compare(result, GC_PR_FGD, result, CMP_EQ);
//定义最终的空白输出图像
Mat foreground(image.size(), CV_8UC3, Scalar(255, 255, 255));
//以result为掩膜,得到最终图像
image.copyTo(foreground, result);
//result = result & 1;
imshow("结果图", foreground);
waitKey(0);
}
/*Mat result;//定义用来存储结果的图像矩阵
image = imread("1.jpg", 1); //必须以三通道形式读入
Rect rectangle(0, 0, 900, 600);//定义包含前景的矩形区域
Mat bgModle, fgModle; //定义两个内部矩阵
//调用grabCut函数,迭代4次,最后一个参数指明使用带边框的矩形模型
grabCut(image, result, rectangle, bgModle, fgModle, 5, GC_INIT_WITH_RECT);
//以CMP_EQ模式比较result与GC_PR_FGD(可能属于前景的像素),将结果存储在result中
compare(result, GC_PR_FGD, result, CMP_EQ);
//定义最终的空白输出图像
Mat foreground(image.size(), CV_8UC3, Scalar(255, 255, 255));
//以result为掩膜,得到最终图像
image.copyTo(foreground, result);
//result = result & 1;
imshow("前景图", foreground);
waitKey(0);*/
}
这段代码使用两种方法来定义矩形区域,然后用相同的方式执行了grubCut算法,主函数里被注释的那些事使用预定义的矩形来当做算法的参数,没有被注释的一部分使用鼠标事件来自己绘制矩形区域,作为算法的参数。这段代码还可以加东西,比如用自定义的掩膜作为函数的第二个参数。自定义掩膜涉及到操作像素,由于需要操作的像素比较少,建议使用直接操作的方法。
6、对于RGB颜色空间,当三个分量的取值相同时,就可以得到灰度图像。
7、RGB色彩空间并不是色彩均匀的色彩空间,对颜色之间的差距感知程度不一样。CIE L*a*b是这种模型之一。
void cvtColor( InputArray src, OutputArray dst, int code, int dstCn = 0 );这是转换函数的原型,第三个参数可以举例为COLOR_BGR2Lab,书上的那种表示方法已经过时了。
8、HSV色彩空间
色调(hue)、饱和度(saturation)、亮度(value),它是一个直观的色彩空间
下面这张图是对上图代码中色调掩码越过中轴线那一部分的解释,色调的模型是圆周,饱和度的模型是数轴,所以对色调的掩膜求法不能同于饱和度。
END!