You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

优化修改PIL Image的代码:Numpy、多进程、GPU方案探讨

游戏地图制作程序性能优化方案

问题背景

我近期开发了一款游戏地图制作程序,游戏地图被划分为多个“Province(省份)”,由provinces.bmp文件内的同色像素组定义。程序中有三类基于PIL Image、供tkinter展示的操作存在明显性能瓶颈:

  • 加载阶段:需按颜色将每个像素存入对应Province对象,当前通过遍历全部像素实现(基础游戏地图为5000×2000,共1000万次遍历);
  • 切换MapMode(地图模式):需基于省份的文化、所有者等信息重新上色,当前需遍历每个省份的全部像素;
  • 省份数据更新:用户修改省份数据后,需重新遍历该省份像素上色并更新tkinter展示。

最初考虑用Python的CUDA库,但担忧无法适配现有数据结构,且大量Province对象的像素集合难以部署,现寻求最优优化方案。

附带最小可复现示例代码:

from random import randint
import tkinter as tk
from PIL import Image
import numpy
from PIL import ImageTk
from sys import stdout

class Province:
    def __init__(self, id, colour, resource):
        self.id = id
        self.colour = colour
        self.pixels = []
        self.resource = resource

def updateProvinceOnMap(mapImage, province, resourcesToColour):
    colour = resourcesToColour[province.resource]
    for pixel in province.pixels:
        mapImage.putpixel(pixel, colour)

if __name__ == "__main__":
    colourToProvinces = dict()
    provinces = []
    provinceMapImage = Image.open("../TestMod/map/provinces.bmp")
    provinceMapArray = numpy.array(provinceMapImage)
    resourcesToColour = {"Livestock": (0, 255, 0), "Iron": (96, 96, 96), "Gold": (255, 255, 0), "Wood": (102, 51, 0)}
    resources = ["Livestock", "Iron", "Gold", "Wood"]
    resourceCount = 4
    idCounter = 1

    # Issue 1: Populating the provinces from the image
    print("Populating Provinces")
    stdout.flush()
    for y in range(0, len(provinceMapArray)):
        for x in range(0, len(provinceMapArray[y])):
            colour = (provinceMapArray[x][y][0], provinceMapArray[x][y][1], provinceMapArray[x][y][2])
            if colour not in colourToProvinces:
                resource = resources[randint(0, 3)]
                newProvince = Province(idCounter, colour, resource)
                idCounter += 1
                colourToProvinces[colour] = newProvince
                provinces.append(newProvince)
            colourToProvinces[colour].pixels.append((x, y))

    # Issue 2: Creating image from a province's field; in this case, resource
    print("Generating Map")
    stdout.flush()
    mapImage = Image.new("RGB", (len(provinceMapArray), len(provinceMapArray[0])))
    for province in provinces:
        updateProvinceOnMap(mapImage, province, resourcesToColour)
    
    window = tk.Tk()
    window.geometry("1024x776")
    canvas = tk.Canvas(window, width=len(provinceMapArray), height=len(provinceMapArray[0]))
    canvas.pack()
    canvas.create_image(0, 0, image=ImageTk.PhotoImage(mapImage))
    window.mainloop()

核心优化思路

用Numpy向量运算替代Python循环,将计算转移到C层执行;同时调整数据结构,避免存储大量像素坐标,改用「颜色-省份ID映射」和「ID属性数组」实现批量操作。

1. 加载阶段优化(解决1000万次遍历瓶颈)

原代码逐个像素遍历并添加到Province的pixels列表,内存占用高且速度慢。优化后:

  • 用numpy.unique一次性提取所有唯一颜色及对应像素位置
  • 直接为每个颜色创建Province,无需逐个像素判断

优化代码片段:

# 替代原加载阶段的双重循环
provinceMapArray = numpy.array(provinceMapImage)
# 将RGB通道合并为单个uint32值,避免元组作为字典键的开销
flat_colours = provinceMapArray.reshape(-1, 3).view('uint32').ravel()
unique_colours, inverse_indices = numpy.unique(flat_colours, return_inverse=True)

colourToProvinces = {}
provinces = []
idCounter = 1
resources = ["Livestock", "Iron", "Gold", "Wood"]

for colour_uint32 in unique_colours:
    # 转回RGB元组
    colour = numpy.frombuffer(numpy.array(colour_uint32, dtype='uint32').tobytes(), dtype='uint8')[::-1][:3]
    colour = tuple(colour)
    resource = resources[randint(0, 3)]
    newProvince = Province(idCounter, colour, resource)
    colourToProvinces[colour_uint32] = newProvince
    provinces.append(newProvince)
    idCounter += 1

# 创建省份ID映射数组(后续上色核心依赖)
province_id_array = inverse_indices.reshape(provinceMapArray.shape[:2])

2. 切换MapMode优化(批量上色替代逐像素遍历)

原代码遍历每个省份的所有像素调用putpixel,效率极低。优化后:

  • 创建与地图同尺寸的颜色数组
  • 根据省份ID对应的属性批量填充颜色
  • 直接转为PIL Image,无需逐个修改像素

优化代码片段:

def generate_map_mode(province_id_array, provinces, resourcesToColour):
    # 构建ID到颜色的映射字典
    id_to_colour = {p.id: resourcesToColour[p.resource] for p in provinces}
    # 将ID数组批量转为颜色数组
    colour_array = numpy.array([id_to_colour[id] for id in province_id_array.ravel()], dtype='uint8')
    colour_array = colour_array.reshape(province_id_array.shape + (3,))
    # 直接转为PIL Image
    return Image.fromarray(colour_array)

# 生成地图模式图像
mapImage = generate_map_mode(province_id_array, provinces, resourcesToColour)

3. 省份数据更新优化(局部批量更新)

原代码遍历单个省份的所有像素修改,优化后:

  • 利用省份ID数组定位该省份的所有像素区域
  • 批量修改颜色数组对应区域
  • 仅更新tkinter画布的局部区域,而非全图重绘

优化代码片段:

def update_province(province_id_array, province, new_colour, mapImageArray):
    # 生成该省份的像素掩码
    mask = province_id_array == province.id
    # 批量设置颜色
    mapImageArray[mask] = new_colour
    # 计算更新区域的边界,用于tkinter局部刷新
    y_coords = numpy.where(mask.any(axis=1))[0]
    x_coords = numpy.where(mask.any(axis=0))[0]
    return (x_coords[0], y_coords[0], x_coords[-1]+1, y_coords[-1]+1)

# 使用示例:修改省份资源后更新地图
target_province = provinces[0]
target_province.resource = "Gold"
new_colour = resourcesToColour[target_province.resource]
# 假设mapImageArray是当前地图的Numpy数组
update_region = update_province(province_id_array, target_province, new_colour, mapImageArray)
# 生成局部图像并更新tkinter画布
updated_image = Image.fromarray(mapImageArray)
photo = ImageTk.PhotoImage(updated_image.crop(update_region))
canvas.create_image(update_region[0], update_region[1], image=photo, anchor=tk.NW)
canvas.image = photo  # 保留引用防止被垃圾回收

额外优化建议

  • 避免全图重绘:每次更新仅刷新修改的局部区域,减少tkinter开销
  • 内存优化:不再存储Province的pixels列表,改用ID数组映射,5000×2000地图可节省约80MB内存
  • PIL批量操作:若必须使用PIL对象,改用ImageDraw.point批量绘制像素,比putpixel快10倍以上

内容的提问来源于stack exchange,提问作者Graapefruit

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 03:25:49