再战ArcaeaB30生成器:Python模块PIL实战图像处理与拼接
书接上回ArcaeaB30录入和导出:Python简单的xlsx、json处理和图片编辑
这次是参考上一次经验,经过完全重写的版本的开发笔记。
文章目录
- 前言
- 一、数据从哪来?从社区中现有的B30查询手段说起
- 二、Excel侧数据读入与宏处理
- 三、Python侧程序
- 1.素材加载
- 2.打开文件
- 3.加载工作簿,获取信息
- 4.逐个制作信息卡
- a.获得曲绘
- b.卡底生成
- c.信息添加
- d.完成
- 5.全部缝合到背景
- a.卡片的缝合
- b.文字信息(玩家信息)的缝合
- 6.保存
- 效果展示
- 总结
- 附录:Python源码
结果如下:
前言
- 何为B30?
这是一个围绕移动端节奏游戏Arcaea的玩家数据潜力值展开的话题,具体机制可参阅Arcaea中文维基。
B30即Best 30,玩家若能知晓自己的B30具体内容,可以更有针对性地练歌推歌性歌弃坑,更高效地提升潜力值,因此较为重要。 - 此程序解决的问题是:在得到玩家的数据后,转换成信息高度集中且易读的一图流B30报表
下面是按照我的开发思路回忆整理的笔记正文。
一、数据从哪来?从社区中现有的B30查询手段说起
在我的上一次开发中,我使用了手动在电脑端录入Excel表格这一手段获取到玩家打歌的信息。此方法弊端很突出,具体表现为:
- 双端操作、手动打字输入带来的麻烦
- 不低的出错率
而在上一次开发之后,我尝试了一种由其他玩家提出过的方案:把打歌成绩截图用光学文字识别转换成可用的信息并记录,并在成功之后尝试将程序转移到移动端。但受我经验积累不足影响,识别准确率依然不高,在第一步便基本宣告失败。
事实上,社区主流查询手段大多是基于QQ机器人(例如软糖酱)的查询,其独立完成模拟玩家查询的数据请求、处理、图片生成等工作并直接返回成图。但在最近的一次(或几次)更新中,Bot查分受到了很大影响,存活无几。
囿于本人技术限制,不能做到独立完成数据请求的工作,因此我转而着手“借用”在线查分器返回的结果进行二次处理。于是有了这一次开发。
我并没有直接或间接联系在线查分服务的提供者或维护者,只提供一种数据二次加工的手段;以前且查且珍惜,现在仍然不会改变,不要滥用他人提供的服务。
二、Excel侧数据读入与宏处理
在网站上完成查询之后,自己的打歌数据可以用其自带的“导出CSV”功能保存表格到本地,但是该方法不保存详细数据(玩家名/潜力值详细数值/打歌详细成绩和日期等),因此我选择手动复制HTML元素到Excel处理。
不用爬虫,还是那句话,且查且珍惜
以及,输入按照如下操作进行:
第一步,输入玩家UID开始查询,往下拉找到玩家信息这三行,复制并直接粘贴到工作簿的第一行至第三行;
第二步,等待查询程序完成所有成绩拉取工作后,按下F12
,找到id为scores
的div元素,右键-复制-复制元素,然后到工作表的第四行直接粘贴。
获得数据之后,直接写宏,全部都是 高中级别的 字符串处理。过程略。
结果是保存到同一工作簿下的Song和Player两个工作簿,分别保存玩家数据和打歌成绩数据。
三、Python侧程序
用到的模块:
os sys
:本地路径相关的操作
tkinter.filedialog
:文件打开对话框
json
:处理json文件
openpyxl
:处理Excel工作簿
PIL
的Image ImageFont ImageDraw
:处理图片
1.素材加载
后面生成图片的时候素材都是要自己贴上去的,因此我选择提前加载上所有资源,这样如果资源缺失就直接提醒(报错退出)了
获取程序所处位置:
root_path = os.path.dirname(os.path.realpath(sys.argv[0]))
检测文件夹存在:
def checkSrc():if not os.path.isdir(os.path.join(root_path, 'src')):return True
然后在try下面加载,这样如果缺了可以捕获到错误提示缺了啥。
字体, json文件 和图片文件都要加载。
篇幅过于冗长,代码留给最后再放。
2.打开文件
写一个requirePath函数,顺便在函数内完成检查工作:
def requirePath():global filenameglobal savepathprint('Select a xlsx file:')filename = tkinter.filedialog.askopenfilename(title = '选择文件', filetypes=[('Excel工作簿','.xlsx'), ('Excel启用宏的工作簿','.xlsm'), ('Excel2003工作簿','.xls'), ('所有文件','.*')])if not os.path.isfile(filename):print('无效的文件名: %s'%filename)return -1print('filename = %s'%filename)print('Determine save path:')savepath = tkinter.filedialog.askdirectory(title = '选择保存路径')if not os.path.isdir(savepath):print('无效的保存地址: %s'%savepath)return -1print('savepath = %s'%savepath)return 0
这一段没什么好说道的
3.加载工作簿,获取信息
openpyxl提供了加载工作簿的方法。
book = openpyxl.load_workbook(path)
,返回工作簿对象存给book
sheet = book['Sheet1']
其中的Sheet1表存给sheet,表名按需改
sheet.cell(row,column).value
返回row行,column列的单元格的值。
需要注意的是:直接用sheet.cell(r,c)
返回的是cell对象,而不是其中的值,不好直接拿来计算。
写一个函数:
def openWorkbook(path):global sheetsheet={}try:book = openpyxl.load_workbook(path)except Exception as e:print('加载工作簿出错,详细信息:')print(e)exit(1)try:sheet['song'] = book['Song']except Exception as e:print('找不到数据,确保主表名为Song')exit(1)try:sheet['player'] = book['Player']except Exception as e:pass
找不到玩家信息表选择pass,是因为我希望程序能兼容.csv导出的那种形式的文件,即不含详细信息也能出来。
然后在主程序里用一个while循环读取数据,i是工作表中的列号(1是表头,从2开始):
while not sheet['song'].cell(i,1).value == None and i<=34:print('歌曲#%d生成...'%(i-2))temp=[]for j in range(1,11):if j>=5 and j<=9 and sheet['song'].cell(i,j).value==None:temp.append(0)else:temp.append(sheet['song'].cell(i,j).value)temp.append(i-1)cards.append(makeCard(temp))#makeCard是接下来要定义的一个函数i=i+1
循环条件中,读到空意味着数据读完了,这很正常;i<34
是最大读取33首歌的信息,因为最多咱要放到图里的只有33首:best30内的,外加有希望冲进b30的额外三首歌。
4.逐个制作信息卡
这一次开发与上一次的思路明显不同的地方就在于此。
之前我选择一遍遍历加底图,再一遍遍历加信息,再一遍加这,再一遍加那,代码就显得很冗长。
这一次用一点模块化的思路,我把一个成绩给你,你给我一个卡,集卡集满了我再一起缝到背景上。
这里就要写上一节用到的makeCard
函数。
分析输入:我用while把工作表中一行,也就是一首歌的成绩全部打包成一个List了,接下来我将要把这个List传给函数。
分析输出:我希望它直接返回给我一个PIL的图片对象。
全代码如下:
def makeCard(data):songjpg = searchSongData(data[0],data[1],data[9])#见下文,另一个自定义函数,用来查歌if data[10]<31:r=0g=0b=0for i in range(0,20):for j in range(0,130):rgb=songjpg.getpixel((i,j))r=r+rgb[0]g=g+rgb[1]b=b+rgb[2]r=int(r/3200)g=int(g/3200)b=int(b/3200)card = Image.new('RGB',(390,130),'#'+str(hex(r))[-2:].replace('x','0')+str(hex(g))[-2:].replace('x','0')+str(hex(b))[-2:].replace('x','0'))else:card = Image.new('RGB',(390,130),'#b68f17')card.paste(songjpg,(259,0),mask[0])card.paste(diff[data[1].upper()],(11,5),diff['BYD'])card.paste(arrowpng,(60,77),arrowpng)try:#clear statuscard.paste(badge[data[8]],(270,70),badge[data[8]])except Exception as e:passif data[4]==0:#pure,far,lostcard.paste(Image.new('RGB',(9,74),'#333333'),(170,43),mask[2])else:accbar=Image.new('RGB',(9,74),'#a55cb4')#maxaccbar.paste(Image.new('RGB',(9,74),'#794484'),(0,int(74*int(data[5])/2/(int(data[4])/2+int(data[6])+int(data[7])))))#pureaccbar.paste(Image.new('RGB',(9,74),'#FFAA11'),(0,int(74*int(data[4])/2/(int(data[4])/2+int(data[6])+int(data[7])))))#faraccbar.paste(Image.new('RGB',(9,74),'#DD4444'),(0,int(74*(int(data[4])/2+int(data[6]))/(int(data[4])/2+int(data[6])+int(data[7])))))#lostcard.paste(accbar,(170,43),mask[2])card.paste(grade[pts2grade(int(data[2]))],(207,76),grade[pts2grade(int(data[2]))])#textdraw = ImageDraw.Draw(card)drawText(draw,26,1,'#'+str(data[10])+' '+data[0][:15],(237,237,237),0,1,True,(35,35,35))drawText(draw,27,43,format(int(data[2]),',')+'pts',(237,237,237),1,1,False,(77,77,77))if data[4]!=0:drawText(draw,187,42,"%d(+%d)"%(data[4],data[5]),(237,237,237),2)drawText(draw,187,71,data[6],(237,237,237),2)drawText(draw,187,100,data[7],(237,237,237),2)if data[9]==0:drawText(draw,24,89,'??',(188,188,188),4,1,False,(77,77,77))else:drawText(draw,24,89,data[9],(188,188,188),4,1,False,(77,77,77))drawText(draw,95,85,'%.2f'%(data[3]),(237,237,237),3,1,False,(77,77,77))return card
很长,我们一点点看。
a.获得曲绘
songjpg = searchSongData(data[0],data[1],data[9])
用一个函数,用给出的信息搜索对应歌曲的曲绘。该函数如下:
def searchSongData(title,diff):for i in songdic['songs']:if i['title_localized']['en']==title:dl_str=''by_str='base.jpg'if i.get('remote_dl',False):dl_str='dl_'if diff.upper()=='BYD':by_str='3.jpg'return Image.open(os.path.join(root_path,'src','songs',dl_str+i['id'],by_str)).resize((130,130))print('没找到歌曲:%s,检查src对应版本号或数据文件'%title)return Image.new('RGB',(130,130),'#333333')
songdic
是上文加载资源的时候,json文件转成的字典。
if i.get('remote_dl',False):
需要下载的歌曲有remote_dl=True
,而不需要下载的歌曲没有对应的键值。所以这里是带默认值的获取,没有这一个键就缺省为False。
然后细心的同学发现了:诶上面明明传进来3个变量啊,怎么这里只有2个呢?
因为后来被双星人大佬指出:Arcaea中有两首Quon,一首ftr8 & byd10,另一首ftr9+,名字一模一样,所以只凭名字是无法区分这两首歌的。作为补救措施,加传了一个值,也就是打的这首歌的定数,用它来判断是哪首。因此在最终版本的代码里传了三个参。
但是加了额外的判断之后代码可读性下降了,完整的代码留到最后展示,各位知道有这么回事就行
b.卡底生成
正如整张图片是在背景上涂涂画画,这里做的小卡片同样需要一个基底。
怎么来?
我将用Image.new
方法创建一个带颜色的 390*130 矩形,赋给card
变量
if data[10]<31:r=0g=0b=0for i in range(0,20):for j in range(0,130):rgb=songjpg.getpixel((i,j))r=r+rgb[0]g=g+rgb[1]b=b+rgb[2]r=int(r/3200)g=int(g/3200)b=int(b/3200)card = Image.new('RGB',(390,130),'#'+str(hex(r))[-2:].replace('x','0')+str(hex(g))[-2:].replace('x','0')+str(hex(b))[-2:].replace('x','0'))else:card = Image.new('RGB',(390,130),'#b68f17')
这里对于Best30中的歌曲计算了曲绘中左侧20x130
像素点的平均RGB值,稍微减暗一点作为背景色。
之外的那三首使用统一的土黄色#b68f17
作为背景。
c.信息添加
接下来,先把各个图片元素粘贴上去。所有元素(图片、文字等)都对应某项信息,细的不必深究,大概看个回事就行。
card.paste(songjpg,(259,0),mask[0])card.paste(diff[data[1].upper()],(11,5),diff['BYD'])card.paste(arrowpng,(60,77),arrowpng)try:#clear statuscard.paste(badge[data[8]],(270,70),badge[data[8]])except Exception as e:passif data[4]==0:#pure,far,lostcard.paste(Image.new('RGB',(9,74),'#333333'),(170,43),mask[2])else:accbar=Image.new('RGB',(9,74),'#a55cb4')#maxaccbar.paste(Image.new('RGB',(9,74),'#794484'),(0,int(74*int(data[5])/2/(int(data[4])/2+int(data[6])+int(data[7])))))#pureaccbar.paste(Image.new('RGB',(9,74),'#FFAA11'),(0,int(74*int(data[4])/2/(int(data[4])/2+int(data[6])+int(data[7])))))#faraccbar.paste(Image.new('RGB',(9,74),'#DD4444'),(0,int(74*(int(data[4])/2+int(data[6]))/(int(data[4])/2+int(data[6])+int(data[7])))))#lostcard.paste(accbar,(170,43),mask[2])card.paste(grade[pts2grade(int(data[2]))],(207,76),grade[pts2grade(int(data[2]))])
这里在paste的时候使用到了遮罩(mask),大概是贴出比较美观的圆角矩形的最简单的方法了。后面还会用到。
遮罩用到的PNG文件提前用Photoshop做好,在加载资源的时候加载到名为masks
的List中了。
pts2grade
是一个将分数转换成对应评级的自定义函数
不一定有的元素要么打上try
,要么用if
判断一下。
以及:这里我整了点花活,即根据各个判定1的占比画一个竖直长条来反映整体精准度,即代码中的accbar
。
然后是文字部分。在这之前,我自己写了一个drawText函数用来生成带描边的文字:
def drawText(drawObj,x,y,content,color,font_num,outline=0,outlinebold=False,outlinecolor=(0,0,0)):if outline>0:if outlinebold:for i in range(1,outline+1):for j in range(1,outline+1):drawObj.text((x-i,y-j),str(content),outlinecolor,font[font_num])drawObj.text((x-i,y+j),str(content),outlinecolor,font[font_num])drawObj.text((x+i,y-j),str(content),outlinecolor,font[font_num])drawObj.text((x+i,y+j),str(content),outlinecolor,font[font_num])else:for i in range(1,outline+1):drawObj.text((x-i,y),str(content),outlinecolor,font[font_num])drawObj.text((x+i,y),str(content),outlinecolor,font[font_num])drawObj.text((x,y-i),str(content),outlinecolor,font[font_num])drawObj.text((x,y+i),str(content),outlinecolor,font[font_num])drawObj.text((x,y),str(content),color,font[font_num])
怎么画描边应该一眼就看懂了罢。然后把它用到makeCard函数中:
draw = ImageDraw.Draw(card)drawText(draw,26,1,'#'+str(data[10])+' '+data[0][:15],(237,237,237),0,1,True,(35,35,35))drawText(draw,27,43,format(int(data[2]),',')+'pts',(237,237,237),1,1,False,(77,77,77))if data[4]!=0:drawText(draw,187,42,"%d(+%d)"%(data[4],data[5]),(237,237,237),2)drawText(draw,187,71,data[6],(237,237,237),2)drawText(draw,187,100,data[7],(237,237,237),2)if data[9]==0:drawText(draw,24,89,'??',(188,188,188),4,1,False,(77,77,77))else:drawText(draw,24,89,data[9],(188,188,188),4,1,False,(77,77,77))drawText(draw,95,85,'%.2f'%(data[3]),(237,237,237),3,1,False,(77,77,77))
format(int(data[2]),',')
的效果是将数字用逗号分隔。例如10000000
变成10,000,000
,与游戏中的显示形式相同。
以及标题处非常粗暴地限制15个字符就完事了,如果要认真做的话可以加个遮罩的图片优化效果。
为什么加描边呢?其实很好理解的嘛要是不加的话有些歌是白底那么白字当然就看不清了。但是Arcaea用的字体GeosansLight
实在太细了,加了描边也不是很美观
(字本身可能就一两个像素粗,还要加描边那几乎是描边粗了)
凑合着用吧
d.完成
好那么到这里为止卡面已经完全生成了,测试的时候可以打一句card.show()
看一眼
最后的最后,返回
return card
5.全部缝合到背景
a.卡片的缝合
在主程序内打开背景,用一个循环把cards
里的卡片一张张贴上去就行了。
i
是上文遍历工作簿时使用的变量,i-2
即所有成绩的个数。
print('全部歌曲卡片生成完成,开始拼接')
bg = Image.open(os.path.join(root_path,'src','bg.jpg'))
for j in range(0,min(30,i-2)):bg.paste(cards[j],(45+395*(j%3),525+135*(j//3)),mask[1])
if i>32:for j in range(30,min(i-2,33)):bg.paste(cards[j],(45+395*(j%3),545+135*(j//3)),mask[1])
坐标都提前量好,然后测试的时候来回看几眼调整,八九不离十。
这里的做法是 :
- 如果成绩数不到30项,那么全贴就行了
- 如果成绩数大于30,那么竖直方向隔开20px,再把第31~33项成绩(若有)贴上去
b.文字信息(玩家信息)的缝合
需要的玩家信息之前没有拿,现场获取一下。
主程序内如下代码:
try:playerData=getPlayerData(sheet['player'])
except Exception as e:playerData=['Unknown Player',0.00,b30/30,0.00,b30max/10]
stitchBg(bg, playerData)
依然希望能兼容常规方法导出的文件,因此做了一下默认参数。
getPlayerData
返回List形式的玩家数据,代码很简单就留到最后了。
再传递给stitchBg
函数:
def stitchBg(bg,playerdata):draw = ImageDraw.Draw(bg)bg.paste(ptt2image(playerdata[1]),(58,50),ptt2image(playerdata[1]))if playerdata[1]==0:width = get_font_render_size(font[5],'--.--')[0]drawText(draw,158-width//2,102,'--.--',(237,237,237),5,5,True,(63,63,63))else:width = get_font_render_size(font[5],str(playerdata[1]))[0]drawText(draw,148-width//2,102,str(playerdata[1]),(237,237,237),5,5,True,(63,63,63))drawText(draw,277,88,playerdata[0],(237,237,237),7,5,True,(63,63,63))drawText(draw,642,224,'Best30:',(237,237,237),8)drawText(draw,642,335,'Recent10:',(237,237,237),8)drawText(draw,642,442,'MaxPossible:',(237,237,237),8)drawText(draw,1010,224,'%.5f'%playerdata[2],(237,237,237),8)drawText(draw,1010,335,'%.5f'%playerdata[3],(237,237,237),8)drawText(draw,1010,442,'%.5f'%playerdata[4],(237,237,237),8)
这里的if
依然是做默认值。
ptt2image
返回当前玩家潜力值对应的图片背景。
原理是一样的,只是累一点,没啥技术含量
6.保存
已经来到最后一步了。写一个保存的函数savePic
:
def savePic(pic, path):try:pic.save(os.path.join(path,'output.png'),'PNG')except Exception as e:print('保存到此路径失败:%s'%path)exit(1)print('成功保存:%s'%os.path.join(path,'output.png'))
然后在主程序里调用就可以了。
也可以顺手写一个bg.show()
放出预览。但是这一方法是不能用于保存图片的,show的一瞬间生成的临时图片会马上被删除。
效果展示
我的个人B30图:
可以看到,最上面是个人潜力值和玩家名显示,以及潜力值的 详细数值2 显示。接着是所有成绩的排列,一张卡片从上到下,从左到右依次显示的信息是:
- 排名和歌曲名
- 分数
- 定数 -> 成绩定数
- Pure,Far,Lost的判定数显示
- 成绩评级
- 通关类型
以及我某位双星人好友的导出图~~(展示未经同意)~~(逃
拜他所赐发现了Quon的漏洞
以及不含详细信息,只有B30成绩的导出效果:
总结
还好上次留了笔记啊不然现在就算想复制粘贴一下子都看不懂了www
总之非常感谢所有人
以及再次默默感谢在线查询服务的提供者,提醒各位且查且珍惜。
附录:Python源码
import json
import tkinter.filedialog
import os
import sys
from PIL import Image, ImageFont, ImageDraw
import openpyxl
root_path = os.path.dirname(os.path.realpath(sys.argv[0]))#这是程序所处的位置
font=[]
badge={}
diff={}
grade={}
mask=[]
rating=[]def checkSrc():if not os.path.isdir(os.path.join(root_path, 'src')):return True
def loadRes(srcpath):print('开始加载资源,请稍候')global songdictry:#songlist打开filejson = open(os.path.join(srcpath,'songs','songlist'), 'r', encoding='UTF-8')songdic=json.load(filejson)filejson.close()#src/fontsfont.append(ImageFont.truetype(os.path.join(srcpath,'fonts','score.ttf'), 30))#标题font.append(ImageFont.truetype(os.path.join(srcpath,'fonts','score.ttf'), 24))#ptsfont.append(ImageFont.truetype(os.path.join(srcpath,'fonts','score.ttf'), 18))#pure,far,lostfont.append(ImageFont.truetype(os.path.join(srcpath,'fonts','ptt.ttf'), 30))#结果pttfont.append(ImageFont.truetype(os.path.join(srcpath,'fonts','ptt.ttf'), 24))#定数font.append(ImageFont.truetype(os.path.join(srcpath,'fonts','ptt.ttf'), 72))font.append(ImageFont.truetype(os.path.join(srcpath,'fonts','ptt.ttf'), 60))font.append(ImageFont.truetype(os.path.join(srcpath,'fonts','ptt.ttf'), 100))font.append(ImageFont.truetype(os.path.join(srcpath,'fonts','ptt.ttf'), 50))#src/png/cardbadge['Easy Clear']=Image.open(os.path.join(srcpath,'png/card/badge/ez.png')).resize((60,47))badge['Normal Clear']=Image.open(os.path.join(srcpath,'png/card/badge/nm.png')).resize((60,47))badge['Hard Clear']=Image.open(os.path.join(srcpath,'png/card/badge/hd.png')).resize((60,47))badge['Full Recall']=Image.open(os.path.join(srcpath,'png/card/badge/fr.png')).resize((60,47))badge['Pure Memory']=Image.open(os.path.join(srcpath,'png/card/badge/pm.png')).resize((60,47))badge['Track Lost']=Image.open(os.path.join(srcpath,'png/card/badge/fail.png')).resize((60,47))diff['BYD']=Image.open(os.path.join(srcpath,'png/card/diff/byd.png'))diff['FTR']=Image.open(os.path.join(srcpath,'png/card/diff/ftr.png'))diff['PRS']=Image.open(os.path.join(srcpath,'png/card/diff/prs.png'))diff['PST']=Image.open(os.path.join(srcpath,'png/card/diff/pst.png'))grade['EX+']=Image.open(os.path.join(srcpath,'png/card/grade/ex+.png')).resize((72,35))grade['EX']=Image.open(os.path.join(srcpath,'png/card/grade/ex.png')).resize((72,35))grade['AA']=Image.open(os.path.join(srcpath,'png/card/grade/aa.png')).resize((72,35))grade['A']=Image.open(os.path.join(srcpath,'png/card/grade/a.png')).resize((72,35))grade['B']=Image.open(os.path.join(srcpath,'png/card/grade/b.png')).resize((72,35))grade['C']=Image.open(os.path.join(srcpath,'png/card/grade/c.png')).resize((72,35))grade['D']=Image.open(os.path.join(srcpath,'png/card/grade/d.png')).resize((72,35))rating.append(Image.open(os.path.join(srcpath,'png/ptt/rating_0.png')).resize((200,200)))rating.append(Image.open(os.path.join(srcpath,'png/ptt/rating_1.png')).resize((200,200)))rating.append(Image.open(os.path.join(srcpath,'png/ptt/rating_2.png')).resize((200,200)))rating.append(Image.open(os.path.join(srcpath,'png/ptt/rating_3.png')).resize((200,200)))rating.append(Image.open(os.path.join(srcpath,'png/ptt/rating_4.png')).resize((200,200)))rating.append(Image.open(os.path.join(srcpath,'png/ptt/rating_5.png')).resize((200,200)))rating.append(Image.open(os.path.join(srcpath,'png/ptt/rating_6.png')).resize((200,200)))rating.append(Image.open(os.path.join(srcpath,'png/ptt/rating_off.png')).resize((200,200)))global arrowpngarrowpng=Image.open(os.path.join(srcpath,'png/card/arrow.png')).resize((41,55))#src/png/maskmask.append(Image.open(os.path.join(srcpath,'png/mask/songmask_130px.png')))mask.append(Image.open(os.path.join(srcpath,'png/mask/cardmask_390x130.png')))mask.append(Image.open(os.path.join(srcpath,'png/mask/accbarmask_9x74.png')))#src/png/pttfor i in ['0','1','2','3','4','5','6','off']:rating.append(Image.open(os.path.join(srcpath,'png/ptt/rating_'+i+'.png')))#Done.print('100%')except Exception as e:print('加载资源出错,详细信息:')print(e)exit(1)
def requirePath():global filenameglobal savepathprint('Select a xlsx file:')filename = tkinter.filedialog.askopenfilename(title = '选择文件', filetypes=[('Excel工作簿','.xlsx'), ('Excel启用宏的工作簿','.xlsm'), ('Excel2003工作簿','.xls'), ('所有文件','.*')])if not os.path.isfile(filename):print('无效的文件名: %s'%filename)return -1print('filename = %s'%filename)print('Determine save path:')savepath = tkinter.filedialog.askdirectory(title = '选择保存路径')if not os.path.isdir(savepath):print('无效的保存地址: %s'%savepath)return -1print('savepath = %s'%savepath)return 0
def openWorkbook(path):global sheetsheet={}try:book = openpyxl.load_workbook(path)except Exception as e:print('加载工作簿出错,详细信息:')print(e)exit(1)try:sheet['song'] = book['Song']except Exception as e:print('找不到数据,确保主表名为Song')exit(1)try:sheet['player'] = book['Player']except Exception as e:passdef searchSongData(title,diff,rate=0):quonflag=Truefor i in songdic['songs']:if i['title_localized']['en']==title:if title=='Quon' and quonflag:if rate<9 or rate>=10:quonflag=Falsecontinuedl_str=''by_str='base.jpg'if i.get('remote_dl',False):dl_str='dl_'if diff.upper()=='BYD':by_str='3.jpg'try:return Image.open(os.path.join(root_path,'src','songs',dl_str+i['id'],by_str)).resize((130,130))except Exception as e:if diff.upper()=='BYD':by_str='base.jpg'return Image.open(os.path.join(root_path,'src','songs',dl_str+i['id'],by_str)).resize((130,130))print('没找到歌曲:%s,检查src对应版本号或数据文件'%title)return Image.new('RGB',(130,130),'#333333')
def pts2grade(score):if score>=9900000:return 'EX+'elif score>=9800000:return 'EX'elif score>=9500000:return 'AA'elif score>=9200000:return 'A'elif score>=8900000:return 'B'elif score>=8600000:return 'C'else:return 'D'
def drawText(drawObj,x,y,content,color,font_num,outline=0,outlinebold=False,outlinecolor=(0,0,0)):if outline>0:if outlinebold:for i in range(1,outline+1):for j in range(1,outline+1):drawObj.text((x-i,y-j),str(content),outlinecolor,font[font_num])drawObj.text((x-i,y+j),str(content),outlinecolor,font[font_num])drawObj.text((x+i,y-j),str(content),outlinecolor,font[font_num])drawObj.text((x+i,y+j),str(content),outlinecolor,font[font_num])else:for i in range(1,outline+1):drawObj.text((x-i,y),str(content),outlinecolor,font[font_num])drawObj.text((x+i,y),str(content),outlinecolor,font[font_num])drawObj.text((x,y-i),str(content),outlinecolor,font[font_num])drawObj.text((x,y+i),str(content),outlinecolor,font[font_num])drawObj.text((x,y),str(content),color,font[font_num])
def makeCard(data):songjpg = searchSongData(data[0],data[1],data[9])if data[10]<31:r=0g=0b=0for i in range(0,20):for j in range(0,130):rgb=songjpg.getpixel((i,j))r=r+rgb[0]g=g+rgb[1]b=b+rgb[2]r=int(r/3200)g=int(g/3200)b=int(b/3200)card = Image.new('RGB',(390,130),'#'+str(hex(r))[-2:].replace('x','0')+str(hex(g))[-2:].replace('x','0')+str(hex(b))[-2:].replace('x','0'))else:card = Image.new('RGB',(390,130),'#b68f17')card.paste(songjpg,(259,0),mask[0])card.paste(diff[data[1].upper()],(11,5),diff['BYD'])card.paste(arrowpng,(60,77),arrowpng)try:#clear statuscard.paste(badge[data[8]],(270,70),badge[data[8]])except Exception as e:passif data[4]==0:#pure,far,lostcard.paste(Image.new('RGB',(9,74),'#333333'),(170,43),mask[2])else:accbar=Image.new('RGB',(9,74),'#a55cb4')#maxaccbar.paste(Image.new('RGB',(9,74),'#794484'),(0,int(74*int(data[5])/2/(int(data[4])/2+int(data[6])+int(data[7])))))#pureaccbar.paste(Image.new('RGB',(9,74),'#FFAA11'),(0,int(74*int(data[4])/2/(int(data[4])/2+int(data[6])+int(data[7])))))#faraccbar.paste(Image.new('RGB',(9,74),'#DD4444'),(0,int(74*(int(data[4])/2+int(data[6]))/(int(data[4])/2+int(data[6])+int(data[7])))))#lostcard.paste(accbar,(170,43),mask[2])card.paste(grade[pts2grade(int(data[2]))],(207,76),grade[pts2grade(int(data[2]))])#textdraw = ImageDraw.Draw(card)drawText(draw,26,1,'#'+str(data[10])+' '+data[0][:15],(237,237,237),0,1,True,(35,35,35))drawText(draw,27,43,format(int(data[2]),',')+'pts',(237,237,237),1,1,False,(77,77,77))if data[4]!=0:drawText(draw,187,42,"%d(+%d)"%(data[4],data[5]),(237,237,237),2)drawText(draw,187,71,data[6],(237,237,237),2)drawText(draw,187,100,data[7],(237,237,237),2)if data[9]==0:drawText(draw,24,89,'??',(188,188,188),4,1,False,(77,77,77))else:drawText(draw,24,89,data[9],(188,188,188),4,1,False,(77,77,77))drawText(draw,95,85,'%.2f'%(data[3]),(237,237,237),3,1,False,(77,77,77))return card
def getPlayerData(sheet):data=[]for i in range(0,5):data.append(sheet.cell(i+1,2).value)return data
def get_font_render_size(font,content):canvas=Image.new('RGB',(1024,1024))draw=ImageDraw.Draw(canvas)draw.text((0,0),content,(255,255,255),font)bbox=canvas.getbbox()size=(bbox[2]-bbox[0],bbox[3]-bbox[1])return size
def ptt2image(ptt):if ptt>=12.5:return rating[6]elif ptt>=12:return rating[5]elif ptt>=11:return rating[4]elif ptt>=10:return rating[3]elif ptt>=7:return rating[2]elif ptt>=3.5:return rating[1]elif ptt>0:return rating[0]else:return rating[7]
def stitchBg(bg,playerdata):draw = ImageDraw.Draw(bg)bg.paste(ptt2image(playerdata[1]),(58,50),ptt2image(playerdata[1]))if playerdata[1]==0:width = get_font_render_size(font[5],'--.--')[0]drawText(draw,158-width//2,102,'--.--',(237,237,237),5,5,True,(63,63,63))else:width = get_font_render_size(font[5],str(playerdata[1]))[0]drawText(draw,148-width//2,102,str(playerdata[1]),(237,237,237),5,5,True,(63,63,63))drawText(draw,277,88,playerdata[0],(237,237,237),7,5,True,(63,63,63))drawText(draw,642,224,'Best30:',(237,237,237),8)drawText(draw,642,335,'Recent10:',(237,237,237),8)drawText(draw,642,442,'MaxPossible:',(237,237,237),8)drawText(draw,1010,224,'%.5f'%playerdata[2],(237,237,237),8)drawText(draw,1010,335,'%.5f'%playerdata[3],(237,237,237),8)drawText(draw,1010,442,'%.5f'%playerdata[4],(237,237,237),8)
def savePic(pic, path):try:pic.save(os.path.join(path,'output.png'),'PNG')except Exception as e:print('保存到此路径失败:%s'%path)exit(1)print('成功保存:%s'%os.path.join(path,'output.png'))#def Main():
root = tkinter.Tk()
root.withdraw()
#打开文件
if checkSrc():print('目录下没有src文件夹,请查看:%s'%os.path.join(root_path, 'src'))exit(1)
loadRes(os.path.join(root_path, 'src'))
if requirePath()<0:exit(1)
openWorkbook(filename)
i=2
cards=[]
b30=0.0
b30max=0.0
while not sheet['song'].cell(i,1).value == None and i<=34:print('歌曲#%d生成...'%(i-2))temp=[]for j in range(1,11):if j>=5 and j<=9 and sheet['song'].cell(i,j).value==None:temp.append(0)else:temp.append(sheet['song'].cell(i,j).value)temp.append(i-1)cards.append(makeCard(temp))if i<=31:b30=b30+temp[3]if i<=11:b30max=b30max+temp[3]i=i+1
print('全部歌曲卡片生成完成,开始拼接')
bg = Image.open(os.path.join(root_path,'src','bg.jpg'))
for j in range(0,min(30,i-2)):bg.paste(cards[j],(45+395*(j%3),525+135*(j//3)),mask[1])
if i>32:for j in range(30,min(i-2,33)):bg.paste(cards[j],(45+395*(j%3),545+135*(j//3)),mask[1])
try:playerData=getPlayerData(sheet['player'])
except Exception as e:playerData=['Unknown Player',0.00,b30/30,0.00,b30max/10]
stitchBg(bg, playerData)
bg.show()
savePic(bg, savepath)root.destroy()
音游术语,玩家打击精准度不同会得到不同的判定。在Arcaea中有四个判定等级,即Pure, Far, Lost;Pure下面还有MaxPure(俗称大P)和Pure(俗称小P) ↩︎
依次:Best30即B30所有成绩的平均值,Recent10即最近游玩的10次成绩的平均值,此两者计算得出潜力值;MaxPossible是不更新最高分的情况下玩家潜力值的最大值,即R10取B30最高十项时潜力值的值。只有B30平均值可以自己计算得出,R10在缺失时会显示为0.00。 ↩︎
再战ArcaeaB30生成器:Python模块PIL实战图像处理与拼接相关推荐
- python log函数_求你别再花大价钱学 Python 之爬虫实战
引子 Python 基本概念 Python 优势和劣势 优势 Python 的劣势 Python 安装设置 Python 基本语法 程序例子 Python 基本语法 Python 爬虫实现 爬虫相关 ...
- 《零基础掌握 Python 入门到实战》笔记
Python 零基础掌握 Python 入门到实战笔记 文章目录 Python 内置对象类型 基本交互语句 常用内置函数 整数与浮点数 基本数学运算 高级数学运算 字符串 序列 索引 切片 成员函数 ...
- python图像等比例压缩_python使用pil进行图像处理(等比例压缩、裁剪)实例代码
PIL中设计的几个基本概念 1.通道(bands):即使图像的波段数,RGB图像,灰度图像 以RGB图像为例: >>>from PIL import Image >>&g ...
- python压缩图片像素_python使用pil进行图像处理(等比例压缩、裁剪)实例代码
PIL中设计的几个基本概念 1.通道(bands):即使图像的波段数,RGB图像,灰度图像 以RGB图像为例: 2.模式(mode):定义了图像的类型和像素的位宽.共计9种模式: 3.尺寸(size) ...
- Python图片处理模块PIL(pillow)
Python图片处理模块PIL(pillow) 本篇包含:一.Image类的属性:1.Format 2.Mode 3.Size 4.Palette 5.Info ...
- python django web典型模块开发实战_带你读《Python Django Web典型模块 开发实战》之一:从新浪微博聊起多端应用-阿里云开发者社区...
Python Django Web典型模块 开发实战 点击查看第二章 点击查看第三章 寇雪松 编著 第1章 从新浪微博聊起多端应用 当人们听到"新浪",脑海里第一个浮现的关联词是& ...
- python pil_使用Python的PIL模块来进行图片对比
在使用google或者baidu搜图的时候会发现有一个图片颜色选项,感觉非常有意思,有人可能会想这肯定是人为的去划分的,呵呵,有这种可能,但是估计人会累死, 开个玩笑,当然是通过机器识别的,海量的图片 ...
- PDF转图片再转长图、python、pil
PDF转图片再转长图.python.pil 直接贴代码 运行环境 直接贴代码 # -*- coding: utf-8 -*- """ 1.安装库pip install p ...
- python的PIL库部分模块函数
python的PIL库部分模块函数 1.Image.open("文件路径") 打开图片文件 1.image.convert(mode) 将其转换为某模式 2.Image.new(模 ...
最新文章
- 【干货】Dask快速搭建分布式集群(大数据0基础可以理解,并使用!)
- 【转】汇编 代码段数据段堆区栈区
- linux 普通用户登陆系统su - root的时候报错su: Authentication failure
- SQL Cookbook:一、检索记录(13)按模式搜索
- python实现推荐系统代码_推荐系统之矩阵分解及其Python代码实现
- Python入门--字典元素的操作,key的判断(in not in),字典元素删除(del),字典元素的增加,清空(clear()),修改
- KMP原理及使用的再总结
- [ScyllaHide] 05 ScyllaHide的Hook原理
- html5选择时间,科技常识:HTML5新控件之日期和时间选择输入的实现代码
- 【Python密度泛函理论】
- social network 学习心得
- mysql 小于转义_MyBatis中大于和小于号的转义写法
- 苹果手机有护眼模式吗_调节手机明暗度能起到护眼模式一样的效果?
- 谈谈对 Database Plus 认识与畅想
- 5G室内定位来了,化工厂人员定位,电厂室内定位都有用它!-新导智能
- 2023中国传媒大学计算机考研信息汇总
- 新款H3C服务器图形化界面配置raid
- 基于深度神经网络的中药材识别
- 微信小游戏egret.getDefinitionByName不能获取类的实例
- java 第三方登录之新浪微博登录
热门文章
- Flink Hadoop Compatibility
- SQL server 删除某行语句
- 实木地板安装需要注重细节
- SLAM综述阅读笔记七:Visual and Visual-Inertial SLAM: State of the Art, Classification,and Experimental 2021
- Struts2学习笔记总结
- [Matlab] 界面布局最牛篇
- 3.3 SPI串行Flash配置模式
- 面向过程和面向对象程序设计的区别
- Excel汉语全拼函数,批量获得汉字的拼音
- 计算机应用基础2007,计算机应用基础(Windows XP+Office 2007)