通过Python编程在《我的世界》中用彩色羊毛绘制世界名画《蒙娜丽莎》。
前置条件
- 一台安装了Java8(JRE)和Python的PC/Mac
- Python编程基础(本文使用2.7)
- 计算机图像的一点儿基本认识(RGB、BMP位图)
- 好奇心和折腾的热情
准备环境
安装Minecraft和Forge
推荐使用HMCL下载Minecraft。HMCL是一个跨平台的Minecraft启动器,可以方便的下载不同版本的游戏以及Forge、OptiFine等常用插件。除官方安装源之外,HMCL还提供了多个镜像服务器,对于网络不太灵光的我们非常有用。
启动HMCL,打开“启动器设置”标签,在“下载源”中选择一个(推荐BMCLAPI)。切换到“游戏设置”标签页,选择“游戏下载”,点击“刷新”获取可用的版本列表。本文使用最近刚刚发布的1.11版本。安装成功后上方的“版本”下拉选单中会出现对应的选项,选择之。
点击“自动安装” -> “Forge”,选择一个Forge版本下载。完成后“版本”选单中会出现带有游戏版本和forge版本的选项,例如“1.11-forge1.11-13.19.1.2189”。
回到“主页”,在版本中选择带forge的选项,“登陆”选“离线模式”,“名字”输入任意用户名,点击“启动Minecraft”。这时HMCL还会去下载一些游戏需要的资源文件,等下载完成后就会出现Minecraft主界面,如果左下角有“Powered by Forge”字样,就表示安装成功。
安装Raspberry Jam Mod
Raspberry Jam Mod是arpruss为Minecraft开发的插件,在PC版Minecraft中的实现了树莓派版的Python接口。可以从这里下载最新版本。作者提供了自动安装工具RaspberryJamMod-Installer.exe,但是对非官方版本的Minecraft,可能会失败,不推荐使用。
下载mods.zip和python-scripts.zip两个文件。从mods.zip中解压出跟Minecraft版本对应的插件文件RaspberryJamMod.jar。找到Minecraft的安装目录:对于Windows用户,Minecraft安装在HMCL可执行文件所在目录下的.minecraft文件夹;对于Mac用户,则在~/.minecraft目录。拷贝RaspberryJamMod.jar到.minecraft/mods目录。启动Minecraft,选择“Mod”,左侧列表中出现“Raspberry Jam Mod”则表示安装成功。
解压python-scritps.zip,将得到的mcpypi目录拷贝到.minecraft目录。在Minecraft中创建一个新世界,建议使用“创造”模式,并将“世界类型”改为“超平坦”。进入游戏后,输入/py donut,执行mcpypi中的donut.py脚本,看一下效果。或者也可以在Minecraft之外执行脚本:
# 请先切换至mcpypi目录
python donut.py
Minecraft在窗口失去焦点时会自动打开主菜单并暂停游戏,在进行Python编程时很不方便。打开.minecraft目录下的options.txt文件,找到下面这一行,把true改为false就可以解决这个问题。
pauseOnLostFocus:true
开始编程
在Python中与Minecraft交互
接下来可以自己写一个脚本实验一下。我们跳过Hello World,直接在地图上放置一个方块。注意,因为要导入mine.py这个文件,下面的脚本必须在mcpypi目录下执行。
from mine import *
mc = Minecraft.create()
mc.setBlock(0, 0, 1, block.WOOL_WHITE)
函数setBlock(x, y, z, b)将在(x, y, z)这个坐标放置一个方块b。在Minecraft中,玩家出生点的坐标为(0, 0, 0)。这个脚本在出生点的旁边(0, 0, 1)这个位置放一个白色羊毛方块。setBlock()函数的最后一个参数是一个Block类型的对象,这里使用的是block模块中的预置对象WOOL_WHITE,即白色羊毛。我们也可以自己生成一个新的对象。
b = Block(35, 0)
第一个参数表示方块类型,35是羊毛;第二个参数为附加数据,是可选的。对于羊毛方块,附加数据就表示羊毛的颜色。后面会继续讨论这个问题。
准备图片
找一张蒙娜丽莎的图片。为了避免在Minecraft里建造的“图片”过大,需要限制一下原始图片的尺寸。这里使用ImageMagick命令行工具convert,将图片转换成宽度为64像素的小图,转换过程中宽高比保持不变。
convert -geometry 64x mona_lisa.png mona_lisa_small.png
读取图片
绘图之前,首先要读取出原始图片的内容。计算机中的图片,本质上都是是由不同颜色的像素点组成的矩形阵列[1]。不同格式的图片文件(jpg、png)只是使用了不同的压缩算法进行处理而已。我们绕过这些具体的细节,直接使用Pillow[2]这个软件包来读取图片文件。首先还是用pip安装Pillow。
pip install Pillow
下面这段代码将读取/path/to/you/image.png这个图片文件,然后打印出(0, 0)这个点。打印的结果是形如(92, 107, 79)的一个三元组,也就是(0, 0)这个点的RGB颜色值。
from PIL import Image
im = Image.open("/path/to/you/image.png")
print im.getpixel((0, 0))
作画
在Minecraft中绘图,就是用不同颜色的方块去组成一个图像矩阵。Minecraft提供16种不同颜色的羊毛方块,正好用来当作“颜料”。但是,多数图片(以及真实世界)中的颜色远远不止16种,因此我们需要写一个函数,将某个RGB颜色值映射到一种羊毛方块,并且尽可能保证二者在视觉上相近。这就涉及到颜色比较的问题,其实是一个比较复杂的问题。不过StackOverflow上有一个简单的方案。下面Python代码实现了这一算法:
def ColorDistance(c1, c2):
rmean = (c1[0] + c1[0]) / 2;
r = c1[0] - c2[0]
g = c1[1] - c2[1]
b = c1[2] - c2[2]
return sqrt((((512+rmean)*r*r)>>8) + 4*g*g + (((767-rmean)*b*b)>>8))
ColorDistance()函数计算两个RGB颜色之间的差异值,差异越小,表示两个颜色越接近。所以我们要做的,就是从16中羊毛颜色中,选择一个与当前点最接近的。Minecraft的Wiki上给出了这16种颜色对应RGB值。下面的MapColor()函数实现了这个选择过程:
wool_colors = [ (221,221,221),
(219,125,62),
(179,80,188),
(107,138,201),
(177,166,39),
(65,174,56),
(208,132,153),
(64,64,64),
(154,161,161),
(46,110,137),
(126,61,181),
(46,56,141),
(79,50,31),
(53,70,27),
(150,52,48),
(25,22,22)
]
def MapColor(c):
idx = -1
min_dist = float_info.max
for i, wc in enumerate(wool_colors):
d = ColorDistance(wc, c)
if d < min_dist:
min_dist = d
idx = i
return idx
MapColor()函数用了一个for循环去遍历所有的羊毛颜色,并记录下当前遇到的最小差异值(min_dist)和对应的下标(idx)。这是C/C++这些老式语言的做法,虽然比较容易理解,不过看起来有点啰嗦。而Python有更优雅实现方式:
from operator import itemgetter
def MapColor2(c):
distances = [ColorDistance(wc, c) for wc in wool_colors]
idx, min_dist = min(enumerate(distances), key=itemgetter(1))
return idx
接下来,我们只要依次读取图片中的每个点,用MapColor()函数选择对应的羊毛方块,再用setBlock()函数放置方块,就可以画出任何图像了。参考下面代码:
width, height = im.size
for x in range(width):
for y in range(height):
p = im.getpixel((x, y))
idx = MapColor(p)
b = block.Block(35, idx)
mc.setBlock(width - x - 1, height - y - 1, 50, b)
下面是我的作品:
一点扩展
所谓扩展,其实我在实验过程中走的一点儿弯路,顺便分享一下。为了用Minecraft中的16色羊毛作画,我的第一个想法是先把图片转换成16色位图(BMP)[3],然后只要把位图中的16种颜色分别映射到某一种羊毛方块,就可以作画了。打开一个16色位图文件,用getpixel()函数看一下某个点的值:
>>> from PIL import Image
>>> im = Image.open("/path/to/your/image.bmp")
>>> print im.getpixel((0, 0))
7
这次的打印结果变成了7,而不是之前的RGB三元组。这是因为16色位图使用了一个“调色板(Palette)”机制。没错,这就跟我们画画用的调色板是一样的,它记录了这张图片中使用的所有颜色。对于16色位图,调色板就是16个RGB三元组。有了调色板之后,对于图片中的每一个像素点,就只需要记录一个0-15的索引值,而不需要花3个字节去记录RGB。这种方法极大的节省存储空间,在早期计算机存储容量比较小的情况下,是非常有效的压缩方法。我们可以通过im.palette来获得位图文件的调色板。
>>> im.palette.getdata()
('BGRX', '\x00\x00\x00\x00\x00\x00\x80\x00\x00\x80\x00\x00\x00\x80\x80\x00\x80\x00\x00\x00\x80\x00\x80\x00\x80\x80\x00\x00\x80\x80\x80\x00\xc0\xc0\xc0\x00\x00\x00\xff\x00\x00\xff\x00\x00\x00\xff\xff\x00\xff\x00\x00\x00\xff\x00\xff\x00\xff\xff\x00\x00\xff\xff\xff\x00')
第一个字段'BGRX'为调色板的模式,第二个就是调色板对应的二进制数据。调色板以4字节为一个单位,其中前三字节为该颜色的RGB值,最后一个字节为0。下面这段代码将解析这段二进制数据并生成一个RGB三元组组成的数组:
from struct import unpack
bin_palette = im.palette.getdata()[1]
rgb_palette = [unpack("BBB", bin_palette[i:i+3]) for i in range(0, 64, 4)]
对于使用了调色板的位图文件,要获取某个点的颜色,就多了一个查找调色板的过程。上面的代码只要稍作修改就可以使用。
width, height = im.size
for x in range(width):
for y in range(height):
p = im.getpixel((x, y))
idx = MapColor(rgb_palette[p])
b = block.Block(35, idx)
mc.setBlock(width - x - 1, height - y - 1, 30, b)
相关资料
Raspberry Jam Mod的作者arpruss在Instructables上发表的一篇教程,想进一步了解这个插件的功能,可以读一下。本文也参考了这篇文章的内容。
Raspberry Jam Mod基本实现了树莓派版Minecraft的所有API。Martin O'Hanlon整理了这套API的参考文档。Raspberry Juice Plugin在第三方的Minecraft服务器Bukkit中实现了同样的API。
使用McPaint这个插件可以直接用鼠标在Minecraft里作画。
关于颜色的比较,可以使用python-colormath这个包。
-
其实还有另一种矢量图片,不是记录每个点的颜色,而是用点和线的组合来表示各种图形。矢量图片的优点是进行缩放的时候不会降低图片质量,在专业设计中用的比较多。 ↩
-
Pillow是PIL(Python Image Library)的一个fork版本,专门用于处理各种格式的图像。原始版本的PIL已经无人维护,不推荐使用。 ↩
-
转换可以通过Windows自带的画图工具完成:打开“文件” -> “另存为” -> “BMP图片”,“保存类型”选择“16色位图”。 ↩