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

基于Pillow的逐像素图像处理性能优化求助

优化ASCII艺术图转换性能的求助

我用以下方法将普通图片转换为ASCII艺术图,但在性能较弱的处理器上,即使处理小图片也会导致设备卡顿。尝试用Numpy优化但未取得进展,希望得到优化方面的帮助。算法来自Ascify-Art项目。

原始代码

from PIL import Image, ImageDraw, ImageFont
import math

def make_magic_old(photo, chars="01", char_size=15, char_width=10, char_height=18, scale=0.09):
    # Rounding scale
    scaleFactor = round(scale, 3)
    # Calculate the length of the character list
    charLength = len(list(chars))
    # Calculate the interval for converting a pixel value into a character
    interval = charLength / 256
    # Convert the image to RGB
    photo = photo.convert("RGB")
    # Load font
    fnt = ImageFont.truetype("assets/fonts/FiraCode-Bold.ttf", char_size)
    # Get size of the image
    width, height = photo.size
    # Scaling the image
    photo = photo.resize((int(scaleFactor * width), int(scaleFactor * height * (char_width / char_height))), Image.Resampling.NEAREST)
    # Getting the sizes in a new way after scaling
    width, height = photo.size
    # Load pixels
    pix = photo.load()
    # Create a new image to display the result
    outputImage = Image.new("RGB", (char_width * width, char_height * height), color="black")
    # Create a drawing tool
    draw = ImageDraw.Draw(outputImage)
    # Replace pixes to text
    for i in range(height):
        for j in range(width):
            r, g, b = pix[j, i]
            # Calculate the average color value
            h = int(r / 3 + g / 3 + b / 3)
            # Convert pixel colors
            pix[j, i] = (h, h, h)
            # Display a symbol instead of a pixel
            draw.text((j * char_width, i * char_height), chars[math.floor(h * interval)], font=fnt, fill=(r, g, b))

    return outputImage

def main():
    photo = Image.open("test.png")
    result_photo = make_magic(photo)
    result_photo.save("result.jpg")
    print("Done!")
    
if __name__ == "__main__":
    main()

Numpy优化尝试代码

import numpy as np

def make_magic(photo, chars="01", char_size=15, char_width=10, char_height=18, scale=0.09):
    # Rounding scale
    scaleFactor = round(scale, 3)
    # Calculate the length of the character list
    charLength = len(chars)
    # Convert the image to RGB and then to numpy array
    photo = np.array(photo.convert("RGB"))
    # Load font
    fnt = ImageFont.truetype("assets/fonts/FiraCode-Bold.ttf", char_size)
    # Get size of the image
    height, width, _ = photo.shape
    # Scaling the image
    photo = np.array(Image.fromarray(photo).resize((int(scaleFactor * width), int(scaleFactor * height * (char_width / char_height))), Image.NEAREST))
    # Getting the sizes in a new way after scaling
    height, width, _ = photo.shape
    # Convert the image to grayscale
    grayscale_photo = np.mean(photo, axis=2).astype(np.uint8)
    # Calculate indices for character selection
    indices = (grayscale_photo * (charLength / 256)).astype(int)
    # Create a new image to display the result
    outputImage = Image.new("RGB", (char_width * width, char_height * height), color="black")
    # Create a drawing tool
    draw = ImageDraw.Draw(outputImage)
    # Create character array
    char_array = np.array(list(chars))
    # Replace pixels with text
    for i in range(height):
        for j in range(width):
            draw.text((j * char_width, i * char_height), char_array[indices[i, j]], font=fnt, fill=tuple(photo[i, j]))
    return outputImage

处理效果

处理前:
处理前图片

处理后:
处理后图片


优化方案

核心瓶颈分析

你的两次尝试中,逐像素调用draw.text()是最大性能杀手——这是PIL的单线程阻塞操作,循环次数等于缩放后图片的像素总量,函数调用开销累积后会导致严重卡顿。Numpy优化仅处理了灰度计算和字符索引,但未解决绘图循环的核心问题。

具体优化方法

1. 批量行绘制(减少draw.text()调用次数)

将每行的字符先拼接成字符串,再整行绘制,能大幅降低函数调用次数。如果需要保留逐字符的原像素颜色,可优化循环逻辑减少冗余操作:

from PIL import Image, ImageDraw, ImageFont
import numpy as np

def make_magic_optimized(photo, chars="01", char_size=15, char_width=10, char_height=18, scale=0.09):
    scaleFactor = round(scale, 3)
    charLength = len(chars)
    char_map = np.array(list(chars))
    interval = charLength / 256

    # 图片缩放(直接用PIL操作,避免numpy与PIL频繁转换)
    photo = photo.convert("RGB")
    orig_w, orig_h = photo.size
    new_w = int(scaleFactor * orig_w)
    new_h = int(scaleFactor * orig_h * (char_width / char_height))
    photo_scaled = photo.resize((new_w, new_h), Image.Resampling.NEAREST)
    
    # Numpy批量计算灰度值与字符索引
    img_np = np.array(photo_scaled)
    grayscale = np.mean(img_np, axis=2).astype(np.uint8)
    indices = (grayscale * interval).astype(int)
    char_lines = ["".join(char_map[row]) for row in indices]

    # 创建输出画布
    output_w = char_width * new_w
    output_h = char_height * new_h
    output_img = Image.new("RGB", (output_w, output_h), "black")
    draw = ImageDraw.Draw(output_img)
    fnt = ImageFont.truetype("assets/fonts/FiraCode-Bold.ttf", char_size)

    # 逐行处理字符与颜色
    for i in range(new_h):
        line_chars = char_lines[i]
        row_colors = img_np[i]
        for j in range(new_w):
            draw.text((j * char_width, i * char_height), line_chars[j], font=fnt, fill=tuple(row_colors[j]))
    
    return output_img

2. 预生成字符模板(彻底避开绘图循环)

预先生成每个字符的空白模板,再用Numpy批量替换颜色并拼接,完全绕过draw.text()的循环开销:

from PIL import Image, ImageDraw, ImageFont
import numpy as np

def make_magic_template(photo, chars="01", char_size=15, char_width=10, char_height=18, scale=0.09):
    scaleFactor = round(scale, 3)
    charLength = len(chars)
    char_map = np.array(list(chars))
    interval = charLength / 256

    # 图片缩放
    photo = photo.convert("RGB")
    orig_w, orig_h = photo.size
    new_w = int(scaleFactor * orig_w)
    new_h = int(scaleFactor * orig_h * (char_width / char_height))
    photo_scaled = photo.resize((new_w, new_h), Image.Resampling.NEAREST)
    img_np = np.array(photo_scaled)
    grayscale = np.mean(img_np, axis=2).astype(np.uint8)
    indices = (grayscale * interval).astype(int)

    # 预生成所有字符的白色模板(黑色背景)
    fnt = ImageFont.truetype("assets/fonts/FiraCode-Bold.ttf", char_size)
    char_templates = {}
    for char in chars:
        char_img = Image.new("RGB", (char_width, char_height), "black")
        draw = ImageDraw.Draw(char_img)
        draw.text((0, 0), char, font=fnt, fill=(255, 255, 255))
        char_templates[char] = np.array(char_img)

    # 批量拼接生成输出图
    output_np = np.zeros((new_h * char_height, new_w * char_width, 3), dtype=np.uint8)
    for i in range(new_h):
        for j in range(new_w):
            char = char_map[indices[i,j]]
            template = char_templates[char]
            # 用原像素颜色替换模板中的白色区域
            color = img_np[i,j]
            mask = template == 255
            output_np[i*char_height:(i+1)*char_height, j*char_width:(j+1)*char_width][mask] = color
    
    return Image.fromarray(output_np)

3. 额外小优化

  • 预加载字体:将字体加载放在函数外部,避免每次调用函数都读取字体文件。
  • 减少类型转换:尽量在PIL或Numpy单环境内完成操作,避免两者频繁转换。
  • 多线程处理:若CPU核心充足,可将行分配给多个线程生成子图后拼接(注意ImageDraw非线程安全,需每个线程独立创建绘图对象)。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 07:43:12