一、RGB与HSV颜色系统
数字图像处理中常用的采用模型是RGB(红,绿,蓝)模型和HSV(色调,饱和度,亮度),RGB广泛应用于彩色监视器和彩色视频摄像头,我们平时的图片一般都是RGB模型。而HSV模型更符合人描述和解释颜色的方式,HSV的彩色描述对人来说是自然且非常直观的,CAMshift是基于HSV颜色系统的。
HSV模型中颜色的参数分别是:色调(H:hue),饱和度(S:saturation),亮度(V:value)。由A. R. Smith在1978年创建的一种颜色空间, 也称六角锥体模型(Hexcone Model)。
- 色调(H:hue):用角度度量,取值范围为0°~360°,从红色开始按逆时针方向计算,红色为0°,绿色为120°,蓝色为240°。它们的补色是:黄色为60°,青色为180°,品红为300°;
- 饱和度(S:saturation):取值范围为0.0~1.0,值越大,颜色越饱和。
- 亮度(V:value):取值范围为0(黑色)~255(白色)
OpenCV下有个函数可以直接将RGB模型转换为HSV模型,OpenCV中H∈ [0, 180), S ∈ [0, 255], V ∈ [0, 255]。我们知道H分量基本能表示一个物体的颜色,但是S和V的取值也要在一定范围内,因为S代表的是H所表示的那个颜色和白色的混合程度,也就说S越小,颜色越发白,也就是越浅;V代表的是H所表示的那个颜色和黑色的混合程度,也就说V越小,颜色越发黑。
经过实验,一些基本的颜色H的取值可以如下设置:
Green 38-75,Blue 75-130,Red 160-179
OpenCV 的文档中是这样解释的:原本输出的 HSV 的取值范围分别是 0-360, 0-1, 0-1;但是为了匹配目标数据类型 OpenCV 将每个通道的取值范围都做了修改,于是就变成了 0-180, 0-255, 0-255,并且同时解释道:为了适应 8bit 0-255 的取值范围,将 hue 通道 0-360 的取值范围做了减半处理,这就是为什么 OpenCV 中 H通道的取值范围是0-180。
二、图像的前置操作
- 读取视频
//打开视频
VideoCapture capture;
capture.open("test.mp4");
if (!capture.isOpened())
{
printf("could not find \n");
}
//命名窗口
namedWindow("Origin", CV_WINDOW_AUTOSIZE);
namedWindow("ROI", CV_WINDOW_AUTOSIZE);
- 选中ROI区域
此处直接调用selectROI,selectROI是跟踪API的一部分,需要引用Opencv_contrib库。
Rect2d first = selectROI("Origin", frame);
// int 转换 double
selection.x = first.x;
selection.y = first.y;
selection.width = first.width;
selection.height = first.height;
printf("ROLx = %d;ROLy=%d,width =%d;height = %d", selection.x, selection.y, selection.width, selection.height);
- 将图像从RGB表示转化为HSV表示
OpenCV中使用cvtColor()函数就可以实现颜色空间的转换。注意opencv中颜色方块的排列顺序是BGR,而不是熟悉的RGB,因此颜色映射码是COLOR_BGR2HSV。
//转换到 HSV
cvtColor(frame, hsv, COLOR_BGR2HSV);
- 提取Hue分量
获取hsv图像后,需要提取出其中的Hue分量,即颜色分割。
inRange(hsv, Scalar(0, smin, vmin), Scalar(180, smax, vmax), mask);
inRange()函数的功能是检查输入数组的每个元素是不是在给定范围内。代码中可以看出,检查的是hsv的像素的Hue分量是否在0-180之间,Saturation分量是否在smin-255之间,Value分量是否在vmin和vmax之间,返回验证矩阵mask,如果hsv的像素点满足条件,那么mask矩阵中对应位置的点置255,不满足条件的置0。
HSV范围是opencv中规定的,因此Hue的范围是0-180,Saturation和Value的范围是0-255。如果是Photoshop,那么HSV的范围就是0-360°和0-1了。
- 提取Hue分量
通道复制函数mixChannels(),此函数由输入参数复制某通道到输出参数特定的通道中。
void mixChannels(
const Mat* src, //输入的数组,所有的数组必须有相同的尺寸和深度
size_t nsrcs, //第一个参数src输入的矩阵数
Mat* dst, //输出的数组,所有矩阵必须被初始化,且大小和深度必须与src[0]相同
size_t ndsts, //第三个参数dst输入的矩阵数
const int* fromTo,//对指定的通道进行复制的数组索引
size_t npairs) //第五个参数fromTo的索引数
mixChannels(&hsv, 1, &hue, 1, channels, 1);
输入一个矩阵hsv,输出一个size和depth与hsv完全相同的矩阵hue,复制hsv[0]通道到hue[0]通道,也就是“提取”图像的Hue分量,储存在hue矩阵中。
拿到图像Hue分量的数据后,图像的前置处理完成。
三、ROI区域直方图计算与绘制
图像的前置处理完成并获取hue矩阵后,需要计算ROI区域关于hue的一维颜色直方图。这个直方图除非重新框选,否则在循环中只计算和绘制一次。calcHist()函数原型如下:
calcHist(
const Mat* images, //输入的数组
int nimages, //输入数组的个数
const int* channels, //需要统计的通道索引
InputArray mask, //掩膜
OutputArray hist, //输出的目标直方图
int dims, //需要计算的直方图维度
const int* histSize, //在每一维上直方图的个数。如果是一维直方图,就是竖条(bin)的个数。
const float** ranges, //每一维数值的取值范围数组
bool uniform, //直方图是否均匀的标识符
bool accumulate //是否累加。如果为true,在下次计算的时候不会首先清空hist
)
//ROI直方图计算
Mat roi(hue, selection);
Mat maskroi(mask, selection);
//输入的数组&roi只有一个,统计的通道是0通道
//使用的掩膜是maskroi,输出一维直方图,有16个竖条,取值范围是{0,180}。
calcHist(&roi, 1, 0, maskroi, hist, 1, &bins, &Phranges);
//直方图之后再归一化到0-255
normalize(hist, hist, 0, 255, NORM_MINMAX);
//画出直方图
//每个条的宽度
int binw = drawing.cols / bins;
//定义一个缓冲单bin矩阵,1行16列,用于存放颜色数据,用于直方图hsize个bin的“染色”。
Mat colorIndex = Mat(1, bins, CV_8UC3);
for (int i = 0;i < bins;i++)
{
colorIndex.at<Vec3b>(0, i) = Vec3b(saturate_cast<uchar>(i * 180 / bins), 255, 255);
}
cvtColor(colorIndex, colorIndex, COLOR_HSV2BGR);
for (int i = 0;i < bins; i++)
{
//val是直方图hist的相对histimg的高度。hist.at(i)获取了第i个bin直方图数据,除以255后得到百分比
//再乘以histimg的行数就得到了相对高度,最后进行int的强制类型转换,转换为整数。
int val = saturate_cast<int>(hist.at<float>(i)*drawing.rows / 255);
//之后使用rectangle()函数进行16个bin的绘制值得注意的是矩阵的坐标系以左上角为原点,y轴是向下的,而需要展示给人看的直方图图案是左下角为原点,y轴向上的
//因此rectangle的两个标定点的纵坐标是histimg.rows和(histimg.rows - val)而不是0和val。
rectangle(drawing, Point(i*binw, drawing.rows), Point((i + 1)*binw, drawing.rows - val), Scalar(colorIndex.at<Vec3b>(0, i)), -1, 8, 0);
}
四、反向投影
calcBackProject()函数原型如下:
void calcBackProject(
const Mat* images, //输入的数组
int nimages, //输入数组的个数
const int* channels, //需要统计的通道索引
InputArray hist, //输入的直方图
OutputArray backProject,//目标的反向投影
const float** ranges, //每一位数值的取值范围
double scale=1, //输出方向投影的缩放因子
bool uniform=true //指示直方图是否均匀的标识符
)
calcBackProject(&hue, 1, 0, hist, backprojection, &Phranges);
backprojection &= mask;
五、CAMshift目标追踪
RotatedRect CamShift(
InputArray _probImage,
Rect& window,
TermCriteria criteria
)
CamShift函数接受3个参数:反向投影,矩形搜索框,和迭代中止条件。TermCriteria( CV_TERMCRIT_EPS | CV_TERMCRIT_ITER, 10, 1 ))
示例意思:精度先达到1或者迭代次数先达到10次时,停止迭代
获取CamShift的返回值,是一个旋转矩形,根据旋转矩形绘制一个椭圆形显示在图像上作为追踪结果。
//CamShift函数接受3个参数:反向投影,矩形搜索框,和迭代中止条件。
RotatedRect trackBox = CamShift(backprojection, selection, TermCriteria((TermCriteria::COUNT | TermCriteria::EPS), 10, 1));
//获取CamShift的返回值,是一个旋转矩形,根据旋转矩形绘制一个椭圆形显示在图像上作为追踪结果。
ellipse(frame, trackBox, Scalar(0, 0, 255), 3, 8);
流程图
效果图
完整源代码已经上传CSDN下载,如需要可自行下载