带你实现TexturePacker中的网格打包功能
现在手头上有一堆相同大小的图片,如下图所示:
我想将它们打包进一张大图片中,主要是便于资源管理。本着能偷懒就偷懒的原则,看看市面上有没有类似功能的软件,果然,找到了一款名为TexturePacker的软件。但很可惜,这个功能是收费的。毕竟需求比较简单,还是自己动手,丰衣足食,试着用Python实现下吧。
1. 图片的读取与保存
在Python中,我们可以使用pillow库处理图片,例如读写图片;此外,可以使用numpy将读取的图片转化为numpy数组,这样处理起来会很方便。为此,首先运行pip install numpy
和pip install pillow
安装这两个库。安装完成后,导入它们:
import numpy as np
from PIL import Image
首先实现图片的读取功能:指定图片的路径,将其转化为numpy数组:
def read_image_as_numpy(image_path:str) -> np.ndarray:
"""读取一张图片,并转化为numpy数组
Args:
image_path: 图片路径
"""
image = Image.open(image_path)
arr = np.array(image, dtype=np.uint8)
return arr
接下来是图片的保存功能:将numpy数组保存为图片:
def save_numpy_as_image(arr: np.ndarray, save_path:str):
"""将numpy数组保存为图片
Args:
arr: numpy数组
save_path: 保存图片的路径
"""
image = Image.fromarray(arr)
image.save(save_path)
2. 图片的打包
将小图片合并很简单,无非是数组的切片操作。现在的问题是,如果有21张图片,我们可以将其打包为3x7的大图片;但如果有19张呢?可以有不同的选择,例如4x5或是3x7, 这样分别会产生1个或2个位置的浪费。
当然,可以让用户决定按照怎样的大小打包。我想了想自己的需求:打包后的图片,水平方向上最好有5~8个网格,而且浪费的内存越少越好。从而,实现一个函数,给定图片的张数n,找到满足要求的分解大小:
def best_size(n:int) -> tuple[int,int]:
"""为正整数n找到最佳的分解大小"""
if n <= 8:
return 1, n
else:
for t in [8, 7, 6, 5]:
if n % t == 0:
return n // t, t
best_w, best_h = n // 8 + 1, 8
for t in [7, 6, 5]:
w, h = n // t + 1, t
if best_w * best_h > w * h:
best_w, best_h = w, h
return best_w, best_h
接下来就是为每张小图片找到它在大图片中的位置(这里假设处理的都是png图片):
def grid_packer(dir_path:str):
"""网格打包
Args:
dir_path: 需要打包的文件夹路径
"""
# 1. 加载文件夹中的png图片(要求所有图片必须大小相同)
image_arrs = [
read_image_as_numpy(os.path.join(dir_path, filename))
for filename in os.listdir(dir_path) if filename.endswith('.png')
]
# 2. 计算打包后的最佳大小
best_x, best_y = best_size(len(image_arrs))
# 3. 将小图片写入大图片中对应的位置
image_w, image_h, image_channel = image_arrs[0].shape
target_image = np.zeros((best_x * image_w, best_y * image_h, image_channel), dtype=np.uint8)
for i, image_arr in enumerate(image_arrs):
i_x, i_y = i // best_y, i % best_y
target_image[i_x*image_w:(i_x+1)*image_w, i_y*image_h:(i_y+1)*image_h, :] = image_arr
save_numpy_as_image(target_image, f'{os.path.basename(dir_path).lower()}_{best_x}x{best_y}_{len(image_arrs)}.png')
大功告成,最终效果如下图所示:
完整代码如下:
grid_packer.py
import sys, os
import numpy as np
from PIL import Image
def read_image_as_numpy(image_path:str) -> np.ndarray:
"""读取一张图片,并转化为numpy数组
Args:
image_path: 图片路径
"""
image = Image.open(image_path)
arr = np.array(image, dtype=np.uint8)
return arr
def save_numpy_as_image(arr: np.ndarray, save_path:str):
"""将numpy数组保存为图片
Args:
arr: numpy数组
save_path: 保存图片的路径
"""
image = Image.fromarray(arr)
image.save(save_path)
def best_size(n:int) -> tuple[int,int]:
"""为正整数n找到最佳的分解大小"""
if n <= 8:
return 1, n
else:
for t in [8, 7, 6, 5]:
if n % t == 0:
return n // t, t
best_w, best_h = n // 8 + 1, 8
for t in [7, 6, 5]:
w, h = n // t + 1, t
if best_w * best_h > w * h:
best_w, best_h = w, h
return best_w, best_h
def grid_packer(dir_path:str):
"""网格打包
Args:
dir_path: 需要打包的文件夹路径
"""
image_arrs = [
read_image_as_numpy(os.path.join(dir_path, filename))
for filename in os.listdir(dir_path) if filename.endswith('.png')
]
best_x, best_y = best_size(len(image_arrs))
image_w, image_h, image_channel = image_arrs[0].shape
target_image = np.zeros((best_x * image_w, best_y * image_h, image_channel), dtype=np.uint8)
for i, image_arr in enumerate(image_arrs):
i_x, i_y = i // best_y, i % best_y
target_image[i_x*image_w:(i_x+1)*image_w, i_y*image_h:(i_y+1)*image_h, :] = image_arr
save_numpy_as_image(target_image, f'{os.path.basename(dir_path).lower()}_{best_x}x{best_y}_{len(image_arrs)}.png')
if __name__ == '__main__':
if len(sys.argv) != 2:
print('usage: packer [directory path]')
elif not (os.path.exists(sys.argv[1]) and os.path.isdir(sys.argv[1])):
print(f'error: {sys.argv[1]} does not exist or is not a directory')
else:
grid_packer(sys.argv[1])