Pillow 这个东西我最早是拿它做图像批量压缩和加水印后来做计算机图形学作业才发现它比我以为的能打得多。很多人一听到“计算机图形学”脑袋里都是 OpenGL、DirectX、着色器、光线追踪这些吓人的名词实际上图形学里最基础的那部分——光栅化、颜色模型、几何变换、像素操作、抗锯齿用 Pythpon 配 Pillow 就能跑得明明白白。这篇内容没有学院派那种“先讲二十年数学”的架子就是把 Pillow 当画板从像素一路画到图像合成适合刚入门 Python、想找个直观角度理解图形学核心概念的读者也适合那些需要用 Python 快速生成或处理图像、又不想一上来就啃 C 图形库的开发者。很多人不知道Pillow 其实是 PILPython Imaging Library的延续版本。PIL 早期是 Python 图像处理的标配后来年久失修Pillow 接手成了事实标准。它底层用 C 语言实现核心图像解码和像素操作所以单张图片的读写速度是够用的。做图形学实验时Pillow 的价值不在于渲染多复杂的 3D 场景而在于它给了你一个“像素级”的操控入口你能读到一个像素的 RGB 值能改它能把它画出来还能把几百个像素组合成几何图形——这就是光栅化的雏形。1. 用 Pillow 学图形学一定要先搞懂这些底层逻辑1.1 Pillow 到底能做什么、不能做什么Pillow 不是完整的计算机图形学工具包这是一开始就要明确的事。它做不了实时 3D 渲染没有网格重建也没有 PBR 材质系统。但 Pillow 覆盖了图形学的一条关键主线二维图像的生成、变换、合成与分析。换句话说图形学课程里讲的“把三维模型投影到二维平面并绘制出来”这个最终环节Pillow 能接住。比如你要写一个 z-buffer 算法计算出一堆像素点的颜色以后怎么显示出来Pillow 直接就能把这些像素点写进内存图像再保存成 PNG。再比如做图像滤波、边缘检测、颜色空间转换Pillow 内置了卷积核、高斯模糊、灰度变换这些都是图形学基础课必讲的内容。所以我的定位是Pillow 是图形学入门的“练习场”。它让你不被编译环境、GPU 驱动、着色器语法这些外围问题干扰专注在“像素和几何”这件事本身。等你把坐标系、颜色模型、变换矩阵这些核心概念玩熟了再上 OpenGL 或 Vulkan会发现很多原理是相通的。1.2 像素坐标系一切图形操作的地基图形学里最容易搞混的就是坐标系。Pillow 遵循大多数图像库的约定左上角是原点x 轴向右y 轴向下。这个和我们平时用的数学坐标系不一样数学上 y 向上图像坐标 y 向下。这个差异出现的时候最好自己动手画一次。我拿 10x10 的黑色画布做过测试from PIL import Image, ImageDraw img Image.new(RGB, (10, 10), black) draw ImageDraw.Draw(img) draw.point((5, 2), fillred) img.save(coordinate_test.png)保存之后放大看红点出现在第二行第六列的位置而不是从底部往上数的第三行。这在做几何旋转、镜像、翻转时特别容易踩坑我早年写一个绕中心旋转的算法坐标算对了但结果总是偏移最后发现就是栽在 y 轴方向上。建议所有图形学实验都统一用“行、列”来思考像素位置先定行再定列就不容易绕晕。1.3 颜色模型RGB 只是最常见的一种图形学绕不开颜色模型。Pillow 支持的图像模式很全常见的有模式含义典型场景11位像素黑白二值掩膜L8位灰度0-255灰度图、明度计算RGB24位真彩色最常见的彩色图像RGBA32位带 Alpha 通道透明合成、叠加CMYK四色印刷模型打印输出HSV色相、饱和度、明度颜色处理实验RGB 是“加法混色”屏幕上的红色和绿色叠加会得到黄色。这个和绘画的“减法混色”相反因为屏幕是发光的。做图像混合、光照效果模拟时很多操作要回到颜色模型去理解。比如给图像打一个“暖光”直接给所有像素的 R 通道加一个固定值效果往往脏兮兮的换成 HSV 空间去调色相和饱和度就会自然很多。Pillow 里转换颜色空间很简单img Image.open(photo.jpg) img_hsv img.convert(HSV)convert 是 Pillow 里被低估的 API它不仅是换个模式底层还做了完整的颜色空间转换计算。做图形学实验时我经常把 RGB 转 HSV 分析色相分布再做调整最后再转回 RGB 保存。2. 环境准备与核心 API从零搭建 Pillow 图形实验室2.1 环境搭建别在安装这一步浪费时间写 Python 图形学实验环境就两个要求有 Python、有 Pillow。以我常用的 Anaconda 或官方 Python 3.10 为例安装就是一条命令的事pip install pillow如果是国内网络可以换镜像源比如清华的 PyPI 镜像pip install pillow -i https://pypi.tuna.tsinghua.edu.cn/simple安装完验证一下from PIL import Image, ImageDraw, ImageFilter, ImageChops, ImageEnhance print(Image.__version__)能打出版本号就说明一切就绪。这里补一句Pillow 和 numpy 是绝配图形学里做批量像素运算时Python 原生循环慢到怀疑人生转成 numpy 数组以后速度能提升几十倍。所以顺手把 numpy 也装上pip install numpy编辑器方面VSCode 配 Python 插件就够用了不需要 IDE。唯一要注意的是解释器路径必须选对。很多人电脑里装了多个 Python导致 VSCode 里 import PIL 报错其实包装在了另一个 Python 环境里。在 VSCode 里按 CtrlShiftP输入 “Python: Select Interpreter”选中你装 Pillow 的那个 Python 环境即可。2.2 打开、创建、保存Image 对象的核心操作Pillow 的一切从 Image 开始。它是内存里一幅图像的对象化表示对应的核心概念是“像素缓冲区和元信息宽、高、模式”。读取一张图from PIL import Image img Image.open(input.png) print(img.size) # (width, height) print(img.mode) # RGB / RGBA / L 等 print(img.format) # PNG / JPEG 等创建一个新图img Image.new(RGB, (800, 600), white)Image.new 的第一个参数是模式第二个是尺寸第三个是背景色。背景色可以传英文颜色名、十六进制字符串或者元组。如果你是做透明图层用 RGBA 模式加 (0, 0, 0, 0) 就是全透明。保存时要注意Pillow 保存 JPEG 不支持透明通道保存时会自动把 Alpha 丢掉。如果你处理的是 PNG记得始终用 PNG 或 WebP 格式保存保留 Alpha 通道。保存质量参数img.save(output.jpg, quality95)quality 参数只在 JPEG 和 WebP 格式下有效PNG 是无损压缩不吃质量参数。2.3 绘图原语图形学的“画笔工具箱”图形学里所有图像最终都要被“画”出来Pillow 的 ImageDraw 模块提供了完整的 2D 绘图原语from PIL import Image, ImageDraw img Image.new(RGB, (500, 500), white) draw ImageDraw.Draw(img) draw.line((50, 50, 400, 50), fillred, width3) draw.rectangle((100, 100, 300, 250), outlineblue, width2) draw.ellipse((200, 200, 350, 350), fillgreen) draw.polygon([(400, 400), (450, 300), (480, 420)], outlinepurple, fillyellow) draw.arc((100, 100, 300, 300), start0, end180, fillblack, width2) img.save(primitives.png)这里 polygon 和 ellipse 是图形学里最基本的图元。要注意的是ellipse 的参数是外接矩形的左上角和右下角不是圆心和半径很多人第一次用总在这里犯迷糊。想做真正圆形的圆就得让外接矩形是正方形。补一个经验之谈画带边线的形状时fill 是内部填充色outline 是边框色width 是边框宽度。两者不要写反否则出来的图形和你想要的大概相反。3. 图形学核心实操像素操作、几何变换与卷积滤波3.1 像素级操作用代码“点亮”每一个像素图形学的本质是操作像素。Pillow 提供了两个层次的操作接口slow 但直观的 getpixel/putpixel以及 fast 但稍微绕一点的 load。直接改像素的写法很符合直觉from PIL import Image img Image.new(RGB, (200, 200), black) pixels img.load() for y in range(200): for x in range(200): # 做一个简单的渐变x 越大越红 pixels[x, y] (x * 255 // 200, 0, 0) img.save(gradient.png)这里有一个隐藏的性能陷阱循环里调用 load() 返回的像素访问对象速度还凑合但你如果每像素调用一次 getpixel就会发现慢得离谱尤其在操作几百张图片的时候。原因是 getpixel/putpixel 每次调用都要做边界检查、模式转换和函数调用开销。更优雅、更图形学的做法是使用 numpy 数组import numpy as np from PIL import Image # 直接生成一个渐变数组 h, w 200, 200 arr np.zeros((h, w, 3), dtypenp.uint8) arr[:, :, 0] np.linspace(0, 255, w, dtypenp.uint8) # R 通道渐变 img Image.fromarray(arr) img.save(numpy_gradient.png)这两种方式的结果一样但执行速度差了几十倍。计算机图形学里有个原则叫“批处理优于逐像素”在 Python 里这个原则就更关键了因为 Python 循环本身就慢。我再分享一个真实经验做像素级光照模拟时我一开始用嵌套循环逐像素计算光照强度跑一张 1080p 的图要几十秒换成 numpy 的广播机制以后耗时直接降到 0.2 秒以内。所以凡是遇到“对每个像素做计算”的场景第一步永远是考虑能不能转成数组运算。3.2 几何变换缩放、旋转背后的矩阵与插值图形学里的几何变换本质就是坐标的映射。Pillow 的 resize、 rotate、 transform 都是这个映射的具体实现。resize 改变图像尺寸最重要的参数是 resample。这个参数决定缩放时用什么插值算法三种关键选项resample 参数插值算法效果Image.NEAREST最近邻边缘清晰但锯齿严重Image.BILINEAR双线性平滑但略微模糊Image.BICUBIC双三次质量最高速度稍慢img_small img.resize((400, 400), Image.BICUBIC)把一张大图缩小再放大你就会看到不同插值算法的差异。用最近邻放大后边缘呈明显锯齿状这其实就是图形学里“欠采样”的直接表现——源图像素不够插值算法在硬猜中间像素的颜色。rotate 的用法则是img_rotated img.rotate(45, expandTrue, fillcolor(255, 255, 255))这里 expandTrue 会扩大画布避免旋转后图像被裁剪fillcolor 填充旋转留下的空白区域。rotate 默认是逆时针旋转用角度制传入。如果你想更自由地做仿射变换比如倾斜、任意四边形映射用 transform 搭配一个仿射矩阵from PIL import Image # 仿射变换矩阵a, b, c, d, e, f # x a*x b*y c # y d*x e*y f matrix (1, 0.2, 0, 0.2, 1, 0) img_sheared img.transform( img.size, Image.AFFINE, matrix, resampleImage.BICUBIC )看到这个矩阵别慌它就是二维线性变换加平移的组合写法。这个矩阵在图形学里就是变换矩阵在 2D 下的具体形态你把它扩展成 3x3 再乘上齐次坐标本质上和图形学教材里的变换矩阵是同一套数学。能用 Pillow 亲手把一个矩阵作用到图像上看到效果比单纯看公式印象深得多。3.3 卷积与滤波图像处理的地基滤波是图形学和图像处理交叉的核心。Pillow 内置的 ImageFilter 模块提供了高斯模糊、中值滤波、边缘增强等常用滤波器它们的基础都是卷积运算。一个最典型的卷积操作是用一个 3x3 的核遍历图像每个像素把核覆盖区域的像素值和核系数相乘再求和得到该像素的新值。这就是图形学里空间滤波的标准过程。高斯模糊在 Pillow 里就一行img_blurred img.filter(ImageFilter.GaussianBlur(radius2))radius 控制模糊半径半径越大越模糊。高斯核的权重是钟形曲线中心大、边缘小所以模糊效果很自然。做“景深模拟”或者“柔和阴影”时这个操作特别常用。中值滤波更特殊它取核覆盖区域内所有像素值的中位数作为新值对去除椒盐噪声效果极好img_median img.filter(ImageFilter.MedianFilter(size3))size 是核边长必须是奇数。中值滤波的概念也好理解假设一片区域里有个白点噪声取中位数时白点会被周围大多数正常像素的颜色淹没噪声就没了。如果内置滤波不够你还可以自定义卷积核from PIL import ImageFilter custom_kernel ImageFilter.Kernel((3, 3), [-1, -1, -1, -1, 9, -1, -1, -1, -1], scale1) img_sharpened img.filter(custom_kernel)这个核是经典的锐化核中心权重 9周围全是 -1输出结果是“原图 边缘增强”的效果。熟悉图形学的人一眼就能看出来这就是拉普拉斯算子的变体。自己写核、自己调参数、自己观察效果变化比背公式直观得多。4. 进阶实验动手实现几个图形学小项目4.1 算法可视化手写一个抗锯齿直线图形学第一课通常是画直线。Pillow 自带的 line 已经做了抗锯齿但如果你想真正理解直线光栅化那就自己实现一个最基础的 DDA 算法甚至画一条“带锯齿”的直线看看。DDADigital Differential Analyzer的思路很简单已知两点坐标算出斜率沿着 x 方向一步步走y 的值每次加上斜率然后取整得到像素位置from PIL import Image def draw_line_dda(img, x0, y0, x1, y1, color): dx x1 - x0 dy y1 - y0 steps max(abs(dx), abs(dy)) x_inc dx / steps y_inc dy / steps x, y x0, y0 for _ in range(steps 1): img.putpixel((round(x), round(y)), color) x x_inc y y_inc img Image.new(RGB, (200, 200), white) draw_line_dda(img, 20, 100, 180, 120, (0, 0, 0)) img.save(dda_line.png)放大这张图你会看到明显的阶梯状锯齿这就是“走样”。如果要抗锯齿就要用到灰度加权让边缘像素以不透明度过渡。Pillow 的 line 默认抗锯齿是因为它内部做了这个加权。亲手画一条带锯齿的线再对比 Pillow 的平滑线你就彻底理解抗锯齿是个什么东西了。4.2 从零写一个图片合成器Alpha 混合实战Alpha 混合是图形学合成的基本操作。公式极其简单output src * alpha dst * (1 - alpha)Pillow 的 Image.blend 和 Image.alpha_composite 都实现了这个概念,但如果你自己写一遍对“透明度”的理解会上升一个台阶from PIL import Image import numpy as np bg Image.open(bg.jpg).convert(RGBA) fg Image.open(fg.png).convert(RGBA) bg_arr np.asarray(bg).astype(float) fg_arr np.asarray(fg).astype(float) alpha fg_arr[:, :, 3:4] / 255.0 result fg_arr[:, :, :3] * alpha bg_arr[:, :, :3] * (1 - alpha) result_img Image.fromarray(result.astype(np.uint8)) result_img.save(composited.png)这段代码里最关键的是 alpha 通道的提取注意 fg_arr 是 RGBA 四通道[:, :, 3:4] 取的是第四通道但保持了二维结构h, w, 1这样 numpy 广播乘法不会出维度错。这个细节我在实际写代码时踩过坑直接用 [:, :, 3] 会得到 (h, w) 的二维数组广播时会少一个维度运算就会报错或结果错乱。alpha 混合做出来的效果你既可以把文字图层叠在背景上做水印也可以把两张图片按渐变遮罩融合成全景图这已经是专业图像合成软件里的基础操作了。4.3 简单“光照”用像素运算模拟光晕效果图形学里光照是个大话题但入门的时候可以用一个很朴素的模型理解物体受光后其亮度由光源方向和表面法线的夹角决定。在二维图像里简化这个模型就是让距离“光源中心”近的像素更亮远的更暗。import numpy as np from PIL import Image img Image.open(texture.jpg).convert(RGB) arr np.asarray(img).astype(float) h, w, _ arr.shape cx, cy w // 2, h // 2 # 光源在图像中心 y, x np.mgrid[0:h, 0:w] dist np.sqrt((x - cx) ** 2 (y - cy) ** 2) intensity np.clip(1 - dist / (h * 0.6), 0, 1) # 光源从暖白过渡到暗 lit arr * intensity[:, :, np.newaxis] lit_img Image.fromarray(lit.astype(np.uint8)) lit_img.save(lighting.png)np.mgrid 生成了所有像素的坐标网格dist 计算每个像素到光源中心距离intensity 根据距离衰减。效果就是图片中心被照亮边缘渐暗很像舞台灯光打下来。这个模型虽然简单但“距离决定强度”的思想正是点光源衰减的核心后面学真实光照时你会发现公式只是加了更多因子而已。4.4 图像通道拆分与合成RGB 分量的可视化图像通道拆分是图形学里常见的调试工具。很多时候你做色彩平衡、色偏校正都需要单独观察一个通道。from PIL import Image import numpy as np img Image.open(photo.jpg).convert(RGB) arr np.asarray(img) r arr[:, :, 0] g arr[:, :, 1] b arr[:, :, 2] # 展示单个通道其他通道置零 Image.fromarray(np.stack([r, np.zeros_like(r), np.zeros_like(r)], axis-1)).save(channel_r.jpg) Image.fromarray(np.stack([np.zeros_like(g), g, np.zeros_like(g)], axis-1)).save(channel_g.jpg) Image.fromarray(np.stack([np.zeros_like(b), np.zeros_like(b), b], axis-1)).save(channel_b.jpg)这种可视化能让颜色模型变得非常具象。你把三张通道图放到一起看会发现蓝色通道往往像夜景摄影红色通道则像热成像。我做颜色校正时经常先用这种方式分析哪个通道溢出再针对性地调整比凭感觉乱调效率高得多。5. 实战避坑指南我在 Pillow 图形学实操中踩过的 12 个坑5.1 安装与环境问题坑 1pip 装了 Pillow 但 import 失败。这种八成是“多 Python 环境”问题。Windows 下打开命令提示符输入 where python看看到底用的是哪个。VSCode 里也确认解释器路径。Linux 下则注意是 pip 还是 pip3python 还是 python3。坑 2输入图片格式不支持。Pillow 默认支持常见格式但如果遇到 HEIC、WebP、某些 TIFF需要额外安装插件。WebP 需要 libwebp可以在安装 Pillow 时确认是否附带。如果图片打不开报 “cannot identify image file”先用工具转个格式再说。坑 3中文路径报错。如果你的 Windows 用户名叫中文或者图片路径里有中文Pillow 的 open 在某些版本会报错或找不到文件。最简单粗暴的解法是把图片和脚本都放在纯英文路径下。这不是 Pillow 的锅是 Windows 默认编码的历史包袱。5.2 API 使用与概念问题坑 4RGB 还是 BGR 搞错。Pillow 用 RGB 顺序OpenCV 用 BGRnumpy 数组本身不区分颜色顺序但 np.asarray 拿到图片后按 Pillow 的 RGB 顺序。如果你先用 OpenCV 读图再用 Pillow 保存颜色会直接反掉红色变蓝色。坑 5旋转方向反了。Pillow 的 rotate 是逆时针转动这个和很多人的直觉相反。作业里要求顺时针旋转 90 度直接写 rotate(90) 是不对的要写 rotate(-90)。坑 6坐标位置偏移。在图像中心画图形你可能会用 (w/2, h/2)但注意像素是整数w/2 如果是小数Pillow 会接受但不能精确对应某个像素。处理这类位置我习惯先定义整数坐标再运算避免浮点坐标累积误差。坑 7mode 概念的坑。一张 RGBA 图转成 RGB 后会丢失透明信息再转回 RGBA 也找不回来Alpha 通道已经被丢弃。如果需要保留透明信息要先把 Alpha 通道单独存好转换后再贴回去。对初学者要记住 convert 是不可逆的信息损失过程除非明确这是你想要的。坑 8滤镜参数理解错误。比如 GaussianBlur 的 radius 的值近似为高斯核的标准差不是模糊半径。radius2 和 radius5 差异非常明显建议用实际图片反复试不同参数形成直观的量感。5.3 性能与质量坑坑 9循环逐像素操作太慢。这个问题我已经强调过再补充一个数据800x600 的图片用 getpixel 循环一次可能要 1-2 秒用 numpy 数组运算同样的操作在毫秒级完成。记住纯 Python 循环在图像尺寸面前就是灾难。凡是可以用 numpy 向量化运算的绝对不要用循环。坑 10JPEG 压缩产生的色块。做像素操作实验时输出结果如果保存成 JPEG会因为有损压缩出现颜色溢出的色块尤其在边缘对比强烈的区域。图形学实验建议全部输出为 PNG无损且能看到真实结果。坑 11resize 时误用默认参数。Pillow resize 的默认 resample 参数是 NEAREST很多人不知道这一点放大后会看到明显的像素方块。一般做高质量缩放用 BICUBIC做缩略图或速度优先的场景用 BILINEAR 就足够。坑 12坐标轴方向导致旋转结果像做镜像。这个坑在前面说过再强调一次改像素时putpixel 第一个参数是 (x, y) 结肠列行但很多人总习惯先写“行”。写代码时可以刻意定义一个辅助函数然后把参数调换过来减少犯错。5.4 三个最实用的排错技巧技巧 1把中间结果保存出来看看。每做完一步变换就保存一张 PNG 或者截图看效果。图形学调试最忌讳脑补数据什么形状直接视觉化出来问题一眼就能定位。技巧 2缩小到 100x100 调试。如果算法在大图上跑不出正确效果先用小图调试小图可以像素级打印检查定位逻辑错误比大图快得多。验证正确后再放大到原尺寸。技巧 3用数组形状断言尽早发现纬度错误。做 numpy 数组操作时每次做完形状变化都用 print(arr.shape) 检查维度尤其是涉及通道操作时。很多报错发生前形状其实已经悄悄不对了。6. 从 Pillow 走向更深图形学下一步怎么走Pillow 只是图形学入门的起点但它是理解后续知识的“螺母”。当你习惯用像素视角看图像后关于接下来的方向我说一点个人的方向和体会。如果你想继续深入渲染管线建议从 Pillow 平滑过渡到 PyOpenGL。Pillow 里学的颜色模型、坐标变换、矩阵运算在 OpenGL 里全部依然适用变化主要体现在渲染流程的自动化程度上。比如你在 Pillow 手写了一个仿射变换矩阵在 OpenGL 里就是用 glUniformMatrix 传个矩阵显卡帮你完成后续的光栅化。如果你对图像处理更感兴趣下一步可以学 scikit-image、OpenCVPillow 学的卷积原理在那里会被大量复用到。如果你沉迷于数学建模的感觉则可以去看看三维几何库比如 trimesh、pyrender。这时候你会发现Pillow 学到的“画布、像素、通道”所有概念都在。我给自己的实践边界是Pillow 负责二维图形的生成与合成Geometric 库做三维模型加载numpy 负责所有数学运算OpenGL 负责最后一步渲染。每个环节选最顺手的工具合起来构成一个简单的“软件渲染管线”。这条路走通之后你对图形学真正主干的理解会踏实很多。最后还是说句实在话。图形学的门槛不在 API 也不在环境在一开始要对每个操作都追问“为什么”。用 Pillow 亲手实现一个算法的时候你看到的不只是效果变化还有背后最原始的计算逻辑。建议你在完成我上面任何一个实验后把参数改了再看效果比如变换矩阵里的系数、滤波核的权重、光照衰减速度每个改动都会让画面给出最直接的反馈。这种“改一个参数、立刻看效果”的循环是入门图形学最好的加速器。