【问题标题】:Filling gaps in shape edges填充形状边缘的间隙
【发布时间】:2020-08-11 07:30:57
【问题描述】:

是否有一种算法可以很好地填充示例图像上的孔?膨胀效果不好,因为在我最终设法连接这些曲线的末端之前,曲线变得非常粗。我想避免加粗线条。感谢您的帮助。

是的,图像中的任何字母或形状都可以带有类似的孔。

【问题讨论】:

  • 之后有腐蚀的膨胀怎么样。 en.wikipedia.org/wiki/Closing_%28morphology%29.
  • 如果空洞小于对象之间的间隙,您可以首先将段聚类到对象,这将使空洞填充更加容易(例如,分别为每个对象扩张)。否则请阅读“格式塔心理学”
  • 您是否考虑过使用霍夫变换来完成线条的完成? OpenCV 确实将 Hough 变换作为 API 的一部分:docs.opencv.org/doc/tutorials/imgproc/imgtrans/hough_lines/…
  • 虽然不止行。
  • @user107986 - 不,当然不是,但是您可以使用霍夫变换尝试将不连续的区域连接在一起。 FWIW,Mark Setchell 有一个非常好的算法(如下),这或多或少也是我所想到的。

标签: opencv image-processing


【解决方案1】:

另一种更简单的方法,可能会更好地转换为OpenCV,因为它使用卷积而不是顺序 Perl/C 代码。

基本上将所有黑色像素设置为10,将所有白色像素设置为0,然后将图像与以下3x3内核进行卷积:

1  1  1
1  10 1
1  1  1

现在,内核中间的黑色像素将给出 100 (10x10),而附近的任何其他黑色像素将给出 10 (10x1)。因此,如果我们想要一个中心黑色像素只有一个相邻黑色像素的点,它将具有 110 (100+10) 的值。因此,让我们将所有值为 110 的像素用红色着色。这给出了这个命令:

convert EsmKh.png -colorspace gray -fill gray\(10\) -opaque black -fill gray\(0\) -opaque white -morphology convolve '3x3: 1,1,1 1,10,1 1,1,1' -fill red -opaque gray\(110\) out.png

使用生成的图像(您可能需要放大间隙才能看到红色):

如果您想要红色像素的列表,请将输出文件名替换为 txt: 并像这样搜索:

convert EsmKh.png -colorspace gray -fill rgb\(10,10,10\) -opaque black -fill rgb\(0,0,0\) -opaque white -morphology convolve '3x3: 1,1,1 1,10,1 1,1,1' txt: | grep "110,110,110"

给出:

86,55: (110,110,110)  #6E6E6E  grey43
459,55: (110,110,110)  #6E6E6E  grey43
83,56: (110,110,110)  #6E6E6E  grey43
507,59: (110,110,110)  #6E6E6E  grey43
451,64: (110,110,110)  #6E6E6E  grey43
82,65: (110,110,110)  #6E6E6E  grey43
134,68: (110,110,110)  #6E6E6E  grey43
519,75: (110,110,110)  #6E6E6E  grey43
245,81: (110,110,110)  #6E6E6E  grey43
80,83: (110,110,110)  #6E6E6E  grey43
246,83: (110,110,110)  #6E6E6E  grey43
269,84: (110,110,110)  #6E6E6E  grey43
288,85: (110,110,110)  #6E6E6E  grey43
315,87: (110,110,110)  #6E6E6E  grey43
325,87: (110,110,110)  #6E6E6E  grey43
422,104: (110,110,110)  #6E6E6E  grey43
131,116: (110,110,110)  #6E6E6E  grey43
524,116: (110,110,110)  #6E6E6E  grey43
514,117: (110,110,110)  #6E6E6E  grey43
122,118: (110,110,110)  #6E6E6E  grey43
245,122: (110,110,110)  #6E6E6E  grey43
76,125: (110,110,110)  #6E6E6E  grey43
456,128: (110,110,110)  #6E6E6E  grey43
447,129: (110,110,110)  #6E6E6E  grey43
245,131: (110,110,110)  #6E6E6E  grey43
355,135: (110,110,110)  #6E6E6E  grey43
80,146: (110,110,110)  #6E6E6E  grey43
139,151: (110,110,110)  #6E6E6E  grey43
80,156: (110,110,110)  #6E6E6E  grey43
354,157: (110,110,110)  #6E6E6E  grey43
144,160: (110,110,110)  #6E6E6E  grey43
245,173: (110,110,110)  #6E6E6E  grey43
246,183: (110,110,110)  #6E6E6E  grey43
76,191: (110,110,110)  #6E6E6E  grey43
82,197: (110,110,110)  #6E6E6E  grey43
126,200: (110,110,110)  #6E6E6E  grey43
117,201: (110,110,110)  #6E6E6E  grey43
245,204: (110,110,110)  #6E6E6E  grey43
248,206: (110,110,110)  #6E6E6E  grey43
297,209: (110,110,110)  #6E6E6E  grey43
309,210: (110,110,110)  #6E6E6E  grey43

现在您可以处理红点列表,并为每个红点找到最近的另一个红点,并用一条直线将它们连接起来 - 如果您真的很感兴趣,也可以进行一些曲线拟合。当然,可能还有一些细化需要做,你不妨设置一个填缝线的最大长度。

【讨论】:

  • 您的解决方案的问题在于它检测到字母“B”中的松散末端,我们不需要为其找到一对。
  • 这就是我在回答结尾处的微调建议的想法。
【解决方案2】:

我对此进行了一些尝试。它可能需要一些调整,但这是一个想法。我的算法如下:

找到恰好有 1 个黑色相邻像素的所有黑色像素,将其涂成红色并将其放入 pixels at ends 的列表中。

遍历所有红色像素的列表,找到最近的其他红色像素并在两者之间画一条直线。

顺便说一句,我只实现了第一部分 - 必须为读者留下一些东西;-)

#!/usr/bin/perl
use strict;
use warnings;
use Image::Magick;
use Data::Dumper;

my $im=Image::Magick->new();
$im->Read('EsmKh.png');

my ($width,$height)=$im->Get('width','height');
my $out=Image::Magick->new();
$out->Read('EsmKh.png');

my @pixels;
# Iterate over pixels
for my $y (0..($height-1)){
   for my $x (0..($width-1)){
      my (@pixel) = split(/,/, $im->Get("pixel[$x,$y]"));
      $pixels[$x][$y]=$pixel[0];
   }
}

# Find black pixels that have precisely 1 black neighbour
for my $y (1..($height-2)){
   for my $x (1..($width-2)){
      next if $pixels[$x][$y]!=0;
      my $neighbours=0;
      for(my $i=$x-1;$i<=$x+1;$i++){
         for(my $j=$y-1;$j<=$y+1;$j++){
            $neighbours++ if $pixels[$i][$j]==0;
         }
      }
      $neighbours--;    # Uncount ourself !
      if($neighbours==1){
         $out->Set("pixel[$x,$y]"=>'red');
      }
   }
}
$out->Write(filename=>'out.png');

结果

您必须放大才能看到红色像素...

放大的图像

【讨论】:

    【解决方案3】:

    获得加厚图像后,您可以使用skeletonization 恢复“瘦”形状。我找到了骨架化的实现here

    如果你想避免过度加厚(因为它会扭曲图像并且形状的一部分会合并在一起),请交替使用轻度侵蚀和骨架化,直到填充孔。

    【讨论】:

      【解决方案4】:

      这是 Mark Setchell 算法的 OpenCVC++ 实现。它非常简单,使用相同的内核并通过cv::filter2D 函数对输入图像进行卷积。我可以选择反转输入图像,使目标像素的值为255

      //Read input Image
      cv::Mat inputImage = cv::imread( "C://opencvImages//blobs.png", cv::IMREAD_GRAYSCALE );
      
      //Invert the image:
      inputImage = 255 - inputImage;
      
      //Threshold the image so that white pixels get a value of 0 and
      //black pixels a value of 10:
      cv::threshold( inputImage, inputImage, 128, 10, cv::THRESH_BINARY );
      

      现在,设置内核并对图像进行卷积,如下所示:

      //Set up the end-point kernel:
      cv::Mat kernel = ( cv::Mat_<int>(3, 3) <<
        1, 1, 1,
        1, 10, 1,
        1, 1, 1
      );
      
      //Convolute image with kernel:
      cv::filter2D( inputImage, inputImage, -1 , kernel, cv::Point( -1, -1 ), 0, cv::BORDER_DEFAULT );
      

      卷积的直接结果是这样的,端点处的像素现在的值为110,在这个输出中可以(几乎)看到:

      让我们对这些像素进行阈值化并将它们叠加在原始图像上。这是结果(红色像素):

      此外,图像的skeleton 可以在开始时计算。骨架的归一化线宽为1 pixel。该函数是OpenCV的Extended Image Processing module的一部分:

      #include <opencv2/ximgproc.hpp>
      
      //Compute the skeleton of the input:
      cv::Mat skel;
      int algorithmType = 1;
      cv::ximgproc::thinning( inputImage, skel, algorithmType ); 
      

      【讨论】: