相信對 NBA 感興趣的大兄弟一定不會對球星卡陌生吧,雖然不知道我們們這個圈子對 NBA 感興趣的大兄弟多不多。但是,不感興趣也問題不大,本文闡述的方法其實是通用的圖片合成方法。
讓我們來看一張球星卡:
這種球星卡可以劃分為5個部分
- 球員動作圖
- 球員姓名
- 球隊logo
- 底板背景圖
- 裝飾邊框圖
今天我們要乾的事情就是找到這5個素材然後用 Python 把他們組合起來,那麼這個時候肯定有大兄弟會有疑問了,直接用 PS 套起來不就好了嗎,講道理這樣做確實方便快捷,但是前提是你只做這一張卡,當你要為聯盟大概450名球員製作球星卡的時候,你就需要一個指令碼來幫助你完成了(對 PS 不太熟,如果 PS 也可以,可以告訴我哈)。
這篇文章需要一點 Python 基礎,完全不瞭解 Python 的大兄弟最好去學習一點基礎知識再看。
OK 讓我們開始吧!
準備素材
就像開頭提到的,我們需要5種素材。這5種素材我都會提供若干個給大家練手。
上面的圖片實際上只有4個素材,還有一個就是球員的名字了,球員的名字我們可以在組合過程中使用 ImageDraw 和 ImageFont 載入球員姓名。
為了避免字型路徑和中文亂碼的問題,我還提供了一個微軟雅黑的字型。
素材可以在 這裡 clone 或者下載,宣告:本文所有涉及的素材和圖片僅供交流學習使用。
開始寫程式碼
我們的場景是為聯盟中的所有球員製作球星卡,那麼所有的球員自然是從資料庫裡面查出來的了,這裡為了練習,我們可以 mock 一些資料(雖然,講道理,波什並不能放在 SUPER 裡面,但是這裡只有一張裝飾邊框圖,所以就勉為其難的和吾皇放在一個等級了)。
mock_data = [
{
'id': 1966,
'cn_name': '勒布朗-詹姆斯',
'team_id': 5,
'category': 'SUPER'
},
{
'id': 1977,
'cn_name': '克里斯-波什',
'team_id': 14,
'category': 'SUPER'
}
]複製程式碼
有資料之後,我們就來遍歷這些球員,找到我們需要的屬性,再傳入到一個組合函式中。
def compose_all(all):
for player in all:
id = player['id']
# if id == 1966:
if True:
category = player['category']
player_img = str(id) + '.png'
team_id = player['team_id']
team_img = str(team_id) + '.png'
name = player['cn_name']
category = player_category.index(category) + 1
category_img = 'card_bg_' + str(category) + '.png'
output_name = str(id) + '.png'
print('start compose ' + str(id))
compose(player_img, name, team_img, category_img, output_name)複製程式碼
這裡有個個人習慣,因為經常在伺服器上寫一些指令碼,所有if True:
那個地方就是除錯用的,當一個球員除錯沒問題之後,註釋掉,跑程式碼,這樣可以不用再調整縮排了,不知道其他的大兄弟這個地方喜歡怎麼寫。
在這裡我預設會對球員分檔(根據一些資料資訊)
player_category = ["SUPER", "CORE", "BLUE", "SIX", "BENCH"]複製程式碼
對應檔位的裝飾邊框分別為card_bg_1.png
,card_bg_2.png
等。
檢查5個素材是否都拿到了:
- 球員動作圖 -> player_img
- 球員姓名 -> name
- 球隊logo -> team_img
- 底板背景圖 —> None
- 裝飾邊框圖 -> card_bg_n.png (n 對應檔位)
還差一個底板背景圖,因為每個球員底板背景圖都一樣,所以在組合函式中直接使用就好了。
在我們去組合球星卡之前,還有一個問題需要解決,那就是我們不能保證所有素材都在同一個目錄下,那麼我們就需要給每個素材指定一個目錄,這樣我們在組合球星卡的時候就可以一馬平川了。
team_path = './logo/'
player_path = './player_img/'
output_path = './trading_cards/'
font_file = './assets/msyh.ttf'
card_decorate_path = './assets/'複製程式碼
設定好路徑之後寫上我們的組合函式,為了保證這個函式的正常執行,我們需要匯入三個模組。
import os
import numpy as np
from PIL import Image, ImageFont, ImageDraw複製程式碼
如果提示沒有找到模組,請使用下面的命令進行安裝
pip install Pillow
pip install numpy複製程式碼
Pillow 關於圖片處理的詳細文件請參考 Pillow
下面是我們的組合函式
def compose(player_img, name, team_logo, category_img, output_name):
card_bg = card_decorate_path + 'bg.png'
player_img_offset_height = 15
if not os.path.isfile(player_path + player_img):
need_manual_compose.append(player_img)
print(player_path + player_img + ' is not exist')
return
player_img = Image.open(player_path + player_img).convert('RGBA')
bg_img = Image.open(card_decorate_path + category_img).convert('RGBA')
card_bg_img = Image.open(card_bg).convert('RGBA')
logo = Image.open(team_path + team_logo).convert('RGBA')
logo = logo.resize((100,100), Image.ANTIALIAS)
card_bg_img.paste(player_img, (35,player_img_offset_height), player_img)
card_bg_img.paste(bg_img, (0,0), bg_img)
card_bg_img.paste(logo, (95,315), logo)
font = ImageFont.truetype(font_file, 20)
d = ImageDraw.Draw(card_bg_img)
try:
name = unicode(name, 'utf-8')
except NameError:
name = name
d.text((12, 12), name, font=font, fill=(255,255,255))
card_bg_img.save(output_path + output_name, quality=100)複製程式碼
有幾個問題需要說明一下:
- 有些球星動作的素材可能找不到,那麼就將找不到的球員記錄下來,最後手工處理。
- 因為我們要使用到 alpha 通道,所以需要 convert('RGBA')
- 在圖片 paste 之前必須保證團片和貼上範圍畫素一樣,不一樣的話就使用 resize 函式變成一樣的,Image.ANTIALIAS 引數的作用是抗鋸齒,這樣 resize 出來的圖片邊緣會更圓潤。
- b 圖片貼在 a 圖片上,使用 a.paste(b, (x,y), b),(x,y) 為左上角的座標,第三個引數 b 是作為 mask,如果不使用這個引數會導致 b 圖片透明的部分也覆蓋在 a 上面。
- 這個程式在 python2 和 python3 上都可以執行。
OK,讓我們來看一看結果怎麼樣吧
恩,似乎還不錯,但是大家會發現波什的手沒了,所以說一馬平川什麼的都是騙人的。
經過我個人的觀察,會發現大部分的球星動作圖都是和詹姆斯類似的(即球員的動作在圖片中的位置是靠下的),如果下移貼上座標會導致球星卡的主要局域出現大面積的空白。一計不成,再生一計,我們可以對類似波什的動作圖做特殊處理,下移他們的貼上座標就可以了。
ok,問題來了,人眼一看就會知道哪個動作圖高哪個動作圖低,那麼 Python 怎麼才能知道呢?
可以看到,每張動作圖的大小是一樣的,但是具體的動作在圖片中的分佈是不一樣的。
這個時候我們就需要numpy
這個庫來幫助我們把圖片轉換成畫素矩陣,然後我們對矩陣進行逐行掃描並記錄有效畫素出現的位置,這樣就可以判斷哪些動作圖是偏高的。
def calculateUsefulHeight(img):
img = Image.open(player_path + img).convert('RGBA')
w, h = img.size
mat = np.array(img)
for i in range(mat.shape[0]):
if not allEqual(mat[i]):
return h - i
def allEqual(line):
w = len(line)
if not w:
return True
init_value = line[0][3]
step = 10
for i in range(int(round(w/step))):
if line[i * step][3] == init_value:
continue
else:
return False
return True複製程式碼
然後再在組合函式中加入對動作圖高的特殊處理的程式碼就可以了。
def compose(player_img, name, team_logo, category_img, output_name):
...
# deal with high player image
h = calculateUsefulHeight(player_img)
# 這個地方的310是球星卡展示球員動作的最大高度
if h > 310:
offset = h - 310
player_img_offset_height += offset複製程式碼
再來跑一遍,看看效果如何。
不錯,這樣就可以了,尤其是一瞬間跑出來 450 張看起來效果還不錯的球星卡還是非常的爽的。
OK了,到這裡應該就可以結束了,原始碼可以在 這裡 得到,裡面包含本文所有涉及的圖片,素材和程式碼。
如果各位大兄弟,有更好的設計和佈局也歡迎和我交流。