前言

  对于图像拼接,前面探讨了通过基于Stitcher进行拼接过渡和基于特征点进行拼接过渡,这2个过渡的方式是摄像头拍摄角度和方向不应差距太大。
  对于特定的场景,本身摄像头拍摄角度差距较大,拉伸变换后也难做到完美的缝隙拼接,这个时候使用渐近过渡反倒是最好的。

 

Demo

  单独蒙版
   

  

  

  

  蒙版过渡,这里只是根据图来,其实可对每个像素对于第一张图为系数k,而第二张为255-k,实现渐近过渡。
  

  

  

  

  直接使用第一张蒙版优化
  

  

  

 

准本蒙版

  蒙版可以混合,也可以分开,为了让读者更好的深入理解原理,这里都使用:
  找个工具,造单色渐进色,红色蒙版,只是r通道,bga都为0
  

  (注意:使用rgba四通道)
  

  (上面这张图,加了边框,导致了“入坑二”打印像素值不对)
  

  由于工具渐进色无法叠层,这个工具无法实现rgba不同向渐进色再一张图(横向、纵向、斜向),更改了方式,每个使用一张图:
  为了方便,不管a通道了,直接a为100%(255)。
  

  再弄另外一个通道的:
  

  在这里使用工具就只能单独一张了:
  

 

一个蒙版图的过渡实例

步骤一:打开图片和蒙版

  

   cv::Mat matLeft = cv::imread("D:/qtProject/openCVDemo/openCVDemo/modules/openCVManager/images/29.jpg");
cv::Mat matRight = cv::imread("D:/qtProject/openCVDemo/openCVDemo/modules/openCVManager/images/30.jpg");
cv::Mat matMask1 = cv::imread("D:/qtProject/openCVDemo/openCVDemo/modules/openCVManager/images/37.png", cv::IMREAD_UNCHANGED);
cv::Mat matMask2 = cv::imread("D:/qtProject/openCVDemo/openCVDemo/modules/openCVManager/images/38.png", cv::IMREAD_UNCHANGED);
cv::Mat matMask3 = cv::imread("D:/qtProject/openCVDemo/openCVDemo/modules/openCVManager/images/39.png", cv::IMREAD_UNCHANGED);
cv::Mat matMask4 = cv::imread("D:/qtProject/openCVDemo/openCVDemo/modules/openCVManager/images/40.png", cv::IMREAD_UNCHANGED);

步骤二:将蒙版变成和原图一样大小

  

    cv::resize(matLeft, matLeft, cv::Size(0, 0), 0.5, 0.5);
cv::resize(matRight, matRight, cv::Size(0, 0), 0.5, 0.5);
cv::resize(matMask1, matMask1, cv::Size(matLeft.cols, matLeft.rows));
cv::resize(matMask2, matMask2, cv::Size(matLeft.cols, matLeft.rows));
cv::resize(matMask3, matMask3, cv::Size(matLeft.cols, matLeft.rows));
cv::resize(matMask4, matMask4, cv::Size(matLeft.cols, matLeft.rows));

步骤三:底图

  由于两张图虽然是同样大小,但是其不是按照整体拼接后的大小,所以需要假设一个拼接后的大小的底图。
  

    // 底图,扩大500横向,方便移动
cv::Mat matResult = cv::Mat(matLeft.rows, matLeft.cols + 500, CV_8UC3);

步骤四:原图融合

  

        // 副本,每次都要重新清空来调整
cv::Mat matResult2 = matResult.clone();
#if 1
// 第一张图,直接比例赋值,因为底图为0
for(int row = 0; row < matLeft.rows; row++)
{
for(int col = 0; col < matLeft.cols; col++)
{
double r = matMask1.at<cv::Vec4b>(row, col)[2] / 255.0f;
// double r = matMask2.at<cv::Vec4b>(row, col)[1] / 255.0f;
// double r = matMask3.at<cv::Vec4b>(row, col)[0] / 255.0f;
// double r = matMask4.at<cv::Vec4b>(row, col)[0] / 255.0f;
matResult2.at<cv::Vec3b>(row, col)[0] = (matLeft.at<cv::Vec3b>(row, col)[0] * r);
matResult2.at<cv::Vec3b>(row, col)[1] = (matLeft.at<cv::Vec3b>(row, col)[1] * r);
matResult2.at<cv::Vec3b>(row, col)[2] = (uchar)(matLeft.at<cv::Vec3b>(row, col)[2] * r);
}
}
#endif

步骤五:另外一张图的融合

  

#if 1
// 第二张图,加法,因为底图为原图了
for(int row = 0; row < matRight.rows; row++)
{
for(int col = 0; col < matRight.cols; col++)
{
double g = matMask2.at<cv::Vec4b>(row, col)[1] / 255.0f;
// 偏移了x坐标
matResult2.at<cv::Vec3b>(row, col + x)[0] += matRight.at<cv::Vec3b>(row, col)[0] * g;
matResult2.at<cv::Vec3b>(row, col + x)[1] += matRight.at<cv::Vec3b>(row, col)[1] * g;
matResult2.at<cv::Vec3b>(row, col + x)[2] += matRight.at<cv::Vec3b>(row, col)[2] * g;
}
}
#endif

步骤六(与步骤五互斥):优化的融合

  

#if 1
// 第二张图,加法,因为底图为原图了(优化)
for(int row = 0; row < matRight.rows; row++)
{
for(int col = 0; col < matRight.cols; col++)
{
double r2;
if(x + col <= matLeft.cols)
{
r2 = (255 - matMask1.at<cv::Vec4b>(row, col + x)[2]) / 255.0f;
}else{
r2 = 1.0f;
}
// 偏移了x坐标
matResult2.at<cv::Vec3b>(row, col + x)[0] += matRight.at<cv::Vec3b>(row, col)[0] * r2;
matResult2.at<cv::Vec3b>(row, col + x)[1] += matRight.at<cv::Vec3b>(row, col)[1] * r2;
matResult2.at<cv::Vec3b>(row, col + x)[2] += matRight.at<cv::Vec3b>(row, col)[2] * r2;
}
}
#endif
 

函数原型

  手码的像素算法,没有什么高级函数。

 

Demo源码

void OpenCVManager::testMaskSplicing()
{
cv::Mat matLeft = cv::imread("D:/qtProject/openCVDemo/openCVDemo/modules/openCVManager/images/29.jpg");
cv::Mat matRight = cv::imread("D:/qtProject/openCVDemo/openCVDemo/modules/openCVManager/images/30.jpg");
cv::Mat matMask1 = cv::imread("D:/qtProject/openCVDemo/openCVDemo/modules/openCVManager/images/37.png", cv::IMREAD_UNCHANGED);
cv::Mat matMask2 = cv::imread("D:/qtProject/openCVDemo/openCVDemo/modules/openCVManager/images/38.png", cv::IMREAD_UNCHANGED);
cv::Mat matMask3 = cv::imread("D:/qtProject/openCVDemo/openCVDemo/modules/openCVManager/images/39.png", cv::IMREAD_UNCHANGED);
cv::Mat matMask4 = cv::imread("D:/qtProject/openCVDemo/openCVDemo/modules/openCVManager/images/40.png", cv::IMREAD_UNCHANGED); #if 0
// 打印通道数和数据类型
// ..\openCVDemo\modules\openCVManager\OpenCVManager.cpp 9166 "2024-10-31 20:07:42:619" 4 24 24
LOG << matMask.channels() << matMask.type() << CV_8UC4; // 4 24
// 打印mask蒙版行像素,隔一定行数打一次
for(int row = 0; row < matMask.rows; row += 10)
{
for(int col = 100; col < matMask.cols; col++)
{
int r = matMask.at<cv::Vec4b>(row, col)[2];
int g = matMask.at<cv::Vec4b>(row, col)[1];
int b = matMask.at<cv::Vec4b>(row, col)[0];
int a = matMask.at<cv::Vec4b>(row, col)[3];
LOG << "row:" << row << ", col:" << col << "r(rgba):" << r << g << b << a;
break;
}
}
#endif // 图片较大,缩为原来的0.5倍
cv::resize(matLeft, matLeft, cv::Size(0, 0), 0.5, 0.5);
cv::resize(matRight, matRight, cv::Size(0, 0), 0.5, 0.5);
cv::resize(matMask1, matMask1, cv::Size(matLeft.cols, matLeft.rows));
cv::resize(matMask2, matMask2, cv::Size(matLeft.cols, matLeft.rows));
cv::resize(matMask3, matMask3, cv::Size(matLeft.cols, matLeft.rows));
cv::resize(matMask4, matMask4, cv::Size(matLeft.cols, matLeft.rows));
// 底图,扩大500横向,方便移动
cv::Mat matResult = cv::Mat(matLeft.rows, matLeft.cols + 500, CV_8UC3); // 第一张图
int key = 0;
int x = 0;
while(true)
{
// 副本,每次都要重新清空来调整
cv::Mat matResult2 = matResult.clone();
#if 1
// 第一张图,直接比例赋值,因为底图为0
for(int row = 0; row < matLeft.rows; row++)
{
for(int col = 0; col < matLeft.cols; col++)
{
double r = matMask1.at<cv::Vec4b>(row, col)[2] / 255.0f;
// double r = matMask2.at<cv::Vec4b>(row, col)[1] / 255.0f;
// double r = matMask3.at<cv::Vec4b>(row, col)[0] / 255.0f;
// double r = matMask4.at<cv::Vec4b>(row, col)[0] / 255.0f;
matResult2.at<cv::Vec3b>(row, col)[0] = (matLeft.at<cv::Vec3b>(row, col)[0] * r);
matResult2.at<cv::Vec3b>(row, col)[1] = (matLeft.at<cv::Vec3b>(row, col)[1] * r);
matResult2.at<cv::Vec3b>(row, col)[2] = (uchar)(matLeft.at<cv::Vec3b>(row, col)[2] * r);
}
}
#endif
#if 0
// 第二张图,加法,因为底图为原图了
for(int row = 0; row < matRight.rows; row++)
{
for(int col = 0; col < matRight.cols; col++)
{
double g = matMask2.at<cv::Vec4b>(row, col)[1] / 255.0f;
// 偏移了x坐标
matResult2.at<cv::Vec3b>(row, col + x)[0] += matRight.at<cv::Vec3b>(row, col)[0] * g;
matResult2.at<cv::Vec3b>(row, col + x)[1] += matRight.at<cv::Vec3b>(row, col)[1] * g;
matResult2.at<cv::Vec3b>(row, col + x)[2] += matRight.at<cv::Vec3b>(row, col)[2] * g;
}
}
#endif
#if 1
// 第二张图,加法,因为底图为原图了(优化)
for(int row = 0; row < matRight.rows; row++)
{
for(int col = 0; col < matRight.cols; col++)
{
double r2;
if(x + col <= matLeft.cols)
{
r2 = (255 - matMask1.at<cv::Vec4b>(row, col + x)[2]) / 255.0f;
}else{
r2 = 1.0f;
}
// 偏移了x坐标
matResult2.at<cv::Vec3b>(row, col + x)[0] += matRight.at<cv::Vec3b>(row, col)[0] * r2;
matResult2.at<cv::Vec3b>(row, col + x)[1] += matRight.at<cv::Vec3b>(row, col)[1] * r2;
matResult2.at<cv::Vec3b>(row, col + x)[2] += matRight.at<cv::Vec3b>(row, col)[2] * r2;
}
}
#endif // cv::imshow("matMask1", matMask1);
// cv::imshow("matLeft", matLeft);
cv::imshow("matResult2", matResult2);
key = cv::waitKey(0);
if(key == 'a')
{
x--;
if(x < 0)
{
x = 0;
}
}else if(key == 'd')
{
x++;
if(x + matRight.cols > matResult2.cols)
{
x = matResult2.cols - matRight.cols;
}
}else if(key == 'q')
{
break;
}
}
}
 

工程模板v1.72.0

  

 

入坑

入坑一:读取通道rgba失败

问题:读取通道rgba失败

  

原因

  是uchar,转换成byte,而不是int
  

解决

  

  

入坑二:读取通道一直是0,0,0,255

问题

  读取通道一直是0,0,0,255。
  

原因

  弄了张图,还是255,然后发现是为了截图更清楚,弄了个边框,而我们打印正好是打印了0位置。
  

  

解决

  最终是要去掉边框,没边框就是空看不出,如下图:
  

  

入坑三:过渡有黑线赋值不对

问题

  直接位置赋值,出现条纹
  

  

原因

  类型是vec4b
  

解决

  

  

入坑四:原图融合比例有黑线

问题

  

原因

  跟上面一样,mask蒙版是rgba的,需要vec4b
  

解决

  

  

OpenCV开发笔记(八十二):两图拼接使用渐进色蒙版场景过渡缝隙的更多相关文章

  1. 树莓派开发笔记(十二):入手研华ADVANTECH工控树莓派UNO-220套件(一):介绍和运行系统

    前言   树莓派也可以做商业应用,工业控制,其稳定性和可靠性已经得到了验证,故而工业控制,一些停车场等场景也有采用树莓派作为主控的,本片介绍了研华ADVANTECH的树莓派套件组UNO-220-P4N ...

  2. .net开发笔记(十二) 设计时与运行时的区别(续)

    上一篇博客详细讲到了设计时(DesignTime)和运行时(RunTime)的概念与区别,不过没有给出实际的Demo,今天整理了一下,做了一个例子,贴出来分享一下,巩固前一篇博客讲到的内容. 简单回顾 ...

  3. 安卓开发笔记(十二):SQLite数据库储存(上)

    SQLite数据库存储(上) 创建数据库 Android专门提供了一个 SQLiteOpenHelper帮助类对数据库进行创建和升级 SQLiteOpenHelper需要创建一个自己的帮助类去继承它并 ...

  4. Java开发笔记(十二)布尔变量论道与或非

    在编程语言的设计之初,它们除了可以进行数学计算,还常常用于逻辑推理和条件判断.为了实现逻辑判断的功能,Java引入了一种布尔类型boolean,用来表示“真”和“假”.该类型的变量只允许两个取值,即t ...

  5. PID控制器开发笔记之十二:模糊PID控制器的实现

    在现实控制中,被控系统并非是线性时不变的,往往需要动态调整PID的参数,而模糊控制正好能够满足这一需求,所以在接下来的这一节我们将讨论模糊PID控制器的相关问题.模糊PID控制器是将模糊算法与PID控 ...

  6. ESP32 开发笔记(十二)LittlevGL 添加自定义字体和物理按键

    LittlevGL 添加自定义字体获取字库 ttf 文件可以从一些网站上获取字库文件,比如请注意字体许可证 生成源文件使用 LittlevGL 提供的字库文件转换工具,将 ttf 字库文件转换为源文件 ...

  7. OpenCV开发笔记(七十二):红胖子8分钟带你使用opencv+dnn+tensorFlow识别物体

    前言   级联分类器的效果并不是很好,准确度相对深度学习较低,本章使用opencv通过tensorflow深度学习,检测已有模型的分类.   Demo       可以猜测,1其实是人,18序号类是狗 ...

  8. OpenCV开发笔记(六十五):红胖子8分钟带你深入了解ORB特征点(图文并茂+浅显易懂+程序源码)

    若该文为原创文章,未经允许不得转载原博主博客地址:https://blog.csdn.net/qq21497936原博主博客导航:https://blog.csdn.net/qq21497936/ar ...

  9. OpenCV开发笔记(六十九):红胖子8分钟带你使用传统方法识别已知物体(图文并茂+浅显易懂+程序源码)

    若该文为原创文章,未经允许不得转载原博主博客地址:https://blog.csdn.net/qq21497936原博主博客导航:https://blog.csdn.net/qq21497936/ar ...

  10. OpenCV开发笔记(五十五):红胖子8分钟带你深入了解Haar、LBP特征以及级联分类器识别过程(图文并茂+浅显易懂+程序源码)

    若该文为原创文章,未经允许不得转载原博主博客地址:https://blog.csdn.net/qq21497936原博主博客导航:https://blog.csdn.net/qq21497936/ar ...

随机推荐

  1. 初探 Rust 语言与环境搭建

    1. Rust 简介 Rust 的历史 起源:Rust 语言最初由 Mozilla 研究员 Graydon Hoare 于 2006 年开始设计,并于 2009 年首次公开. 开发:Rust 是 Mo ...

  2. wiz 为知笔记服务器 docker 迁移爬坑指北

    本文主要是介绍 wiz 为知笔记服务器 docker 从旧服务器迁移到新服务器的步骤以及问题排查. 旧服务器升级 wiz docker 目的:保持和新服务器拉取的镜像版本一致. 官方只留了 wiz d ...

  3. Catlan--卡特兰数--组合数学

    卡特兰数 \(Catlan\) ·赘述 其实发现卡特兰数和之前不同的是,前面的是给你公式,让你去求具体的例子,然而卡特兰数这里是给你大量例子来给你证明和解释什么是卡特兰数. ·定义 对于卡特兰数来说, ...

  4. Spring:基于注解管理bean

    标记与扫描 注解 和 XML 配置文件一样,注解本身并不能执行,注解本身仅仅只是做一个标记,具体的功能是框架检测 到注解标记的位置,然后针对这个位置按照注解标记的功能来执行具体操作. 本质上:所有一切 ...

  5. spm 一阶分析的Microtime onset应该如何填写?

    1. 如果对数据进行了slice timing, 那么在进行一阶分析时应该修改microtime onset和 microtime resolution这两个参数, 假设数据的slice order= ...

  6. ICMAN液位检测方案

    TA是什么? ICMAN液位检测是基于双通道比较电容式液位检测原理,来判断容器中是否有液体或者液体是否达到一定高度. 有什么用? ICMAN液位检测可以实现非接触式检测,起到高低.不同液位提醒.缺水提 ...

  7. C# .net 6 Log4net 安装、配置 以及相关问题处理

    安装log4net 首先下载相关Nuget包 安装的时候注意项目的.net版本,我这里使用的是.net 6 在根目录下面新建一个配置文件 log4net.config,并且添加下面相关配置项目 < ...

  8. Node.js开发博客项目笔记-初始化路由(博客列表、新增、更新、删除、详情、登录)(3)

    工程目录 如图所示,工程目录如下: 我们在工程下新建src的目录,src目录下新建三个文件夹: controller:交互数据放到该目录下: model:model类放到该目录下: router:路由 ...

  9. TypeScript 高级教程 – 把 TypeScript 当强类型语言使用 (第一篇)

    前言 原本是想照着 TypeScript 官网 handbook 写个教程的. 但提不起那个劲... 所以呢, 还是用我自己的方式写个复习和进阶笔记就好了呗. 以前写过的 TypeScript 笔记: ...

  10. ASP.NET Core – MVC

    前言 在 ASP.NET Core – MVC vs Razor Page 里有提到 MVC. 它算是 WebAPI 的抽象. 但是通常 MVC 指的是比较传统的 Website, WebAPI 则是 ...