数字图像处理 | 一个简易提取圆形的工具
背景
最近这段时间,我在我的博客的友情链接功能中完成了新的特性。就像下面这幅图一样,可以在卡片上显示院校校徽。


不过这其中有一个问题是,这些院校的校徽素材网上并没有现成的,所以还得我自己来进行处理。
于是乎我打开了图像处理软件 Photoshop 来处理这些图片素材。
Photoshop 处理流程(以厦门大学为例)
新建图片

新建一张大小为300x300的条件,这个大小是我根据自己的需要设定为标准的。
网上收集校徽素材

打开百度搜索合适的校徽素材,一般要求就是清晰,背景简单不复杂。

然后直接使用截图工具截图,这里为了后续自由变换比较方便,截图时直接截取正方形大小的区域。
调整大小和位置

粘贴至Photoshop中后,用自由变换调整大小,使得校徽刚好内切于这个正方形。就像下面这张图所呈现的一样。

把背景变透明
直接使用魔棒工具选中周围的白色区域,然后删除它们。

最终效果
最终效果如图所示。

虽然有PS的帮助,这项工作变得没有那么麻烦,可要处理数量如此繁多的图像,一张一张来还是有些麻烦。于是我开始思考,这个过程能不能用程序来辅助呢?遂进入这篇文章的正题:一个简单的图像处理程序。

前言
在做这个计划之前,我作了一个思考。我说要做一个处理这个流程的图像处理程序,其实Photoshop就是一个图像处理程序。但是前者是解决特定某个问题的,而后者是泛用型的(而且为了方便地泛用做了GUI界面)。
另一个问题是,学校里并没有教授图像处理相关的课程,调用库函数之类的活儿几乎也是人人都能干。那么科班生和非科班生的区别在哪里呢?
我想,图像处理的本质仍然是数字处理。虽然库函数里读入图像、写到图像就像一个黑盒一样,使用者不必关心其内部如何,但是我大概能摸索出它的内部实现或许是根据文件的编码格式来读取二进制数据放到内存的不同数据结构中(如下图是BMP图像的文件格式,详情可以查看这篇文章);

再例如resize函数,看似简单的调整大小,它的内部实现应该是原图像和新图像之间有某种函数映射,如果是放大图像,新图像像素数量比原图像像素数量多,所以新图像中部分像素肯定是需要通过原图像像素用某种算法来得出。这些思维是我学习学校课程得到的。

所以同样是放大5倍,画图工具和贴图工具产生的最终结果不一样。因为可以选择的插值算法有很多种。如下图所示是Photoshop中的插值算法。

综上所述,即使学校没有直接教学怎么写图像处理的代码,也教会了我们“数据与计算”的思想,有了这个思想再去学习很多东西是很有助力的。
任务规划
通过之前使用Photoshop对图像进行处理,可以把这个任务归结为以下几个步骤:
a->b
b->c
c->d
d->e
}
环境配置
- 使用Python 3.8.12
- 使用
matplotlib、skimage和numpy
1 | import matplotlib.pyplot as plt |
输入图片示例

均为网络收集(好像混入了什么奇怪的东西)。
背景均为简单的白色,图片上没有其他复杂图形,只有一个圆形图形。
接下来这个工具只能处理圆形校徽,暂且不支持异形(如南京大学、华中科技大学等)。好在大部分高校的校徽都是圆形的。
输出图片示例

文件名字是我后来自己修改的。
实现
输入图片
首先先设定三个基础的变量,然后用matplotlib.pyplot里的imread函数读入这个文件。
1 | input="./inputs/" |
定位位置
基本方法
如果想把图片裁切成内切,那么得想办法先定位圆的位置。这里我选择用一种简单粗暴的方法定位圆形,那就是逐行扫描。
由于输入图像的背景都是白色的,而校徽基本上都是非白色的,因此——
- 从头扫描第行,逐个判断像素点是否为白色,如果是,结束扫描,记录这个像素点的为,然后
- 从尾扫描第行,逐个判断像素点是否为白色,如果是,结束扫描,记录这个像素点的为,再扫描行
- 扫描完所有行后,可以得到这过程中最大的值以及取得该值的。
那么就获得了这个圆的圆心为,半径为。有了圆心和半径,自然就可以确定这个圆的位置。
用下面的方法输出处理的结果:
1 | plt.plot([ansl,ansr],[ansx,ansx],"r--o") #画出直径 |

为了可视化上面那个过程,我把程序判断的白色像素点画成了黑色,而非白色像素点保持不变。
发现程序并没有很好地完成这个轮廓的识别。虽然在上图直径似乎是判断准确了,但是这是一个巧合:在轮廓判断不对的情况下,大部分输入图像的直径都是很难判断准确的。这是为什么呢?
这是因为周边那些像素点其实都不是真正的“白色”。如下图所示,这个点虽然看上去是白色,但不是的白色,而程序在判断该像素点是否为白色像素点时,又是直接比对的RGB值。那么有什么好的解决方法吗?我选择引入颜色距离。(我不知道是否还有更好的方法,在这里我姑且这样做了)

颜色距离
首先我在不少课程里面都接触过向量距离这个概念,描述向量距离可以用欧氏距离、曼哈顿距离、切比雪夫距离、马氏距离、余弦距离等等。
而RGB值同样也可以视为是一个向量,在本次应用中,我选择简单的欧氏距离,代码实现如下:
1 | def diff(a,b): |
那么diff(rgb,[255,255,255])结果反馈了某个颜色值与白色的相近程度。接下来就能用一个不等式来判断这个是否为“白色”。
即:if diff(rgb,[255,255,255]) < threshold

如图所示,随着阈值threshold升高,轮廓获取得也就越来越准确。

最后标注的直径也是准确的。
又一个新问题
然而,这在我测试下一张输入图片时,又发现了新的问题。

可以看到这个直径标注得或许有点偏了。这又是为什么呢?
经过分析:

由于这些正切位置的像素在水平方向上变化较小,有些图像会处理成一条“直线”。(因为这是位图而非矢量图)
因此这个地方需要一些修正。我的修正方法是,将同样达到当前最大值的位置加入到列表中。同时当产生新的最大值时,列表要清空。
最后在算法结束时,通过计算列表的平均值来获得。即ansx=int(np.average(anslist))。

可以看到,经过“取平均”方法修正后,可以取得正常的直径了。
至此,轮廓和直径都能正常的判断了,即圆心和半径都能正常判断了。得到了。
把周围白色的变成透明的
把图像某个地方变成透明的,首先要解决的问题是,一个像素点仅有RGB值,是没有办法表达“透明“的信息的。
因此需要引入新的通道,即把RGB变成RGBA,才能表达“透明”的信息。A就是Alpha,不透明度。
我的输入图像为RGB 3通道图像,通过下面这个方法变为RGBA 4通道图像。
1 | def rgb2rgba(img): |
变为四通道后,只要让RGBA的第四维,变为0,就是完全透明了。
而我在上一小节已经把图像的白色背景识别出来了,而且把他们标记成了黑色,实际上只要把这个步骤修改,标记为透明,就能达成这一小节的目标。
用代码表示,则为:
1 | img[x][y]=[0,0,0,0] |
裁切成内切,然后调整为300x300大小
首先根据之前得到的,可以简单地使用ROI裁切到目标区域。
1 | img = img[oy-r:oy+r,ox-r:ox+r] #ROI区域选择 |
然后使用skimage.transform.resize方法(其他图像处理库的resize方法也可以)完成大小的调整。
1 | img = skimage.transform.resize(img,output_shape=(300,300)) |
输出图片
最后用skimage.io.imsave输出图片即可。
1 | skimage.io.imsave(output+filename, img) |
结论
虽然由于我的业务不精湛,制作这个程序花了不少时间,碰了不少壁,但是解决这个问题的过程中,也能学习到不少东西,能够熟练这些图像处理库的用法,为日后做更进一步的工作作一定的铺垫。




