随着python解释器的一阵运行,整个Photoshop都变得索然无味起来

本文是某浏览器索然无味的前篇,讲述了如何用PIL自动拼图。

前段时间搞到一把Galgame,在例行拆包之后(关于拆包,还有一点背时故事,看看好久讲嘛),我看着面前的图片碎片和Photoshop,突然有了一个大胆的想法:既然游戏引擎本身可以组装图片,那我能不能模仿游戏引擎来自动组装呢?

可以!当然可以!俗话说,Python是万能的。在一个整个空气中都弥漫着躁动气息的夜晚,我在机房对着电脑桌面无所事事,算了,发NMD呆,Steam关闭,Notepad++启动!

问题分析


游戏CG资源(解包后)

最开始摆在我眼前的是这玩意。那时我懒得写程序,于是开起PS一个个手动操作,求排列组合。这玩意就是我拼出来的,从说明就可以猜到我有多少mmp要讲。后来呢,我发现游戏脚本中存储了图像重叠上去的顺序,大概是下面这样。

User@PC:~$ grep -n v101 *
m02z_251ch.lua:85:      scene("v101a", 1)
m02z_251ch.lua:110:      part(v101d)
m02z_251ch.lua:115:      part(v101b)
m02z_251ch.lua:124:      part(v101c)
m02z_251ch.lua:200:      part(v101e)
m02z_251ch.lua:208:      part(v101a2)
m02z_251ch.lua:231:      part(v101b)
m02z_251ch.lua:250:      part(v101d)
m02z_251ch.lua:259:      part(v101b)
m02z_251ch.lua:270:      part(v101e)
m02z_251ch.lua:285:      part(v101c)
m02z_251ch.lua:310:      part(v101a2)
m02z_251ch.lua:321:      part(v101c)
m02z_251ch.lua:338:      part(v101a2)
m02z_251ch.lua:343:      part(v101c)
m02z_251ch.lua:376:      part(v101c)
m02z_251ch.lua:449:      part(v101c)
m02z_251ch.lua:475:      part(v101b)
m02z_251ch.lua:523:      part(v101b2)
m02z_251ch.lua:527:      part(v101d2)
m02z_251ch.lua:539:      part(v101a4)
m02z_251ch.lua:550:      part(v101e3)
m02z_251ch.lua:557:      part(v101d4)
m02z_251ch.lua:565:      part(v101e5)
m02z_251ch.lua:586:      part(v101a6)
m02z_251ch.lua:591:      part(v101e5)
m02z_251ch.lua:602:      part(v101d5)

好吧,我就可以通过手动计算推测出有哪些组合实际上出现过了,复杂度瞬间从O(n^2)甚至O(n^3)降低到了O(n)。然而迄今为止我还是手动操作。直到有一天……

直到有一天我有幸搞到了这家厂商的新作却再也不想拼图了。在群里一通胡乱分析之后,我找到了这东西:

5:  patch("v001b", "bg", 128, 64)
8:  patch("v001c", "bg", 624, 64)
11:  patch("v001d", "bg", 624, 64)
14:  patch("v001e", "bg", 464, 160)
17:  patch("v001f", "bg", 464, 160)
20:  patch("v003a2", "bg", 720, 64)
23:  patch("v003b", "bg", 720, 64)
26:  patch("v003c", "bg", 720, 64)
29:  patch("v003d", "bg", 400, 144)
32:  patch("v003e", "bg", 400, 144)
35:  patch("v003f", "bg", 400, 144)
38:  patch("v003g", "bg", 64, 96)
41:  patch("v003h", "bg", 64, 96)
44:  patch("v003i", "bg", 64, 96)
47:  patch("v005a2", "bg", 544, 96)
50:  patch("v005b", "bg", 544, 96)
53:  patch("v005c", "bg", 544, 96)
56:  patch("v007a2", "bg", 304, 112)
59:  patch("v007b", "bg", 304, 112)
62:  patch("v007c", "bg", 304, 112)
65:  patch("v008a2", "bg", 192, 96)
68:  patch("v008b", "bg", 192, 96)
71:  patch("v008c", "bg", 192, 96)
74:  patch("v009a2", "bg", 128, 112)
77:  patch("v009b", "bg", 128, 112)

游戏是程序,脚本也是程序,凭什么它做得我做不得。坐标就在嘴边,动手!

主要难点

其实没什么难点,也就突然发现脚本里面钻出来个if else而已。我不得不尝试处理它,而我在浏览器索然无味中说过,我写不来编译器。安PIL啊配Python环境啊相当轻松。PIL还是好用,主要精力都去处理这个if else结构了。硬要说的话,还有一个自动取名和去重,也耗了不少功夫,主要是要实现自动命名,需要检测哪些图片显示出来了,我又不想开个一兆大的二维数组每次还要遍历一下,反正也费了点工夫。

代码实现

加载坐标

这个简单,正则表达式一匹配,往字典里压就是。

def load_coord(file):
    coord_re = re.compile(coord_regex)
    ret = dict()
    for l in open(file,'r').readlines():
        if coord_re.search(l):
            words = split_words(l)
            ret[words[1]] = (int(words[-2]),int(words[-1]))
    return ret

读取脚本

这个也简单,每行割一下割成数组,根据第一个词判断是什么指令,适当优化一下往数组里压就是。

def load_cmd(file):
    gap = 0
    ret = []
    token_re = re.compile(token_regex)
    for l in open(file,'r').readlines():
        gap += 1
        if token_re.search(l):
            words = split_words(l)
            if words[0] in ['if','else','end']:
                ret.append([words[0]])
                continue
            if words[0] == 'part':
                # continuous part optimization 
                if gap != 1:
                    ret.append([words[0], words[1]])
                else:
                    ret[-1].append(words[1])
                gap = 0
                continue
            if words[0] == 'scene':
                ret.append([s for s in words if not s.isdigit() and '.' not in s])
                continue
    return ret

拼图

这个也简单,读到建立新场景的指令时,把背景图打开,读到叠图的指令时,把图叠上去就是。

def paste_image(bg_fd, fg_name, coords):
    fg_path = '{}/{}.{}'.format(image_dir, fg_name, input_format)
    try:
        c = coords[fg_name]
    except:
        # guess coordinate
        c = (0,0)
    try:
        fg = Image.open(fg_path)
        bg_fd.paste(fg, c)
    except:
        pass

自动取名

这个就有点麻烦了。我要检测当前究竟哪些图在显示。我的实现是,把图片按坐标分组,拼图时检查每一组最近叠上去的是哪一张图。给图片按坐标分组还好,无非就是字典里面塞字典比较气人。这种时候可能LINQ有奇效,不过即使有~~还真有~~我也用不来。检测到当前显示的图片后,即可按照显示的图片来取名。

def output_image(fd, pattern):
    # count max symbol length
    p = []
    for s in pattern[1:]:
        alpha_len = 0
        if s == ' ':
            continue
        for c in s:
            if not c.isdigit():
                alpha_len += 1
            else:
                break
        p.append(alpha_len)
    # if length > 1 (multi alpha) use _ to divide
    use_divide = (max(p) > 1)
    
    # generate output file name
    str = ''
    for s in pattern:
        if s == ' ':
            continue
        str += s
        if use_divide:
            str += '_'
    str = str.rstrip('_')
    out_filename = '{}/{}.{}'.format(output_dir, str, output_format)
    fd.save(out_filename)

查重

其实和自动取名是一样的……把出现过的图片组合保存一下,叠图时如果遇到存在过的组合,不输出就是。

for d in data:
    if d[0] not in variants:
        variants.append(d[0][:])
        output_image(d[1], d[0])

分支处理

好耍的地方来了。如果只是要求处理if条件为真或者假,那还好说,老老实实照着跑就是,可是我要处理if条件同时为真和假的情形(咋瞬间就感觉这东西量子化了)问题就好玩了,不过办法还是有。if出现时,把当前状态压个栈,然后接着执行if里面的逻辑。这样if结束时,我们就同时拥有了if为真和假时的结果,然后把栈里面的最后一个状态弹出来,和当前状态同步执行。当出现else时,把栈顶状态和当前状态交换,显然,栈顶状态仅执行else-end部分,当前状态仅执行if-else部分。如图所示。


红色是正在执行的状态,绿色是未执行的状态

考虑到避免状态多了出问题,每条指令执行完了之后,对活动的状态进行去重。

if cmd[0] == 'if':
    stack.append(data)
if cmd[0] == 'else':
    data , stack[-1] = stack[-1] , data
if cmd[0] == 'end':
    if len(stack) > 0:
        data += stack.pop()

剩下的工作

剩下的工作那就简单了。把上面那一堆串起来,再适当处理一下命令行参数就是。自从有了这东西,拼图轻松愉快。两天的调试终究有作用,新作品从安装到完成,五分钟搞定,瓶颈从手速变成了网速。

发表评论

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