带你实现TexturePacker中的网格打包功能

现在手头上有一堆相同大小的图片,如下图所示:

我想将它们打包进一张大图片中,主要是便于资源管理。本着能偷懒就偷懒的原则,看看市面上有没有类似功能的软件,果然,找到了一款名为TexturePacker的软件。但很可惜,这个功能是收费的。毕竟需求比较简单,还是自己动手,丰衣足食,试着用Python实现下吧。

1. 图片的读取与保存

在Python中,我们可以使用pillow库处理图片,例如读写图片;此外,可以使用numpy将读取的图片转化为numpy数组,这样处理起来会很方便。为此,首先运行pip install numpypip 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])

 

posted @ 2024-05-25 13:39  overxus  阅读(33)  评论(0编辑  收藏  举报