作者:翟天保Steven
版权声明:著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处

实现原理

天空变换是图像分割的一种应用,把图像中的天空与非天空区分割开,结合掩膜将天空更改为其他图像。如何较优地实现天空变换,与识别证件照类似,难点在于两个:

  1. 天空分割。将图像转为HSV并对S和V通道进行直方图均衡化,再通过设定的HSV三通道阈值选定天空的颜色范围,进而提取天空区域(H为78-124,S为0-255,V为78-255,该参数为我大量测试后凭经验所设,考虑到图像多样性,函数我提供了参数接口以便动态调整);thresh为天空区域的掩膜图,反相后的thresh_为非天空区域掩膜图,接下来识别非天空区的轮廓区域,采用外部轮廓方式,这样能提取出多个轮廓区,保留最大的轮廓区,不出意外这个就是前景区(出意外就自己重写该部分代码来判断真实的前景区);然后闭运算填充轮廓区内部微小孔洞,注意这个参数越大,轮廓越完整,但代价是一些孔洞处没法进行图像更换,所以自己把握参数;进行均值滤波,这一步是为了边缘平滑,为后续新天空和非天空区的融合作铺垫;输出Foreground,该掩膜图255的区域为非天空区。
  2. 两区域边缘融合。如果不能很好地融合,就能看出明显的抠图痕迹,所以融合是很关键的一步。首先,将新天空图尺寸调整为原图尺寸;其次,对蒙版区(掩膜)进行均值滤波,其边缘区会生成介于0-255之间的缓存区;再通过比例分配的方式对缓存区的像素点上色,我固定的比例为前景0.3天空0.7,可以使得缓存区颜色倾向于天空色,且实现较好地过渡;最后,蒙版为0的区域为新天空图,蒙版为255的区域不变。

至此,完成了天空变换。C++实现代码如下。

功能函数代码

// 天空分离
cv::Mat SkySeparation(cv::Mat src, Inputparama input)
{// 异常数值修正input.low_h = max(uchar(0), min(uchar(255), input.low_h));input.high_h = max(uchar(0), min(uchar(255), input.high_h));input.low_s = max(uchar(0), min(uchar(255), input.low_s));input.high_s = max(uchar(0), min(uchar(255), input.high_s));input.low_v = max(uchar(0), min(uchar(255), input.low_v));input.high_v = max(uchar(0), min(uchar(255), input.high_v));input.close_size= max(0, min(10, input.close_size));input.blur_size = max(0, min(10, input.blur_size));// 转为hsv通道cv::Mat hsv,nhsv,thresh;cvtColor(src, hsv, COLOR_BGR2HSV);vector<cv::Mat> hsvs;split(hsv, hsvs);cv::Mat h,s,v;// 直方图均衡化equalizeHist(hsvs[1], s);equalizeHist(hsvs[2], v);hsvs[1] = s.clone();hsvs[2] = v.clone();merge(hsvs, nhsv);// 按天空色选出mask并反相cv::Mat low=(cv::Mat_<uchar>{ input.low_h, input.low_s, input.low_v });cv::Mat high = (cv::Mat_<uchar>{ input.high_h, input.high_s, input.high_v  });inRange(nhsv, low, high, thresh);cv::Mat thresh_ = 255 - thresh;// 寻找轮廓,找出最大轮廓作为前景图vector<vector<Point>> contour;// , ncontour;vector<Vec4i> hierarchy;findContours(thresh_, contour, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_NONE);cv::Mat Foreground=thresh_.clone();if (!contour.empty() && !hierarchy.empty()){int max = 0;std::vector<std::vector<cv::Point> >::const_iterator itc = contour.begin();std::vector<std::vector<cv::Point> >::const_iterator itmax;// 遍历所有轮廓int i = 1;while (itc != contour.end()){double area = cv::contourArea(*itc);if (area > max){itmax = itc;max = area;}itc++;}for (auto it = contour.begin(); it != contour.end(); it++){if (it!=itmax){cv::Rect rect = cv::boundingRect(cv::Mat(*it));for (int i = rect.y; i < rect.y + rect.height; i++){uchar *output_data = Foreground.ptr<uchar>(i);for (int j = rect.x; j < rect.x + rect.width; j++){// 将连通区的值置0if (output_data[j] == 255){output_data[j] = 0;}}}}}}// 闭运算cv::Mat element = getStructuringElement(MORPH_ELLIPSE, Size(2*input.close_size+1, 2 * input.close_size + 1));cv::morphologyEx(Foreground, Foreground, MORPH_CLOSE, element);// 滤波cv::blur(Foreground, Foreground, Size(2 * input.blur_size + 1, 2 * input.blur_size + 1));return Foreground;
}

C++测试代码

#include <iostream>
#include <opencv2/opencv.hpp>
#include <time.h>using namespace std;
using namespace cv;// 输入参数
struct Inputparama { uchar low_h = 78;                     // 识别天空区域hsv颜色的最底H值uchar high_h = 124;                   // 识别天空区域hsv颜色的最高H值uchar low_s = 0;                      // 识别天空区域hsv颜色的最底S值uchar high_s = 255;                   // 识别天空区域hsv颜色的最高S值uchar low_v = 78;                     // 识别天空区域hsv颜色的最底V值uchar high_v = 255;                   // 识别天空区域hsv颜色的最高V值int close_size = 4;                 // 非天空区域闭运算尺寸,该值越大则区域越完整,代价是一些孔洞处没法进行图像更换int blur_size = 2;                  // 非天空区域滤波窗口尺寸,该值越大则天空与非天空区衔接处越模糊,适当的数值可以带来较优的融合效果
};cv::Mat SkySeparation(cv::Mat src, Inputparama input);
cv::Mat ImageFusion(cv::Mat src1, cv::Mat src2, cv::Mat mask);int main()
{cv::Mat src = imread("test3.jpg");cv::Mat sky = imread("sky5.jpg");Inputparama input;input.low_h = 78;input.high_h = 124;input.low_s = 0;input.high_s = 255;input.low_v = 78;input.high_v = 255;input.close_size = 4;input.blur_size = 2;clock_t s, e;s = clock();cv::Mat thresh = SkySeparation(src,input);cv::Mat result = ImageFusion(src, sky, thresh);e = clock();double dif = (e - s) / CLOCKS_PER_SEC;cout << "time:" << dif << endl;imshow("original", src);imshow("result", result);waitKey(0);return 0;
}// 天空分离
cv::Mat SkySeparation(cv::Mat src, Inputparama input)
{// 异常数值修正input.low_h = max(uchar(0), min(uchar(255), input.low_h));input.high_h = max(uchar(0), min(uchar(255), input.high_h));input.low_s = max(uchar(0), min(uchar(255), input.low_s));input.high_s = max(uchar(0), min(uchar(255), input.high_s));input.low_v = max(uchar(0), min(uchar(255), input.low_v));input.high_v = max(uchar(0), min(uchar(255), input.high_v));input.close_size= max(0, min(10, input.close_size));input.blur_size = max(0, min(10, input.blur_size));// 转为hsv通道cv::Mat hsv,nhsv,thresh;cvtColor(src, hsv, COLOR_BGR2HSV);vector<cv::Mat> hsvs;split(hsv, hsvs);cv::Mat h,s,v;// 直方图均衡化equalizeHist(hsvs[1], s);equalizeHist(hsvs[2], v);hsvs[1] = s.clone();hsvs[2] = v.clone();merge(hsvs, nhsv);// 按天空色选出mask并反相cv::Mat low=(cv::Mat_<uchar>{ input.low_h, input.low_s, input.low_v });cv::Mat high = (cv::Mat_<uchar>{ input.high_h, input.high_s, input.high_v  });inRange(nhsv, low, high, thresh);cv::Mat thresh_ = 255 - thresh;// 寻找轮廓,找出最大轮廓作为前景图vector<vector<Point>> contour;// , ncontour;vector<Vec4i> hierarchy;findContours(thresh_, contour, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_NONE);cv::Mat Foreground=thresh_.clone();if (!contour.empty() && !hierarchy.empty()){int max = 0;std::vector<std::vector<cv::Point> >::const_iterator itc = contour.begin();std::vector<std::vector<cv::Point> >::const_iterator itmax;// 遍历所有轮廓int i = 1;while (itc != contour.end()){double area = cv::contourArea(*itc);if (area > max){itmax = itc;max = area;}itc++;}for (auto it = contour.begin(); it != contour.end(); it++){if (it!=itmax){cv::Rect rect = cv::boundingRect(cv::Mat(*it));for (int i = rect.y; i < rect.y + rect.height; i++){uchar *output_data = Foreground.ptr<uchar>(i);for (int j = rect.x; j < rect.x + rect.width; j++){// 将连通区的值置0if (output_data[j] == 255){output_data[j] = 0;}}}}}}// 闭运算cv::Mat element = getStructuringElement(MORPH_ELLIPSE, Size(2*input.close_size+1, 2 * input.close_size + 1));cv::morphologyEx(Foreground, Foreground, MORPH_CLOSE, element);// 滤波cv::blur(Foreground, Foreground, Size(2 * input.blur_size + 1, 2 * input.blur_size + 1));return Foreground;
}// 前景背景融合
cv::Mat ImageFusion(cv::Mat src1, cv::Mat src2, cv::Mat mask)
{cv::Mat sky;resize(src2, sky, Size(src1.cols, src1.rows));cv::Mat result = src1.clone();int row = src1.rows;int col = src1.cols;// 改色for (int i = 0; i < row; ++i){uchar *s1 = result.ptr<uchar>(i);uchar *s2 = sky.ptr<uchar>(i);uchar *m = mask.ptr<uchar>(i);for (int j = 0; j < col; ++j){// 蒙版为0的区域就是标准背景区if (m[j] == 0){s1[3 * j] = s2[3 * j];s1[3 * j + 1] = s2[3 * j + 1];s1[3 * j + 2] = s2[3 * j + 2];}// 不为0且不为255的区域是轮廓区域(边缘区),需要虚化处理else if (m[j] != 255){// 边缘处按比例上色int newb = (s1[3 * j] * m[j] * 0.3 + s2[3 * j] * (255 - m[j])*0.7) / ((255 - m[j])*0.7 + m[j] * 0.3);int newg = (s1[3 * j + 1] * m[j] * 0.3 + s2[3 * j + 1] * (255 - m[j])*0.7) / ((255 - m[j])*0.7 + m[j] * 0.3);int newr = (s1[3 * j + 2] * m[j] * 0.3 + s2[3 * j + 2] * (255 - m[j])*0.7) / ((255 - m[j])*0.7 + m[j] * 0.3);newb = max(0, min(255, newb));newg = max(0, min(255, newg));newr = max(0, min(255, newr));s1[3 * j] = newb;s1[3 * j + 1] = newg;s1[3 * j + 2] = newr;}}}return result;
}

测试效果

图1 对比图1

图2 对比图2

图3 对比图3

图4 对比图4

如源码所示,函数输入参数共有5项,其说明如下:

  1. 前6个参数分别为hsv三通道的最大最小值。
  2. close_size为闭运算尺寸,如果处理的图像中有小树林,建议尺寸调小,不然小树林间的缝隙就是原图,有点不协调。
  3. blur_size为滤波窗口尺寸,平滑天空与非天空区衔接处。

总的来说,图像如果有明显天空背景,基本都能成功;天空白色区域过多可能识别不准,因为白色的hsv值和蓝色差太多;天空下面有大片海水,也不太行,就识别出来不符合现实逻辑。

源码只有100多行,看懂原理最重要,比直接调用api更能学到知识。永远记住,“代码是死的,场景是多变的,而人是活的。”,针对不同场景,合理改写代码,才能产出最适合你的代码。

如果函数有什么可以改进完善的地方,非常欢迎大家指出,一同进步何乐而不为呢~

如果文章帮助到你了,可以点个赞让我知道,我会很快乐~加油!

OpenCV-实现天空变换(图像分割)相关推荐

  1. OpenCV中的图像处理 —— 霍夫线 / 圈变换 + 图像分割(分水岭算法) + 交互式前景提取(GrabCut算法)

    OpenCV中的图像处理 -- 霍夫线 / 圈变换 + 图像分割(分水岭算法) + 交互式前景提取(GrabCut算法)

  2. OpenCV离散傅立叶变换DFT的实例(附完整代码)

    OpenCV离散傅立叶变换DFT的实例 OpenCV离散傅立叶变换DFT的实例 OpenCV离散傅立叶变换DFT的实例 #include "opencv2/core.hpp" #i ...

  3. OpenCV使用F变换过滤

    OpenCV使用F变换过滤 使用F变换过滤 目标 模糊变换应用 代码 解释 使用F变换过滤 目标 本教程向您演示如何使用F变换进行图像过滤.你会看见: 背后的基本理论 不同设置的插图. 模糊变换应用 ...

  4. OpenCV使用F变换进行修补

    OpenCV使用F变换进行修补 使用F变换进行修补 目标 介绍 模糊变换应用 代码 解释 使用F变换进行修补 目标 在本教程中,您将学习使用F变换修复图像的工作原理.它包括: 背后的基本理论 三种不同 ...

  5. OpenCV离散傅立叶变换

    OpenCV离散傅立叶变换 离散傅立叶变换 目标 源代码 解释 将图像放大到最佳尺寸 兼顾复杂和真实的值 进行离散傅立叶变换 将真实和复杂的值转换为幅度 裁剪并重新排列 归一化 结果 离散傅立叶变换 ...

  6. Python+OpenCV:形态学变换

    Python+OpenCV:形态学变换 理论 形态学变换是基于图像形状的一些简单操作. 它通常在二值图像上执行.它需要两个输入,一个是我们的原始图像,另一个是结构元素(structuring elem ...

  7. OpenCV Laplacian 拉普拉斯变换

    ::返回OpenCV算子速查表 OpenCV Laplacian 拉普拉斯变换 1. 函数 2. 例程 原图 Sobel和Scharr的处理结果 Laplacian 1. 函数 OpenCV官方文档链 ...

  8. opencv之伪彩变换

    opencv之伪彩变换 我们在处理红外图像时,由于红外图像都是16位的数据,灰度范围较大,对比度不明显,经常通过直方图均衡的方式将图像增强进行8位的显示. 红外图像在一定程度上反映了环境中物体的温度变 ...

  9. OpenCV(26)图像分割 -- 距离变换与分水岭算法(硬币检测、扑克牌检测、车道检测)

    目录 一.基础理论 1.思想 2.原理 二.分水岭实战:硬币 步骤归纳 1.把原图像转二值图 2.开运算去噪 3.确定背景区域(膨胀)(得到背景/最大连通域) 4.确定前景区域(距离变换) (分离)( ...

最新文章

  1. 杨建:网站加速--内容简介
  2. boost::phoenix::function用法的测试程序
  3. prim算法适用条件_内部排序算法的比较及应用
  4. 【matlab】元胞数组的创建
  5. webpack(一)
  6. 手机APP测试如何进行兼容性测试?
  7. Passenger简介
  8. html5页面风格,H5页面的设计风格有哪些?
  9. Excel入门(二)
  10. 抖音搬运视频热门技巧 剪辑后会修改视频md5
  11. 5. 统计学基础1:平均值...四分位数、方差、标准差(均方差)、标准误(标准误差、均方根误差)、 标准分
  12. ubuntu下安装电驴
  13. 当生活真的过得很艰难的时候,你是怎么熬过来的?(内容来自知乎,用于自省)...
  14. 谷歌cloud_Google Cloud如何为您的应用程序安全提供帮助?
  15. idea子模块使用父模块变量properties in parent definition are prohibited
  16. Frame size of 257 MB larger than max allowed 100 MB
  17. 光纤宽带接入工程设计降本增效方案集(2)
  18. 云计算就业前景怎么样 学完后能胜任什么岗位
  19. 注意:Martin Fowler的微服务
  20. 关于JavaScript精确度问题

热门文章

  1. 时间继电器应该怎么选型
  2. 旅游行业如何做好网络舆情监测?
  3. 介绍Namada:链间资产不可知(Asset-agnostic)隐私
  4. 一部值得成年人反复观看的剧
  5. LaTeX公式(对小白十分友好的线上公式生成神器)
  6. 孙子兵法 军形第四(翻译)
  7. 特斯拉Autopilot软件重写
  8. harbor安装时出现ERROR: An HTTP request took too long to complete. Retry with --verbose to obtain debug in
  9. 深度学习与神经网络-吴恩达-第二周优化算法
  10. Java实现简单航班查询系统