作者:翟天保Steven
版权声明:著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处
一、关于图像修补
图像修补的目的是基于已有的图像信息或数据库内信息,对缺失区域进行合理地修复。在诸多领域如电影、摄像、医疗等行业,有广泛的应用。
传统上,图像修补由专业的修复师进行,修复师凭借自身丰富的工作经验和生活阅历,不仅能基于客观信息对图像缺失进行填充,更能进行主观创作,使得二次修复的图片更加生动形象。
但在日益智能化的今天,针对数字图像的修补工作逐渐由人工转向了自动化,这不仅节省了大量人工成本,而且计算机凭借优越的算法和庞大的数据库,对图像的修复效果更高效且优质。通过近几个月风风火火的chatGPT,相信大家也看到了AI的魅力,这是未来的大趋势不可逆。
针对图像修补,本文提出了一种基于等照度线和窗口匹配的图像修补算法,接下来将简单介绍下算法原理和流程,并展示相关的效果图。
二、算法原理和流程
对图像缺失部分进行填充,首先要确定填充哪些内容,谁优先填充谁最后填充;其次要判断用什么数据来填充,能使得填充后的结果更贴近真实,显得不违和。做到了这两点,图像修补工作基本就完成了。
2.1 优先级计算
我们先来讨论填充的优先级,如下图所示,图像白色部分是手动绘制的掩膜区域,该区域的真实信息被擦除了,我们现在要对其进行复原。
在图中红线部分就是等照线,该线的两侧往往有较大的数值差异,因此它也与黑线所示的梯度线呈垂直关系;当等照线与掩膜边界呈垂直时,此时等照线上的像素点特征是非常强烈且明显的,通俗的讲,被填充的点极大可能和它那条等照线上的点类似,沿着这个等照线绘制下去,就可以了。
那我们怎么判断当前点是不是处于与掩膜边界垂直的等照线上呢?可以通过掩膜边界(黑线)的法线和红色等照度线这两个向量判断。等照线与掩膜边界垂直时,那与掩膜边界的法线自然平行,此时两个向量点乘可以使得值最大,因此该值可以作为填补优先级评判的指标,定义为D(x,y)。
除此之外,我们还需要用到一个指标,叫可信度,定义为C(x,y),就是被填补像素所在窗口内,源数据的占比,加入窗口为7*7的尺寸,里面有31个源数据,18个代填补数据,那它的可信度也就是31/49。可信度越高的像素,说明窗口内缺失的数据越少,对它们填补更容易且贴实。
综上,代填补像素的优先级可以定义为P(x,y)=C(x,y)*D(x,y),当然也可以定义为别的,比如乘法变加法等等,大家可以自行发挥。
2.2 数据填充
确认好代填补像素的优先级后,我们对优先级最高的像素所在窗口进行填充,填充基于窗口匹配实现。
窗口匹配顾名思义就是从全图中寻找一个最像要填补的窗口的源数据窗口,把它粘过去即可。如下图所示,黑色窗口的实心部分是代填补窗口中的源数据,空心部分是代填补的数据。我们将黑色窗口实心部分和红色窗口实心部分进行三通道数据减法,对差值平方和累加取平均,可以得到一个匹配误差matchError,寻找全图最小的误差作为最小误差minError,此时对应的红色窗口就是匹配好的源数据。
但是,当出现两个同样的匹配误差后该怎么取舍呢?这时需要用到第二个匹配的指标——最小窗内方差minVarience。即对源数据中空心部分三通道数值求方差,数值减去平均值后平方和累加。方差低则说明数据平稳,不容易出现异常突兀的噪声数据,这样可以让填补的数据更贴实。
2.3 算法流程图
综上,该算法的流程图可简化为:
三、填补效果图
3.1 干涉条纹图填补视频
该案例特征是图像黑白色系相对稳定,近似区域多,因此修补效果也是最好的。
图像修补算法示例视频1
3.2 地图填补视频
该案例特征是图像分为极大区域,如海洋、雪山、陆地、森林,色系复杂,图像细节多且杂。对其修补效果也是相对好的,因为本身糅杂的颜色系统中适当混入一些不和谐因素,凭借肉眼较难准确识别。
图像修补算法示例视频2
3.3 花卉修补
该案例特征是图像色彩相对单调,花瓣区域纹理明显。这类图像色彩简单又不简单,颜色相近又各有区分,修补难度极大。经过图像修补后,可以发现花的边缘修补较成功,但是肉眼还是能看出内部区间存在一定的修补痕迹。
3.4 房屋修补
该案例特征是房屋颜色与天空接近,此时填补区域如果是屋顶瓦砖,便补的很好,因为纹理缘故,匹配的数据也是瓦砖。
但如果填补区域是屋顶侧面,则易出问题,若天空是蓝色还好,但恰巧天空也是棕黄色系,所以填补痕迹就突出了。
该案例也是很不好找,特地找出来做评估。感兴趣的伙伴可以优化窗口匹配函数,比如全局和局部结合匹配等等。
四、代码分享
main.cpp
#include "Inpaint.h"
// 全局变量
int thickness = 5;
cv::Point sPoint(-1, -1);
cv::Mat image, mask;
// 鼠标事件
static void onMouse(int event, int x, int y, int flags, void*){
if (event == cv::EVENT_LBUTTONUP || !(flags & cv::EVENT_FLAG_LBUTTON))
sPoint = cv::Point(-1, -1);
else if (event == cv::EVENT_LBUTTONDOWN)
sPoint = cv::Point(x, y);
else if( event == cv::EVENT_MOUSEMOVE && (flags & cv::EVENT_FLAG_LBUTTON)){
cv::Point ePoint(x,y);
if( sPoint.x < 0 )
sPoint = ePoint;
cv::line( mask, sPoint, ePoint, cv::Scalar::all(255), thickness);
cv::line( image, sPoint, ePoint, cv::Scalar::all(255), thickness);
sPoint = ePoint;
cv::imshow("image", image);
}
}
// 主函数
int main(){
cv::Mat originalImage = cv::imread("6.jpg", 1);
// 无输入图像
if(!originalImage.data){
cout << "Error unable to open input image" << endl;
return 0;
}
// 拷贝图像
image=originalImage.clone();
mask = cv::Mat::zeros(image.size(), CV_8U);
// 设置鼠标事件
cv::namedWindow("image", 1);
cv::imshow("image", image);
cv::setMouseCallback("image", onMouse, 0);
// 循环处理
while(true){
// 键盘事件
char key = (char)cv::waitKey();
// 按'b'跳出循环,结束程序
if (key == 'b')
break;
// 按'r'恢复原始图像
if (key == 'r'){
mask = cv::Scalar::all(0);
image = originalImage.clone();
cv::imshow("image", image);
}
// 按'空格'执行算法
if (key == ' '){
int r = 3;
InpaintAlgorithm *m_algorithm = new InpaintAlgorithm(image, mask, 2 * r + 1, TEMPLATE_MATCHING);
m_algorithm->executeInpaint();
cv::namedWindow("result");
cv::imshow("result", m_algorithm->m_outputImage);
}
// 按'w'增加画笔厚度
if (key == 'w') {
thickness++;
if (thickness > 20)
thickness = 20;
cout << "Thickness = " << thickness << endl;
}
// 按's'减少画笔厚度
if (key == 's') {
thickness--;
if (thickness < 1)
thickness = 1;
cout << "Thickness = " << thickness << endl;
}
}
return 0;
}
Inpaint.h
#ifndef INPAINT_H
#define INPAINT_H
#include <iostream>
#include "opencv2/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/photo/photo.hpp"
using namespace std;
using namespace cv;
// 填补策略
enum INPAINT_METHOD {
TEMPLATE_MATCHING, // 模板匹配
};
// 错误类型
enum ERROR_TYPE {
ERROR_TYPE_OK, // OK
ERROR_TYPE_INPUT, // 输入图像异常
ERROR_TYPE_MASK, // 掩膜异常
ERROR_TYPE_WINDOWSIZE, // 窗尺寸异常
};
// 定义图像填补接口
class Inpaint
{
public:
// 构造函数
Inpaint(cv::Mat inputImage, cv::Mat mask, int windowSize);
// 检查异常
void checkError();
// 执行
virtual void execute() = 0;
public:
ERROR_TYPE errorType; // 错误码
int m_windowSize = 9; // 窗尺寸
cv::Mat m_inputImage; // 输入图像
cv::Mat m_mask; // 掩膜
cv::Mat m_outputImage; // 输出图像
};
// 实现具体策略-模板匹配
class TemplateMatching :public Inpaint
{
public:
// 构造函数
TemplateMatching(cv::Mat inputImage, cv::Mat mask, int windowSize);
// 执行
virtual void execute();
private:
// 初始化
void init();
// 检查是否存在未填补信息
bool checkUnfilled();
// 寻找目标位置集合
void findTargetPoints();
// 计算可信度
void calcConfidence();
// 获取优先级
void getPriority();
// 寻找最佳匹配
void findBestMatch();
// 更新图像
void updateImage();
private:
int targetIndex; // 目标点序号
cv::Point2i m_bestMatchUL; // 最佳匹配点左上角位置
cv::Mat m_updatedImage; // 更新中的图像
cv::Mat m_updatedMask; // 更新中的掩膜
cv::Mat m_confidence; // 可信度
cv::Mat m_oriSourceRegion; // 原始源位置
cv::Mat m_sourceRegion; // 源位置
cv::Mat m_targetRegion; // 目标位置
cv::Mat m_gradientX; // 梯度X
cv::Mat m_gradientY; // 梯度Y
vector<cv::Point2i> targetPoints; // 目标位置点集合
vector<pair<float, float>> normals; // 法线集合
};
// 应用类-图像修补算法调用
class InpaintAlgorithm
{
public:
// 构造函数
InpaintAlgorithm(cv::Mat inputImage, cv::Mat mask, int windowSize, INPAINT_METHOD method);
// 析构函数
~InpaintAlgorithm();
// 设置修补策略
void setInpaintAlgorithm(INPAINT_METHOD method);
// 执行图像修补
void executeInpaint();
public:
int m_windowSize; // 窗尺寸
cv::Mat m_inputImage; // 输入图像
cv::Mat m_mask; // 掩膜
cv::Mat m_outputImage; // 输出图像
private:
Inpaint* m_inpaint; // 图像填补类实例
};
#endif
C++完整代码不免费分享,有意获取者可以私我。算法不是魔法,不能解决一切问题。该算法的核心逻辑可用于工程开发,但仍有许多需要结合实际完善的地方,不建议直接拷贝使用。
注意:当缺失的面积过大或者没有近似的窗口源数据时,填补效果会相对失真,这也是合理的。