如何加速PIL中的逐像素遍历循环操作
像素R/G通道旋转处理提速方案
你需要实现的像素处理逻辑为:
- 将像素R、G通道的取值从
0 <-> 255区间映射到-1 <-> 1区间 - 读取
rotations[i]存储的角度值,将映射后的(r, g)坐标点绕原点(0,0)做二维旋转
原有逐像素嵌套for循环的实现处理4K*4K(约1600万像素)图像时耗时过长,尝试joblib、multiprocessing并行加速时频繁触发pickle相关报错,原有实现代码如下:
c = math.cos(rotations[i]) s = math.sin(rotations[i]) pixels = texture_.load() for X in range(width): for Y in range(height): x = (pixels[X, Y][0]/255 -.5)*2 y = (pixels[X, Y][1]/255 -.5)*2 z = pixels[X, Y][2] x_ = x*c-y*s y_ = x*s+y*c x_ = 255*(x_/2+.5) y_ = 255*(y_/2+.5) pixels[X, Y] = (math.floor(x_), math.floor(y_), z)
最优提速方案
你完全不需要折腾多进程并行,代码慢的核心原因是Python原生for循环逐像素执行的开销极高,1600万次循环跑Python字节码自然耗时久。直接用NumPy做向量化运算即可,所有计算逻辑在C层执行,速度比原生循环快100倍以上,还能彻底避开多进程序列化的pickle报错问题。
实现步骤
- 不要用PIL的
load()方法拿像素访问对象逐点修改,直接将整幅图像转为NumPy数组做批量运算 - 一次性完成所有像素R、G通道的区间映射,不需要逐点计算
- 用数组广播机制一次性完成所有像素点的旋转变换
- 计算结果裁剪到
0~255的合法像素值区间,转回uint8格式后重组为图像
优化后代码
import math import numpy as np from PIL import Image # 预计算旋转参数,和原逻辑保持一致 c = math.cos(rotations[i]) s = math.sin(rotations[i]) # 图像转float32类型的numpy数组,shape为(高度, 宽度, 3) img_arr = np.array(texture_, dtype=np.float32) # 批量完成R/G通道值映射:0~255 -> -1~1 r_norm = (img_arr[..., 0] / 255 - 0.5) * 2 g_norm = (img_arr[..., 1] / 255 - 0.5) * 2 b_channel = img_arr[..., 2] # B通道不做修改 # 批量完成所有点的旋转变换 r_rot = r_norm * c - g_norm * s g_rot = r_norm * s + g_norm * c # 旋转结果映射回0~255区间,向下取整后裁剪到合法像素范围,避免颜色溢出 r_res = np.clip(np.floor(255 * (r_rot / 2 + 0.5)), 0, 255).astype(np.uint8) g_res = np.clip(np.floor(255 * (g_rot / 2 + 0.5)), 0, 255).astype(np.uint8) b_res = b_channel.astype(np.uint8) # 合并三通道,转回PIL图像对象 result_texture = Image.fromarray(np.stack([r_res, g_res, b_res], axis=-1))
补充说明
- 该方案为单进程实现,处理4K*4K图像通常仅需几十到几百毫秒,完全满足常规性能需求
- 之前多进程触发pickle报错,本质是尝试将PIL像素访问对象这类无法序列化的对象传入子进程导致的,NumPy数组序列化成本极低,若后续确实需要多进程批量处理多张图,用NumPy数组传参也能避开这类报错
- 新增的
np.clip操作用于处理旋转后坐标略超出-1~1区间的边界情况,避免映射回像素值时出现小于0或大于255的非法值导致颜色错乱 - 如果有GPU环境,可将NumPy替换为CuPy,把运算搬到GPU上执行,4K图像处理耗时可进一步压缩到10毫秒以内,普通场景下NumPy的性能已经足够。
内容的提问来源于stack exchange,提问作者Apoqlite
相关产品推荐
相关产品推荐

