1 引言
最近,Switch 平台上的一款游戏——《集合吧!动物森友会!》(以下简称动森)非常火爆。写作当天也是刚刚结束了樱花季,钓鱼节和复活节三连(今年也是巧了)。玩家们(比如我):出门听风声,抬头蛋气球。水里一挥杆,全是鱼儿蛋。好不容易紧锣密鼓的活动已经结束,玩家们(好吧我承认就是我)也开始休闲了起来。
之前在自己可以设计岛旗的时候我就想到,如果可以将自己喜欢的图案作为岛旗岂不美哉?但是这也比较困难,因为动森的设计图纸只有 32 × 32 像素。要把自己喜欢的图案设计进去,则先要将原图片压缩成 32 × 32 像素的图片,然后将图片放大以便观察,把每一块的颜色填进去就好了。首先我们不难想到,用画图等软件进行修改,比如我们以皮神为例,原图如下(图片源于网络,侵删):
缩小后是这样的:
再放大显示是这样的:
这时华生发现了盲点,图片上每个像素点的边缘没有框线,这给我们的观察带来了不便。作为一名程序员,我的第一反应当然是编程来解决啦,所以我们的任务就是:将一张图片缩小为 32 × 32 像素,之后将其放大观察,并在像素点的边缘增加网格线。本文将主要使用 PIL 库进行图片的修改,用 matplotlib 库进行图片的显示。
知道任务之后我们就开始干活啦!第二部分将介绍图片的修改,第三部分将介绍图片的查看,第四部分将介绍文字图片的生成(文章提供完整代码,不看讲解的同学可以直接跳过,文章内容适合有一丢丢编程基础的同学阅读)
2 图片的修改—— PIL 库的使用
本文章需要使用 PIL 中的 Image、ImageDraw 和 ImageFont 。所以开头导入文件包:
from PIL import Image, ImageDraw, ImageFont
2.1 缩小图片
首先我们使用 Image.open() 函数打开文件。
Image.open(file) ⇒ image
传入参数为文件完整路径(含文件名),返回一个 image 对象。
接下来我们用 image 对象中的 resize 方法对图片进行缩小。
im.resize(size) ⇒ image
im.resize(size, filter) ⇒ image
resize 函数可以改变图片的大小。其中:参数 size 是二元组,表示图片大小,单位为像素,参数 filter 表示伸缩算法,有四种模式:NEAREST(使用最近邻居,如果不加说明默认是该方法)、BILINEAR(在 2 × 2 环境线性插值),BICUBIC(在 4 × 4 环境三次样条插值),或ANTIALIAS(高质量的下采样滤波器)。文档中说除非使用时速度的重要性远高于质量,否则建议用 ANTIALIAS 方法、笔者将四种方法得到的结果进行对比,如下(输出图片的方法将在第三部分进行介绍):
可以明显看出, NEAREST 图片失真严重,虽然 ANTIALIAS 方法理论上最优,但个人认为在这张图片上 BILINEAR 方法更胜一筹。主要优势体现在下面两个方法黄色区域都有噪点(尤其是边缘区域),这也提醒了我们理论最优的方法不一定是万金油,有时也要视具体情况而定。所以本文采用 BILINEAR 方法作为缩小图片的方法,代码如下:
original_img = Image.open(img_dir)
temp_img = original_img.resize((32,32),Image.BILINEAR)
2.2 放大图片
和缩小图片类似的,我们可以将缩小后的图片用 resize 函数来改变图片的大小进行放大,将长宽分别变为原来的 10 倍,放大为 320 × 320 像素。将图片的进行像素的放大,一方面在于便于观察,另一方面在于可以增加网格线。原图的一个点变成了放大后图片的一个 10 × 10 的像素块,将像素块的边缘画上线,就可以实现网格线的效果。笔者依旧将四种方法进行比较,如下:
通过图片我们可以看出:三种方法在表现效果上又吊打了 NEAREST 方法,但在这里咱们先别急着下结论,待我们加上网格线之后再看。
2.3 增加网格线
我们无法直接在 image 对象上画画,要想画画,我们需要先创建一个画布。ImageDraw 中的 Draw 函数可以将 image 对象变成画布。
Draw(image) ⇒ Draw instance
传入参数为 image 对象变量名,返回 draw 实例。
接下来我们使用 draw 实例中的 line 函数进行画线。
draw.line(xy, options)
其中 xy 指线的起始点和结束点,可以写成 [ (x1 , y1) , (x2 , y2) … ] 也可以写成 [ x1 , y1 , x2 , y2 ] 。option 中的 fill 属性可以设置线的颜色 width 属性可以设置线的宽度。举个例子:
draw.line([0,0,0,319],fill=(200,200,200),width=1)
这条代码就是画了一条从 (0 , 0) 到 (0 , 319) 的一条直线,线条颜色的 RGB 值为 (200, 200, 200) 线条宽度为 1px 。
本文条件下的放大加画线代码如下(以用默认方法为例):
# 默认选项为 NEAREST 方法
out_img_nearest = temp_img.resize((320,320))
draw_nearest = ImageDraw.Draw(out_img_nearest)
for i in range(32):
draw_nearest.line([i*10,0,i*10,319],fill=(200,200,200),width=1)
draw_nearest.line([i*10+9,0,i*10+9,319],fill=(200,200,200),width=1)
draw_nearest.line([0,i*10,319,i*10],fill=(200,200,200),width=1)
draw_nearest.line([0,i*10+9,319,i*10+9],fill=(200,200,200),width=1)
这样我们可以给放大后的图片画上网格,效果如下:
我们想要的结果是,每个 10 × 10 的像素块内只有一种颜色,但是后三种方法在边缘区域均出现一个像素块内有多种过渡颜色的情况,所以在这篇文章的场景下,我们选用 NEAREST 作为放大算法。
这时作者又发现,DIY 的中间有黑色的十字做参照,在后面加入如下代码即可:
draw_nearest.line([159,0,159,319],fill=(0,0,0),width=1)
draw_nearest.line([160,0,160,319],fill=(0,0,0),width=1)
draw_nearest.line([0,159,319,159],fill=(0,0,0),width=1)
draw_nearest.line([0,160,319,160],fill=(0,0,0),width=1)
效果如下:
至此,图片修改就介绍完了。
3 图片的显示 —— matplotlib 的简单应用
按惯例,先导包:
import matplotlib.pyplot as plt
官网上给予了一些常用图表的代码示例,比如柱状图,折线图,散点图,饼图等等。有其他需求的同学们可以参考:https://matplotlib.org/gallery/index.html 十分好用!
3.1 显示单张图片
显示单张图片非常简单,只需要短短三行代码,如下:
plt.imshow(image) # 传入一个图片实例
plt.axis("off") # 去掉坐标轴
plt.show() # 弹窗显示图片
3.2 显示多张图片
为了方便叙述,直接上全过程的代码,显示图片的代码在最后,因为这一部分相较于图片修改要简单一些,所以直接在注释中说明:
from PIL import Image, ImageDraw
import matplotlib.pyplot as plt
img_dir = 'D:/文件/个人/文章/20200412/pika.png'
if __name__ == "__main__":
original_img = Image.open(img_dir)
temp_img = original_img.resize((32,32),Image.BILINEAR)
'''
temp_img_nearest = original_img.resize((32,32))
temp_img_bilinear = original_img.resize((32,32),Image.BILINEAR)
temp_img_bicubic = original_img.resize((32,32),Image.BICUBIC)
temp_img_antialias = original_img.resize((32,32),Image.ANTIALIAS)
fig, axs = plt.subplots(2, 2)
axs[0,0].imshow(temp_img_nearest)
axs[0,0].axis('off')
axs[0,0].set_title("NEAREST")
axs[0,1].imshow(temp_img_bilinear)
axs[0,1].axis('off')
axs[0,1].set_title("BILINEAR")
axs[1,0].imshow(temp_img_bicubic)
axs[1,0].axis('off')
axs[1,0].set_title("BICUBIC")
axs[1,1].imshow(temp_img_antialias)
axs[1,1].axis('off')
axs[1,1].set_title("ANTIALIAS")
plt.show()
'''
# 默认选项为 NEAREST 方法
out_img_nearest = temp_img.resize((320,320))
draw_nearest = ImageDraw.Draw(out_img_nearest)
for i in range(32):
draw_nearest.line([i*10,0,i*10,319],fill=(200,200,200),width=1)
draw_nearest.line([i*10+9,0,i*10+9,319],fill=(200,200,200),width=1)
draw_nearest.line([0,i*10,319,i*10],fill=(200,200,200),width=1)
draw_nearest.line([0,i*10+9,319,i*10+9],fill=(200,200,200),width=1)
draw_nearest.line([159,0,159,319],fill=(0,0,0),width=1)
draw_nearest.line([160,0,160,319],fill=(0,0,0),width=1)
draw_nearest.line([0,159,319,159],fill=(0,0,0),width=1)
draw_nearest.line([0,160,319,160],fill=(0,0,0),width=1)
# BILINEAR
out_img_bilinear = temp_img.resize((320,320),Image.BILINEAR)
draw_bilinear = ImageDraw.Draw(out_img_bilinear)
for i in range(32):
draw_bilinear.line([i*10,0,i*10,319],fill=(200,200,200),width=1)
draw_bilinear.line([i*10+9,0,i*10+9,319],fill=(200,200,200),width=1)
draw_bilinear.line([0,i*10,319,i*10],fill=(200,200,200),width=1)
draw_bilinear.line([0,i*10+9,319,i*10+9],fill=(200,200,200),width=1)
draw_bilinear.line([159,0,159,319],fill=(0,0,0),width=1)
draw_bilinear.line([160,0,160,319],fill=(0,0,0),width=1)
draw_bilinear.line([0,159,319,159],fill=(0,0,0),width=1)
draw_bilinear.line([0,160,319,160],fill=(0,0,0),width=1)
# BICUBIC
out_img_bicubic = temp_img.resize((320,320),Image.BICUBIC)
draw_bicubic = ImageDraw.Draw(out_img_bicubic)
for i in range(32):
draw_bicubic.line([i*10,0,i*10,319],fill=(200,200,200),width=1)
draw_bicubic.line([i*10+9,0,i*10+9,319],fill=(200,200,200),width=1)
draw_bicubic.line([0,i*10,319,i*10],fill=(200,200,200),width=1)
draw_bicubic.line([0,i*10+9,319,i*10+9],fill=(200,200,200),width=1)
draw_bicubic.line([159,0,159,319],fill=(0,0,0),width=1)
draw_bicubic.line([160,0,160,319],fill=(0,0,0),width=1)
draw_bicubic.line([0,159,319,159],fill=(0,0,0),width=1)
draw_bicubic.line([0,160,319,160],fill=(0,0,0),width=1)
# ANTIALIAS
out_img_antialias = temp_img.resize((320,320),Image.ANTIALIAS)
draw_antialias = ImageDraw.Draw(out_img_antialias)
for i in range(32):
draw_antialias.line([i*10,0,i*10,319],fill=(200,200,200),width=1)
draw_antialias.line([i*10+9,0,i*10+9,319],fill=(200,200,200),width=1)
draw_antialias.line([0,i*10,319,i*10],fill=(200,200,200),width=1)
draw_antialias.line([0,i*10+9,319,i*10+9],fill=(200,200,200),width=1)
draw_antialias.line([159,0,159,319],fill=(0,0,0),width=1)
draw_antialias.line([160,0,160,319],fill=(0,0,0),width=1)
draw_antialias.line([0,159,319,159],fill=(0,0,0),width=1)
draw_antialias.line([0,160,319,160],fill=(0,0,0),width=1)
fig, axs = plt.subplots(2, 2) # 创建一个由 2 × 2 个小图片组成的图片
axs[0,0].imshow(out_img_nearest) # 对左上图片进行设置
axs[0,0].axis('off')
axs[0,0].set_title("NEAREST")
axs[0,1].imshow(out_img_bilinear)
axs[0,1].axis('off')
axs[0,1].set_title("BILINEAR")
axs[1,0].imshow(out_img_bicubic)
axs[1,0].axis('off')
axs[1,0].set_title("BICUBIC")
axs[1,1].imshow(out_img_antialias)
axs[1,1].axis('off')
axs[1,1].set_title("ANTIALIAS")
plt.show()
4 其他脑洞 —— 写字
不少岛民喜欢在地上或衣服上写字,我也不例外,我又脑洞大开,可不可以将字转成类似的图片呢?
说干就干,首先要新建一张 32 × 32 像素的图片。
图片创建函数为 Image.new()
Image.new(mode, size, color) ⇒ image
mode 为图片类型,size 为一个二元组表示图片的长宽,color 则表示背景颜色,其中 color 的数据类型取决于图片的类型。
关于图片类型的具体内容可以参考:https://pillow.readthedocs.io/en/stable/handbook/concepts.html#concept-modes 。本文不做具体说明,有需要的同学可以参考一下。
以本文为例,为:
temp_img = Image.new('RGB',(32,32),(255,255,255))
为了修改图片,我们再创建画布:
draw_text = ImageDraw.Draw(temp_img)
之后调用 image 对象中的 text 方法进行写字:
draw.text(position, string, options)
其中 position 是一个二元组表示字的起始位置,string 不多说了,重点是这个 options ,这里的 options 需要输入一个 ImageFont 类的实例。我们可以通过 ImageFont 类中的 truetype 函数创建一个 ImageFont 实例。
ImageFont.truetype(file, size) ⇒ Font instance
其中 file 指操作系统中字体文件的文件名,size 为字体大小。
举个例子,输出一个 32 像素、黑体、黑色的“哈”,完整代码如下:
from PIL import Image, ImageFont, ImageDraw
import matplotlib.pyplot as plt
char = '哈'
fontSet = ImageFont.truetype('simhei.ttf',32)
if __name__ == "__main__":
temp_img = Image.new('RGB',(32,32),(255,255,255))
draw_text = ImageDraw.Draw(temp_img)
draw_text.text((0,0),char, fill=(0,0,0), font=fontSet)
out_img_nearest = temp_img.resize((320,320))
draw_nearest = ImageDraw.Draw(out_img_nearest)
for i in range(32):
draw_nearest.line([i*10,0,i*10,319],fill=(200,200,200))
draw_nearest.line([i*10+9,0,i*10+9,319],fill=(200,200,200))
draw_nearest.line([0,i*10,319,i*10],fill=(200,200,200))
draw_nearest.line([0,i*10+9,319,i*10+9],fill=(200,200,200))
draw_nearest.line([159,0,159,319],fill=(0,0,0),width=1)
draw_nearest.line([160,0,160,319],fill=(0,0,0),width=1)
draw_nearest.line([0,159,319,159],fill=(0,0,0),width=1)
draw_nearest.line([0,160,319,160],fill=(0,0,0),width=1)
plt.imshow(out_img_nearest)
plt.axis("off")
plt.show()
效果如下:
5 文章的不足和思考
- 按照文章的方法,我们无法获得每个像素块的 RGB 值,只能用眼睛观察对比
(不过动森本来也不是用 RGB 来选颜色的)。 - 在动森中一张图片最多只能使用 16 种颜色,本文在生成图片时没有考虑颜色数量的限制。
- 只能用于平面物体的 DIY ,用在衣服上可能会受空间的影响(衣服一般是半球形的,直线可能会发生弯曲)。
- 动森的设计在颜色边缘拐弯处会自动进行平滑化处理,本文也未做考虑。
6 灵魂画师试水作品
笔者试着画一下,效果竟然意外地不错(怕麻烦直接用了默认的 16 色):
以上就是文章的全部内容,希望可以帮到大家,祝动森玩家们在岛上玩得愉快。
文章如有不足或错误,恳请大家批评指正。