该算法出自Criminisi的论文
Region Filling and Object Removal by Exemplar-Based Image Inpainting
该算法只要思路是利用图片的已知区域对待修复区域进行填充。而填充的顺序是根据计算优先级确定的,填充的基本单位是自定义大小的像素块。
先来看一下论文中比较重要的两个图片,
图一介绍了填充的基本原理:
将图像分为已知区域(source region)和待填充或移除区域(target region),填充从target region的边界开始,以边界点p为中心,设置块的大小,形成像素块(图b),然后在已知区域中根据匹配准则找到相似的块,如图c以q’及q"为中心的两个块,最后选取最佳匹配的块进行填充(图d)。
图二介绍了边缘轮廓填充优先级的计算准则:
红色箭头为轮廓的法线法向(垂直于轮廓切线),蓝色为点p像素梯度方向旋转90°。
根据这两个量我们可以计算两个值:
分别是confidence term和data term,两者相乘即为我们该点像素的填充优先级。
算法集体流程可以描述为:
-
读取待修复图片以及其掩膜
-
根据掩膜得到待修复区域的边缘轮廓
-
计算边缘轮廓填充次序(优先级)
-
针对对优先级最高的轮廓点,在原图已知区域寻找最佳匹配的图像块并进行填充
-
更新边缘轮廓,若边缘轮廓.size大于0,表示还未填充完毕,则回到步骤2更新轮廓,开启新一轮迭代,直到填充完毕(没有边缘轮廓点)
算法实现
首先是读取原图和掩码,在主函数里:
int main()
{
cv::Mat srcImage = cv::imread("image/image1.jpg");//读取图片
cv::Mat mask = cv::imread("image/mask1.jpg", 0);//读取掩膜 8位单通道的掩膜
cv::imshow("mask0", mask);
if (srcImage.empty())
{
printf_s("读取图片失败");
return -1;
}
cv::Mat lab_img;
cv::cvtColor(srcImage, lab_img, COLOR_BGR2Lab);//转换颜色空间为LAB
Criminisi criminisi(lab_img);
criminisi.mask(mask);//赋值给Criminisi类的_mask变量
const auto& res_lab = criminisi.generate(); //开启criminisi函数迭代
cv::Mat resImage;
cv::cvtColor(res_lab, resImage, COLOR_Lab2BGR);
cv::imshow("src", srcImage);
cv::imshow("mask", mask);
cv::imshow("res", resImage);
waitKey();
return 0;
}
然后就进入了我们主算法程序了:
/*********************************
函数声明:generate函数
参数:NONE
注释:Criminsi算法主程序体
测试:NONE
**********************************/
cv::Mat Criminisi::generate(void)
{
generate_contour();//生成轮廓
//把矩阵_mask中元素不为0的点全部变为value值
_modified = _original.clone().setTo(cv::Vec3b(0, 0, 0), _mask);//得到待修复的原图
generate_priority();//生成优先队列,得到了对应点坐标的优先级
cv::Mat resSSD;//用来存储匹配度的矩阵
cv::Mat pMask;
cv::Mat pInvMask;
cv::Mat templateMask;
cv::Mat mergeArrays[3];
cv::Mat dilatedMask;
while (_pq.size()) {//遍历_pq set容器
const std::pair<int, int>& point = _pq.rbegin()->second;
const cv::Point p(point.first, point.second);//获取待修复轮廓点
const auto& phi_p = patch(p, _modified);//提取一小块待修复区域
const int radius = (phi_p.rows - 1) / 2;//得到修复半径
pMask = patch(p, _mask, radius);//得到p点待修复区域的_mask的掩膜块,待修复区域为1,已知区域为0
pInvMask = ~pMask;//取反
templateMask = (pInvMask);
//单通道掩膜块合并成三通道掩膜块
for (int i = 0; i < 3; ++i)
mergeArrays[i] = templateMask;
cv::merge(mergeArrays, 3, templateMask);//合并,得到三通道的样本掩膜块
cv::matchTemplate(_modified, phi_p, resSSD, cv::TM_SQDIFF, templateMask); //在_modified中搜索与模板phi_p的templateMask区域相匹配的块并得到相似度矩阵resSSD
cv::dilate(_mask, dilatedMask, cv::Mat(), cv::Point(-1, -1), radius);//将_mask膨胀为dilatedMask
std::cerr << "Points in contour : " << _pq.size() << std::endl;
//将待修复区域对应的resSSD区域设为匹配值最大,排除其干扰
resSSD.setTo(std::numeric_limits<float>::max(), //将resSSD的所有或部分数组元素设置为指定的值max()
dilatedMask(cv::Range(radius, _rows - radius),
cv::Range(radius, _cols - radius)));
cv::Point q;
cv::minMaxLoc(resSSD, NULL, NULL, &q);//resSSD的最小值位置,即最佳匹配位置
q = q + cv::Point(radius, radius);//得到该位置定位点
const auto& phi_q = patch(q, _modified, radius);//提取出最佳匹配块
phi_q.copyTo(phi_p, pMask);//复制填充
cv::Mat confPatch = patch(p, _confidence);
const double confidence = cv::sum(confPatch)[0] /confPatch.total();
confPatch.setTo(confidence, pMask);
pMask.setTo(0);
update_contour(p);//修复p点后,更新边界信息
}
std::cerr << "Completed" << std::endl;
return _modified;
}
在主算法中,首先我们要得到待修复区域的轮廓点集,用到函数
/*********************************
函数声明:generate_contour生成轮廓函数
参数:NONE
注释:遍历整幅图像得到待修复边界
测试:NONE
**********************************/
void Criminisi::generate_contour(void)
{
constexpr int NUM_N = 8;
const int dx8[NUM_N] = { -1, -1, 0, 1, 1, 1, 0, -1 };//用来访问八邻域的数组
const int dy8[NUM_N] = { 0, -1, -1, -1, 0, 1, 1, 1 };
_contour.clear();//清除变量_contour
for (int i = 0; i < _cols; ++i) {
for (int j = 0; j < _rows; ++j) {//遍历图像
for (int k = 0; k < NUM_N; ++k) { //遍历八邻域
if (!_mask.at<uchar>(j, i))//如果当前像素不属于_mask,即不属于待修复区域,则退出
continue;
const int x = i + dx8[k];
const int y = j + dy8[k];
if (x >= 0 && x < _cols && y >= 0 && y < _rows) {//八邻域没有出界
if (!_mask.at<uchar>(y, x)) {//八邻域像素不属于_mask,即代表该点属于边缘
_contour.emplace(i, j);//将pair(i,j)存到set:_contour中
break;//只需有一个就可以退出该for循环
}
}
}
}
}
}
得到边缘轮廓点集后,要计算这些点级的修复优先级次序
/*********************************
函数声明:生成优先级队列 函数
参数:NONE
注释:遍历所有点,根据轮廓点坐标计算其优先级
测试:NONE
**********************************/
void Criminisi::generate_priority(void)
{
_confidence = cv::Mat::zeros(_rows, _cols, CV_64FC1); //置信 矩阵,单通道全为零的矩阵,代表了当前位置点被修复的优先级
for (int i = 0; i < _cols; ++i)
for (int j = 0; j < _rows; ++j)//遍历置信矩阵
_confidence.at<double>(j, i) = !_mask.at<uchar>(j, i);//_mask为0的点置为1,_mask为1的点置为0
_pq.clear(); //存储优先级与轮廓点
_map.clear();//轮廓点到优先级的映射
for (const auto& c : _contour) {
//根据点位置c计算优先级pri
const double pri = priority(c);//c为轮廓点set集 pri为该轮廓点的优先级priority
_pq.emplace(pri, c);//建立轮廓点与优先级的对应关系
_map[c] = pri;
}
}
所有点的优先级会存储到set容器内,set容器自动有序且不重复,所以可以轻松的得到优先级最高的轮廓点。
对优先级最高的轮廓点,在原图已知区域寻找最佳匹配的图像块并进行填充。寻找最佳匹配图像块用到的是opencv自带的模板匹配函数
void cv::matchTemplate(
cv::InputArray image, // 用于搜索的输入图像, 8U 或 32F, 大小 W-H
cv::InputArray templ, // 用于匹配的模板,和image类型相同, 大小 w-h
cv::OutputArray result, // 匹配结果图像, 类型 32F, 大小 (W-w+1)-(H-h+1)
int method // 用于比较的方法
InputArray mask //搜索模板templ的掩码。它必须与templ具有相同的数据类型和大小
);
每填充一个轮廓点,轮廓形状都会改变,所以我们需要更新边缘轮廓。
/*********************************
函数声明:更新轮廓函数
参数:NONE
注释:NONE
测试:NONE
**********************************/
void Criminisi::update_contour(const cv::Point& p)
{
constexpr int NUM_N = 8;
const int dx8[NUM_N] = { -1, -1, 0, 1, 1, 1, 0, -1 };//用来访问八邻域的数组
const int dy8[NUM_N] = { 0, -1, -1, -1, 0, 1, 1, 1 };
const int x_begin = std::max(p.x - 2 * _radius, 0);
const int y_begin = std::max(p.y - 2 * _radius, 0);
const int x_end = std::min(p.x + 2 * _radius, _cols - 1);
const int y_end = std::min(p.y + 2 * _radius, _rows - 1);
for (int i = x_begin; i <= x_end; ++i) {
for (int j = y_begin; j <= y_end; ++j) {
const std::pair<int, int> p = std::make_pair(i, j);
if (_contour.count(p)) {
const double pri = _map[p];
_contour.erase(p);
_pq.erase(std::make_pair(pri, p));
_map.erase(p);
}
}
}
std::set<std::pair<int, int>> new_contour;
for (int i = x_begin; i <= x_end; ++i) {
for (int j = y_begin; j <= y_end; ++j) {
for (int k = 0; k < NUM_N; ++k) {
if (!_mask.at<uchar>(j, i))
continue;
const int x = i + dx8[k];
const int y = j + dy8[k];
if (x >= 0 && x < _cols && y >= 0 && y < _rows) {
if (!_mask.at<uchar>(y, x)) {
new_contour.emplace(i, j);
break;
}
}
}
}
}
for (const auto& nc : new_contour)
_contour.emplace(nc);
for (const auto& nc : new_contour) {
const double pri = priority(nc);
_pq.emplace(std::make_pair(pri, nc));
_map[nc] = pri;
}
}
若边缘轮廓.size大于0,表示还未填充完毕,则回到步骤2更新轮廓,开启新一轮迭代,直到填充完毕(没有边缘轮廓点)。
到此,该算法就运行完毕了。
算法运行效果
这个效果比论文效果差的不是一点半点,但因为我已经忍不住想发文了,代码就没优化,所以这烂图就先发出来吧。
THE END
今天就到这里啦,微信搜索【Opencv视觉实践】,对【计算机视觉/机器视觉算法和软件开发】感兴趣的小伙伴可以一起来学习呀。
关注后 后台回复
【电子书资源】可以领取10G计算机视觉/软件开发相关电子书
【手撕算法代码】可以领取手撕算法系列专栏的所有代码和PDF版论文
【加群】可以加入我们的视觉算法靓仔群~