基于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
相关产品推荐
相关产品推荐

