在《我的世界》里画一幅《蒙娜丽莎》

通过Python编程在《我的世界》中用彩色羊毛绘制世界名画《蒙娜丽莎》。

前置条件

  • 一台安装了Java8(JRE)和Python的PC/Mac
  • Python编程基础(本文使用2.7)
  • 计算机图像的一点儿基本认识(RGB、BMP位图)
  • 好奇心和折腾的热情

准备环境

安装Minecraft和Forge

推荐使用HMCL下载Minecraft。HMCL是一个跨平台的Minecraft启动器,可以方便的下载不同版本的游戏以及Forge、OptiFine等常用插件。除官方安装源之外,HMCL还提供了多个镜像服务器,对于网络不太灵光的我们非常有用。

启动HMCL,打开“启动器设置”标签,在“下载源”中选择一个(推荐BMCLAPI)。切换到“游戏设置”标签页,选择“游戏下载”,点击“刷新”获取可用的版本列表。本文使用最近刚刚发布的1.11版本。安装成功后上方的“版本”下拉选单中会出现对应的选项,选择之。

选择安装源
安装Minecraft

点击“自动安装” -> “Forge”,选择一个Forge版本下载。完成后“版本”选单中会出现带有游戏版本和forge版本的选项,例如“1.11-forge1.11-13.19.1.2189”。

安装Forge

回到“主页”,在版本中选择带forge的选项,“登陆”选“离线模式”,“名字”输入任意用户名,点击“启动Minecraft”。这时HMCL还会去下载一些游戏需要的资源文件,等下载完成后就会出现Minecraft主界面,如果左下角有“Powered by Forge”字样,就表示安装成功。

启动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这个包。


  1. 其实还有另一种矢量图片,不是记录每个点的颜色,而是用点和线的组合来表示各种图形。矢量图片的优点是进行缩放的时候不会降低图片质量,在专业设计中用的比较多。

  2. Pillow是PIL(Python Image Library)的一个fork版本,专门用于处理各种格式的图像。原始版本的PIL已经无人维护,不推荐使用。

  3. 转换可以通过Windows自带的画图工具完成:打开“文件” -> “另存为” -> “BMP图片”,“保存类型”选择“16色位图”。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容

  • # Python 资源大全中文版 我想很多程序员应该记得 GitHub 上有一个 Awesome - XXX 系列...
    aimaile阅读 26,478评论 6 427
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,094评论 4 62
  • GitHub 上有一个 Awesome - XXX 系列的资源整理,资源非常丰富,涉及面非常广。awesome-p...
    若与阅读 18,643评论 4 418
  • 小舟从此逝 酒肉寄余生 I know these storms will come, let it rain le...
    忪痞阅读 201评论 0 1
  • 你被时间掩埋 在回忆的深沟里 抛出一个很久远的故事 让它晕开 烙印下模糊的图腾 有她明朗的笑声 有我不禁的啜泣 清...
    云沐阅读 213评论 0 4