处理图像以找到外部轮廓
我有几百张PNG格式的图片,需要生成对应的黑白图像,显示出物体的外轮廓。这些源PNG图片有透明通道,所以图片中“没有物体”的部分是完全透明的。
比较棘手的是,如果物体内部有孔洞,这些孔洞在轮廓中是不能显示出来的。比如,如果源图像是一个甜甜圈,那么对应的轮廓图像应该是一个锯齿状的圆形线条,而中间不会有更小的圆形线条。
这里有一张示例图片,源图和它的轮廓:
有没有什么库或者命令行工具可以做到这一点?理想情况下,希望能在Python中使用。
4 个回答
我找到了一款非常实用的REST API,用于轮廓追踪,比用其他程序简单多了。我在一个Ruby项目中使用了这个API,现在也用CURL来调用,效果很好!
这个网站上有演示和API文档。
我用你们的甜甜圈图片作为测试的源图。唯一的问题是你们的图片没有透明的背景颜色。所以我用Gimp制作了一个有透明背景的版本,原来黑色的地方变成了透明。这样,当你调用API并设置matching_not_color=0时,就可以参考任何不是透明颜色的图像部分。
现在我多次使用这个API进行测试。你可以看到,可以获取到JSON数据(轮廓的多边形线)或者一张带有绘制轮廓的示例图。我用CURL调用API,命令如下:
curl -v -H "Accept: application/json" -X POST -F "png_image=@rRBal.png" -F "match_not_color=0" -F
"options[compress][linear]=true" -F
"options[compress][visvalingam]=true" -o output.png
http://tracecontour.com/outlines_image
我使用了match_not_color=0。这样我就要求考虑任何不是透明背景颜色的像素。结果我得到了这张PNG图片(根据curl命令的说明,文件名是output.png)。
你可以看到,这里匹配的区域是蓝色的。每条外部多边形线是红色的,每条内部多边形线是绿色的。我尝试了visvalingam选项,得到了一个不那么精确的轮廓,但你可以请求最精确的结果。
如果你这样调用,就会得到包含坐标(x,y)的JSON数据。
curl -v -H "Accept: application/json" -X POST -F
"png_image=@rRBal.png" -F "match_not_color=0" -F
"options[compress][linear]=true" -F
"options[compress][visvalingam]=true" http://tracecontour.com/outlines
&> /dev/stdout
这里是你能得到的最精确的结果。命令如下(没有使用visvalingam选项):
curl -v -H "Accept: application/json" -X POST -F
"png_image=@rRBal.png" -F "match_not_color=0" -F
"options[compress][linear]=true" -o output.png
http://tracecontour.com/outlines_image
结果在这里,非常精确,而且获取速度很快。
这个API非常有用!
OpenCV有一个叫做 findContours 的功能,正好可以满足你的需求。你需要把轮廓提取模式设置为 CV_RETR_EXTERNAL
。要加载你的图片,可以使用 imread 这个功能。
我推荐使用ImageMagick,这个工具可以在这里免费下载。它在很多Linux系统中都自带。它还支持Python、Perl、PHP和C/C++等多种编程语言。
我下面是通过命令行来使用它的。
convert donut.png -channel A -morphology EdgeOut Diamond +channel -fx 'a' -negate output.jpg
简单来说,-channel A
这个命令是用来选择透明度(alpha通道),然后通过形态学处理来提取不透明区域的轮廓。接着,+channel
告诉ImageMagick我现在要重新处理所有的通道。-fx
是一个自定义函数(操作符),我在这里设置输出图像的每个像素值为a
,也就是修改后的alpha通道中的透明度值。
编辑过的内容
接下来这个方法可能比上面的fx
操作符更快:
convert donut.png -channel RGBA -separate -delete 0-2 -morphology EdgeOut Diamond -negate output.png
结果:
如果你有几百张(甚至上千张)图片需要处理,我建议你使用GNU Parallel,这个工具可以在这里找到。它可以利用你所有的CPU核心来快速完成任务。你的命令大概会是这样的——但请先备份文件,并在你熟悉之前在副本上操作!
parallel convert {} -channel A -morphology EdgeOut Diamond +channel -fx 'a' -negate {.}.jpg ::: *.png
这个命令的意思是使用:::
后面的所有内容作为要处理的文件。然后,它会并行处理,利用所有可用的核心,把每个PNG文件转换成对应的JPEG文件,并改名为输出文件名。
我同意amgaera的看法。如果你想找出轮廓,Python中的OpenCV是一个非常好的工具。就像他/她的帖子中提到的,使用findContours
方法,并使用RETR_EXTERNAL
标志来获取形状的最外轮廓。这里有一些可以复现的代码来说明这一点。你首先需要安装OpenCV和NumPy
才能开始。
我不确定你使用的是哪个平台,但:
- 如果你使用的是Linux,只需在终端输入
apt-get
安装libopencv-dev
和python-numpy
(即sudo apt-get install libopencv-dev python-numpy
)。 - 如果你使用的是Mac OS,先安装Homebrew,然后通过
brew install opencv
和brew install numpy
来安装。 - 如果你使用的是Windows,最好的方法是通过Christoph Gohlke的非官方Python包来安装:
http://www.lfd.uci.edu/~gohlke/pythonlibs/
- 检查OpenCV包并安装它要求的所有依赖项,包括NumPy
,你可以在这个页面找到。
无论如何,我拿了你的甜甜圈图片,并提取了只有甜甜圈的部分。换句话说,我创建了这个图片:
至于你的图片是PNG格式并且有透明通道,这其实没关系。只要这张图片里只有一个物体,我们实际上根本不需要访问透明通道。一旦你下载了这张图片,保存为donut.png
,然后运行这段代码:
import cv2 # Import OpenCV
import numpy as np # Import NumPy
# Read in the image as grayscale - Note the 0 flag
im = cv2.imread('donut.png', 0)
# Run findContours - Note the RETR_EXTERNAL flag
# Also, we want to find the best contour possible with CHAIN_APPROX_NONE
contours, hierarchy = cv2.findContours(im.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
# Create an output of all zeroes that has the same shape as the input
# image
out = np.zeros_like(im)
# On this output, draw all of the contours that we have detected
# in white, and set the thickness to be 3 pixels
cv2.drawContours(out, contours, -1, 255, 3)
# Spawn new windows that shows us the donut
# (in grayscale) and the detected contour
cv2.imshow('Donut', im)
cv2.imshow('Output Contour', out)
# Wait indefinitely until you push a key. Once you do, close the windows
cv2.waitKey(0)
cv2.destroyAllWindows()
让我们慢慢看这段代码。首先我们导入OpenCV和NumPy包。我把NumPy导入为np
,如果你到处查找numpy
的文档和教程,都会这样做,以减少输入。OpenCV和NumPy是可以一起工作的,这就是为什么你需要安装这两个包。接着我们使用imread
读取图片。我把标志设置为0
,这样可以把图片转换为灰度图,简单明了。一旦加载了图片,我就运行findContours
,这个函数的输出是一个包含两个部分的元组:
contours
- 这是一个数组结构,给出了你图片中每个检测到的轮廓的(x,y)
坐标。hierarchy
- 这包含了你检测到的轮廓的额外信息,比如拓扑结构,但为了本帖我们可以跳过这部分。
请注意,我指定了RETR_EXTERNAL
来检测物体的最外轮廓。我还指定了CHAIN_APPROX_NONE
标志,以确保我们得到完整的轮廓,而不是近似值。一旦检测到轮廓,我们创建一张完全黑色的新输出图片。这张图片将包含我们检测到的甜甜圈的外轮廓。一旦创建了这张图片,我们就运行drawContours
方法。你需要指定要显示轮廓的图片、之前创建的轮廓结构,以及-1
标志表示要绘制图片中的所有轮廓。如果一切顺利,你应该只会检测到一个轮廓。然后你可以指定轮廓的颜色。在我们的例子中,我们希望它是白色的。接着,你还要指定轮廓的线条粗细。我选择了3个像素的粗细。
最后,我们要做的就是展示结果。我调用imshow
来显示原始的甜甜圈图片(灰度图)和输出的轮廓图。imshow
并不是故事的结束。你不会看到任何输出,直到你调用cv2.waitKey(0)
。这意味着你可以无限期地显示这些图片,直到你按下一个键。一旦你按下一个键,cv2.destroyAllWindows()
会关闭所有打开的窗口。
这是我得到的结果(当你把窗口调整到并排显示时):
作为额外的奖励,如果你想保存这张图片,只需运行imwrite
来保存图片。你需要指定你想保存的图片名称,以及你正在访问的变量。因此,你可以这样做:
cv2.imwrite('contour.png', out)
这样你就会把这张轮廓图片保存为contour.png
。
这应该足够让你入门了。
祝你好运!