前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【手撕算法】Criminisi图像修复算法

【手撕算法】Criminisi图像修复算法

作者头像
周旋
发布2022-08-07 12:26:16
8300
发布2022-08-07 12:26:16
举报
文章被收录于专栏:行走的机械人行走的机械人

该算法出自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 termdata term,两者相乘即为我们该点像素的填充优先级。

算法具体流程可以描述为:

  1. 读取待修复图片以及其掩膜
  1. 根据掩膜得到待修复区域的边缘轮廓
  2. 计算边缘轮廓填充次序(优先级)
  3. 针对对优先级最高的轮廓点,在原图已知区域寻找最佳匹配的图像块并进行填充
  4. 更新边缘轮廓,若边缘轮廓.size大于0,表示还未填充完毕,则回到步骤2更新轮廓,开启新一轮迭代,直到填充完毕(没有边缘轮廓点)

算法实现

1

首先是读取原图和掩码,在主函数里:

代码语言:javascript
复制
int main()
{
  std::chrono::steady_clock clk;
  auto begin = clk.now();//开始计时

  cv::Mat srcImage = cv::imread("image/image1.jpg");//读取图片
  cv::Mat mask = cv::imread("image/mask1.jpg", 0);//读取掩膜  8位单通道的掩膜
  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);

  //结束计时
  auto time = std::chrono::duration_cast<std::chrono::milliseconds> (clk.now() - begin);
  std::cerr << "time : " << time.count() << std::endl;

  std::cout << "Press 'q' to exit" << std::endl;
  while ('q' != cv::waitKey(1));

  return 0;
}

2

然后就进入了我们主算法程序了:

代码语言:javascript
复制
/*********************************
函数声明: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;
}

3

在主算法中,首先我们要得到待修复区域的轮廓点集,用到函数

代码语言:javascript
复制
/*********************************
函数声明: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循环
          }
        }
      }
    }
  }
}

4

得到边缘轮廓点集后,要计算这些点级的修复优先级次序

代码语言:javascript
复制
/*********************************
函数声明:生成优先级队列 函数
参数: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容器自动有序且不重复,所以可以轻松的得到优先级最高的轮廓点。

5

对优先级最高的轮廓点,在原图已知区域寻找最佳匹配的图像块并进行填充。寻找最佳匹配图像块用到的是opencv自带的模板匹配函数

代码语言:javascript
复制
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具有相同的数据类型和大小
);

6

每填充一个轮廓点,轮廓形状都会改变,所以我们需要更新边缘轮廓。

代码语言:javascript
复制
/*********************************
函数声明:更新轮廓函数
参数: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

今天就到这里啦,明天【PatchMatch图像修复算法】见。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-04-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 周旋机器视觉 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档