『OpenCV3』基于色彩分割图片
一、遍历图像实现色彩掩码
本节我们实现这样一个算法,我们指定某种颜色和一个阈值,根据输入图片生成一张掩码,标记符合的像素(和指定颜色的差异在阈值容忍内)。
源代码如下,我们使用一个class完成这个目标,其指定了两种构建函数,并通过逐像素扫描的形式生成掩码(process成员函数)。另外,本class做了仿函数处理(operator成员函数),类似于python中的__call__方法,可以直接调用实例像函数一样进行处理。注意迭代器的使用,需要++it而非
class ColorDetector { private: int maxDist; // 允许的最小差距 cv::Vec3b target; // 目标颜色 cv::Mat result; // 结果Mask图像 public: // 空构造函数 ColorDetector() :maxDist(100), target(0, 0, 0) {}; ColorDetector(uchar blue, uchar green, uchar red, int maxDist) : maxDist(maxDist) { setTargetColor(blue, green, red); }; // 设置颜色差距阈值 void setColorDistanceThreshold(int distance) { if (distance < 0) distance = 0; maxDist = distance; }; // 获取颜色差距阈值 int getColorDistanceThreshold() { return maxDist; }; // 设置待检测颜色 void setTargetColor(uchar blue, uchar green, uchar red) { target = cv::Vec3b(blue, green, red); }; void setTargetColor(cv::Vec3b color) { target = color; }; // 计算与目标颜色的差距 int getDistanceToTargetColor(const cv::Vec3b& color) const { return getColorDistance(color, target); }; // 计算两个颜色之间的距离 int getColorDistance(const cv::Vec3b& color1, const cv::Vec3b& color2) const { return abs(color1[0] - color2[0]) + abs(color1[1] - color2[1]) + abs(color1[2] - color2[2]); }; cv::Mat process(const cv::Mat &image); // operator()使类像函数一样工作 cv::Mat operator()(const cv::Mat &image) { return process(image); }; }; cv::Mat ColorDetector::process(const cv::Mat &image) { // 为Mask结果申请空间 result.create(image.size(), CV_8U); cv::Mat_<cv::Vec3b>::const_iterator it = image.begin<cv::Vec3b>(); cv::Mat_<cv::Vec3b>::const_iterator itend = image.end<cv::Vec3b>(); cv::Mat_<uchar>::iterator itout = result.begin<uchar>(); for (; it != itend; ++it, ++itout) { if (getDistanceToTargetColor(*it) < maxDist) { *itout = 255; } else { *itout = 0; }; }; return result; };
两种调用方法都列举了出来:
void code_3() { // 创建色彩检测器对象 ColorDetector cdetect; cdetect.setTargetColor(10, 50, 10); // 读取图片 cv::Mat image = cv::imread("test.jpg"); // 处理图片 cv::Mat result = cdetect.process(image); cv::imshow("色彩检测", result); // 仿函数 ColorDetector colordetector(230, 190, 130, 150); result = colordetector(image); cv::imshow("仿函数色彩检测", result); };
输出图像展示:
二、threshold函数
使用如下成员函数替换上面的同名成员函数,
cv::Mat ColorDetector::process(const cv::Mat &image) { cv::Mat output; // output存储每个像素点(3通道)残差绝对值 cv::absdiff(image, cv::Scalar(target), output); std::vector<cv::Mat> channels; cv::split(output, channels); // output存储每个位置3通道残差和 output = channels[0] + channels[1] + channels[2]; std::cout << output.channels() << std::endl; // 判断每个位置像素和偏差不大于阈值即为所寻点,生成掩码 cv::threshold( output, // 输入 output, // 输出 maxDist, // 阈值,需要小与255 255, // 标记值(符合条件点) cv::THRESH_BINARY_INV // 不大于阈值点标记为标记值 ); return output; };
使用OpenCV的掩码生成函数threshold可以优化速度,不过由于需要一些中间过程,会消耗额外的内存,输出也可能(万一OpenCV工程师们在源码里添加了奇技淫巧呢)略有差异,
三、Mat(包含Scalar)数值运算API的优势
cv::Vec3b和cv::Scalar
我们简单的提一句cv::Vec3b和cv::Scalar的区别,两者都可以表示3通道像素的基本点,不过Vec更倾向对于原始的数据的格式化view,即和Mat耦合度不高,仅仅是个3元素数组;而Scalar更抽象倾向于表示一个像素,可以和Mat直接广播运算。
Mat的数值运算API
对比两种方式生成的残差矩阵,可以看到第一幅图中偏黑的部分在第二幅图上对应位置是特别亮的部分,联想到我们矩阵的类型是uchar,即无符号整形,存在上溢情况,可以推断API计算(即Mat的数值运算,包含上面程序中使用的加法运算符重载)出来的残差针对0~255做了截断,有效的防止了上溢:
我们将自己实现颜色差值计算的函数进行修改,添加上截断部分,
// 计算两个颜色之间的距离 int getColorDistance(const cv::Vec3b& color1, const cv::Vec3b& color2) const { return cv::saturate_cast<uchar>( abs(color1[0] - color2[0]) + abs(color1[1] - color2[1]) + abs(color1[2] - color2[2])); };
再查看输出的残差图像,发现和API计算结果已经一致(如下图)。
四、基于HSV色彩空间分割皮肤
原理很简单的一个例子,使用HSV色彩空间的两个通道设定阈值筛选符合的像素即可,要点:
HSV空间中的色调、饱和度两项指标可以用于分割皮肤
cv::cvtColor函数用于色彩空间转换
色调空间呈现环状(0~360),所以当max>min时,取两者之间的,当max<=min时,取小于max的和大于min的并集
色调空间在8位表示时使用0~180代替0~360
cv::inRange(img, minpixel, maxpixel, mask) 函数和threshold函数类似,生成掩码
Mat操作熟练地使用位运算可以提升程序效率,并且简化逻辑设计
函数源码见下面
void detectHScolor( const cv::Mat& image, // 输入图片 double minHue, double maxHue, // 色调区间 double minSat, double maxSat, // 饱和度区间 cv::Mat& mask // 输出掩码 ) { cv::Mat hsv; cv::cvtColor(image, hsv, CV_BGR2HSV); std::vector<cv::Mat> channels; cv::split(hsv, channels); // 色调掩码,色调是环形的 // 记录小于maxHue cv::Mat mask0; cv::threshold( channels[0], mask0, maxHue, 255, cv::THRESH_BINARY_INV ); // 记录大于minHue cv::Mat mask1; cv::threshold( channels[0], mask1, maxHue, 255, cv::THRESH_BINARY ); cv::Mat hueMask; if (minHue < maxHue) hueMask = mask0 & mask1; else hueMask = mask0 | mask1; // 饱和度掩码 cv::Mat satMask; cv::inRange(channels[1], minSat, maxSat, satMask); mask = hueMask & satMask; }
调用代码:
cv::Mat skin = cv::imread("skin.jfif"); cv::Mat mask; detectHScolor(skin, 160, 10, 25, 166, mask); cv::Mat detected(skin.size(), CV_8UC3, cv::Scalar(0, 0, 0)); skin.copyTo(detected, mask); cv::imshow("皮肤检测", detected);
效果如下,很一般(本来是找了张人脸做实验的,不过出来的图容易引起不适,虽改之 ),而且由于滤镜的关系地面呈暖色调,没有成功的剔除掉。