简陋无比的 Python 抠图方案,好像还挺像回事儿?

简陋无比的 Python 抠图方案,好像还挺像回事儿?手边没有抠图工具怎么办?几行代码就能搞定!初学编程时的一个趣味小项目,虽然没什么含金量,但也算是一次积极的尝试~

这是我参与2022首次更文挑战的第13天,活动详情查看:2022首次更文挑战

本篇内容改编自我的微信公众号,所述工作是我初学编程时的一个趣味小项目,虽然没什么含金量,但可以算得上是我作为一个初学者的一次勇敢尝试和有趣探索,在此分享给大家,欢迎不吝赐教。

背景介绍

从某APP中截取了我的背单词曲线之后,我敏锐地发现了蕴藏在其中的数学规律。

640.webp

每六个月达到一次峰值,峰值的高度不断减小。为了在图上画一条线来拟合这个折线,我打开 MATLAB 一通操作猛如虎,画出了一个正弦函数受到一个衰减指数函数的调制的图像。但问题来了,我不知道怎么把这两个图贴在一起,需要把 MATLAB 绘制的曲线从白色背景中抠出来。

联想到曾经我用 Excel 抠二维码的憨批操作(当时不用PS),我觉得还是有必要进化一下我的抠图技能。

640.jpg

纯色背景抠图

从原理上讲,从这么一个简单背景中把图案抠出来,无非就是对每一个像素点检查它的颜色,如果是背景色就把透明度变成0,否则保留。因此,这是非常容易实现的。

from PIL import Image

im = Image.open('SaoMaYouJingXi.jpg') #读取图片
im = im.convert('RGBA') #转化成RGBA(RBG+透明度)格式
pix = im.load()width = im.size[0]
height = im.size[1]

for x in range(width):
    for y in range(height):        
        r, g, b, a = pix[x, y]        
        if (r>100)&(g>100): #如果是白色
        a = 0 #透明度改成0
        im.putpixel((x,y),(r,g,b,a)) #把四个参数还给这个像素
        
im.save("SaoMaYouJingXi.PNG") #保存

# 早期写的东西甚至完全没有代码规范意识

小试牛刀:

640.png

不愧是精确到像素的抠图,放大了看也这么干净利索。

变色背景抠图

但,从纯色简单背景中抠图就太没意思了。我又联想到曾经用Excel从照片里抠手绘的班徽:

640.webp

当时也是费了很大功夫,抠出来的效果在浅色背景下还可以,但是放在深色背景中就非常不堪入目了。于是,我打算想办法把班徽重新抠一下。

第一次尝试

既然不是简单的纯黑或纯白背景,没办法直接判断颜色的RGB值大概在什么范围,那么我们首先要让程序去识别并记住背景有哪些颜色,然后再把这些颜色的像素抠掉。

所以我们要分两大步:

  1. 手动从背景中采样,截图保存,导入程序,供其识别、记录;
  2. 把整个图片导入,处理,导出。

这张班徽的照片里,我在四条边附近分别截取了一张背景样本,后来为了优化效果,又在没抠掉的地方补了一张。

640.png

接下来就是让计算机记住这些出现过的颜色。

我们知道,一个像素的颜色可以用R(0-255)G(0-255)B(0-255)三个变量来表示,一张图中的颜色总数不超过256^3

鉴于我们现在只关心某一种颜色有没有出现过,不妨创建一个256x256x256的三维哈希表。

可以想象一个大的正方体,里边有256^3个小格子。初始状态下每个小格子里都是0,当计算机看到出现某一种颜色(m,n,p),就把第m行第n列第p层的那个小格子里的0变成1。等计算机遍历完了所有的像素,这些格子里该填1的都填上了1

这里,我们为了减小计算机的工作量,可以把相邻的3R/G/B值简化成一个(如R=0,1,2→R'=0R=3,4,5→R'=1),把这个256x256x256的正方体压缩一下,长宽高各变为原来的1/3,列表大小缩小到原来的1/27

matrix=[]
for i in range(85):
    matrix.append([])
    for j in range(85):
        matrix[i].append([])
        for k in range(85):
            matrix[i][j].append(0)

然后我们不动这个大盒子,让计算机再去检查第二个样本……等到所有的样本都看完了,盒子就填得差不多了。这时候,背景中出现过的颜色大部分已经被标记了。

我们把这个检查的过程定义为learn

def learn(str):
    im = Image.open(str)
    pix = im.load()
    width = im.size[0]
    height = im.size[1]
    for x in range(width):
        for y in range(height):
            r, g, b = pix[x, y]
            matrix[r][g][b]=1

在本例中,让程序learn完五张采样得到的背景图,再执行抠图的主程序,保存图片。

接下来我们看下效果:

640.webp

失望,非常失望。无论我再怎么给它查漏补缺,补充新的背景来learn,抠出来还是会有大量残留。

不过没关系,我们可以再模糊一点。宁可错抠一千,不放过一个。

第二次尝试

接下来,我们增设一个模糊度dim,意思是在抠图的判断条件中,只要这个点的RGB坐标出现在被标记坐标的附近,三个坐标分量都在某被标记点对应坐标分量±dim的范围内,即该点的RGB坐标在以某被标记点为中心、棱长为2*dim的正方体内,我们就把它抠掉。

具体操作不再赘述。随着我不断增大dim的值,残留的区域确实越来越少,但是结果总是不尽人意。

640.webp

第三次尝试

于是,我不再满足于小立方体级别的模糊,决定大手一挥,激进一点。

这次,我打算计算出背景中所有像素点的R、G、B各自的最大值和最小值,然后把所有R、G、B都在最大值之下、最小值之上(包含最值)的像素全部抠掉。

为了判断方案的可行性,我把图片信息按颜色展开,在坐标系中呈现出来。

640.webp

这里,X、Y、Z轴分别是压缩之后的RGB值,范围是[0,85]中的整数。

每个点的颜色就是它的坐标对应的RGB值所对应的颜色。

在三维坐标系中,引入透明度,可以刻画第四个维度的信息。我们选取该坐标处像素的数量(也就是在原图中有多少个点同为这个颜色)作为透明度的自变量。

为了控制因变量,也就是透明度的范围在[0,1],我们需要知道最多有多少个像素是同一个颜色,把这个最大值作为分母。

同时,为了避免这个最大值过大,导致大部分点的透明度都非常高(透明度越接近0,记为越高),我们换一种映射,把通过比值求出来的相对透明度开四次根号。

最终,我们画出来的图像是这样的:

640.jpg

越不透明的点表示对应颜色的像素越多。

下面我们回到抠图。

分析一下刚刚费了不少力气画的这个图,我们发现,三种主色还是相对来说界限分明的。如果你不信,我可以随便找一张图给你展开一下:

image.png

我们来看一下背景样本中所有颜色的R、G、B的最大值和最小值。

640.webp

这个结果其实非常符合预期,因为从刚才那个坐标系中可以看到,跟背景相近的颜色都集中在R、G、B都非常大的地方。

我们把R、G、B都在对应区间里的点抠掉,然后看一下效果:

640.webp

640.webp

AMAZING!

简直完美。尽管我事实上并不认可这个原理,毕竟本质上这属于莽夫行为。但是不得不说,这个效果确实是非常不错的。

640.webp

对比一下Excel抠图、learn抠图和莽夫抠图:

640.webp

简直是一部愈演愈烈的革命乐章!

总结

至此,我已经完成了我最初的目标,并且积累了一套极其简陋但却好像还挺像回事儿的抠图方案。

在我看来,代码抠图的魅力不仅在于它精确到像素的精准打击,也在于它可以让你随心所欲定制需求的可塑性,还在于它只需要你更改一两个参数、点一下编译运行,就能自动输出成果的便捷。

此外,我还顺便做了一些同样没什么用的小功能,比如马赛克:

640.webp

不论是抠图也好,打码也好,逻辑上不难理解,代码上也不难实现。在日常生活中,有时候手边就是缺乏处理简单任务的专业工具,比如 PhotoShop 。这时候与其安装 Ps ,倒不如自己写一个程序,根据自己的需求编码实现功能,看似麻烦,实则不失为一条捷径。

项目代码:Yuezih-Playground/Ez Image Process (github.com)
(早期工作,年久失修。没有demo,摆烂开源。)

今天的文章简陋无比的 Python 抠图方案,好像还挺像回事儿?分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/17676.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注