做图像处理这么多年,我越来越离不开的一个工具就是 Python 的 Pillow 库。很多人一听“计算机图形学”就觉得是高深的数学和图形渲染,其实我们日常遇到的大量需求——批量改图片、生成验证码、给照片加水印、把数据变成可视化图形、甚至是自己动手写一个简单的光栅渲染器——都可以用 Pillow 轻松实现。它本质上是经典 PIL(Python Imaging Library)的延续版本,保留了简洁的 API,同时又修复了多年不更新带来的兼容性问题。如果你和当初的我一样,手里有一堆图片需要处理,或者想用自己的代码在屏幕上“画”出点什么,那这篇分享就是为你准备的。我会从环境配置、基础绘图、图像合成,一直讲到用像素级操作做函数曲线和分形渲染,把我会踩的坑和用着顺手的方法都写在里面。
1. 图像基础与Pillow环境准备
1.1 为什么选择 Pillow 而不是其他图像库
在开始写代码之前,我想先说清楚一个经常被问到的选择题:图像处理用 Pillow、OpenCV 还是 Matplotlib?
如果你需要做视频流处理、摄像头实时采集、或者大量用到边缘检测、特征点匹配这类高级视觉算法,OpenCV 是更合适的。但它依赖的底层库较多,安装体积大,学习曲线也陡。而 Matplotlib 更适合做数据图表呈现,虽然也能生成图像,但它的定位是“绘图展示”,不是像素级的图像编辑。
Pillow 正好卡在中间:它是一个纯粹的 2D 光栅图形处理库,安装简单,支持几十种图像格式的读写,能处理裁剪、缩放、旋转、滤镜、合成,还能用 ImageDraw 在图像上绘制各种基本图元。它的设计哲学就是“轻量、直接、够用”。做批量图片处理脚本、生成图片素材、调试图形算法原型,Pillow 是首选。很多老项目里
import PIL
,指的就是这个库,因为 Pillow 保留了 PIL 的包名。这一点经常让新手困惑,后面我会专门提。
计算机图形学这个概念听起来很大,但其实 Pillow 覆盖的正是其中的 2D 光栅化部分:把几何描述(点、线、圆、文字)转换成屏幕上的像素,以及对这些像素进行各种变换和处理。想要理解更底层的渲染管线,用 Pillow 做实践是个特别好的入门方式。
1.2 安装与图像文件读写
安装实在太简单了,一条命令:
pip install Pillow
如果是在 Windows 上,请确保你的 Python 环境变量已经配置好,否则
pip
命令可能找不到。遇到下载慢的情况,可以加上国内镜像源,比如清华源:
pip install Pillow -i https://pypi.tuna.tsinghua.edu.cn/simple
装完之后验证一下版本:
from PIL import Image
import PIL
print(PIL.__version__)
Pillow 最基础的两个函数是
Image.open()
和
Image.save()
。这里有个新手容易踩坑的点:
Image.open()
是惰性加载的,它不会一次性把像素数据全部读进内存,只有在调用
load()
、
save()
或者强制转换时才会真正读取文件内容。这就意味着如果你打开了文件却没有用完就关闭文件句柄,后续再操作图像时可能会抛异常。
我习惯的做法是打开后立刻调用
load()
把数据载入内存,或者用
with
语句保证资源释放:
from PIL import Image
with Image.open("example.png") as im:
im.load()
im.thumbnail((400, 400))
im.save("example_thumb.jpg", quality=90)
save()
的时候,Pillow 会根据文件扩展名自动判断输出格式,也可以直接用第二个参数手动指定,比如
im.save("output", format="PNG")
。
1.3 图像模式与坐标系
理解 Pillow 的图像模式是后续一切操作的基础。最常见的三个模式是:
-
RGB
:三通道彩色图,每个像素三个字节。
-
RGBA
:多了一个 Alpha 透明通道,适合处理 PNG 和带透明效果的合成。
-
L
:灰度图,每个像素一个字节,取值范围 0 到 255。
图像内部还有一个“坐标系”的概念。Pillow 使用标准的图像坐标系:左上角是原点
(0, 0)
,水平向右是 x 方向,垂直向下是 y 方向。这一点和我们在数学课上学到的直角坐标系不同,后者 y 轴向上。初学者如果忘记这一点,画出来的图形很可能是上下颠倒的。
创建一张新图像的基本写法:
from PIL import Image
# 创建一个 800×600 的白色画布
img = Image.new("RGB", (800, 600), "white")
第一个参数是模式,第二个是尺寸(宽、高),第三个可以是颜色名称、十六进制字符串或者 RGB 元组。这张画布一旦创建,就代表一个可以自由绘制的二维像素空间了。
2. 用 Pillow 绘制基础图形:把脑子里的图像变成像素
2.1 ImageDraw 与基本图形绘制
图像光有画布不会自己产生内容,真正往上面画图的是
ImageDraw
模块。它提供的接口特别直观,就像在纸上用不同颜色的笔画线、画矩形、画椭圆。
from PIL import Image, ImageDraw
img = Image.new("RGB", (600, 400), "#FFFFFF")
draw = ImageDraw.Draw(img)
# 画一条红色线段
draw.line((50, 50, 550, 50), fill="#FF0000", width=3)
# 画一个蓝色空心矩形
draw.rectangle((100, 100, 300, 200), outline="#0000FF", width=2)
# 画一个绿色实心圆
draw.ellipse((350, 100, 450, 200), fill="#00FF00", outline="#000000")
# 画多边形
draw.polygon([(100, 300), (200, 250), (300, 300)], fill="#FFAA00")
img.save("draw_basic.png")
这里有个细节:
ellipse()
的入参是一个包围圆的矩形区域,而不是圆心和半径。想画一个半径为 50、圆心在
(200, 200)
的圆,就要传入
(150, 150, 250, 250)
。很多从其他图形库转过来的人会在这里卡住,我当初也对着文档愣了半天。
rectangle()
和
ellipse()
支持的参数组合是一样的:
fill
指填充色,
outline
指边框色,
width
指边框宽度。如果你想要半透明效果,图像模式必须是
RGBA
,颜色也要带 alpha 值,比如
fill=(255, 0, 0, 128)
。
2.2 文字与字体渲染
图像上写文字也是高频需求,比如给图片加水印、生成海报标题、做验证码。Pillow 的文字绘制函数在
ImageDraw.text()
,但它默认的字体比较朴素,而且在不同操作系统上表现不一致。我的建议是直接用本地字体文件或者常见的系统字体路径。
from PIL import Image, ImageDraw, ImageFont
img = Image.new("RGB", (500, 200), "white")
draw = ImageDraw.Draw(img)
# 加载一个 32 号字体,Windows 下常见路径
font = ImageFont.truetype("C:/Windows/Fonts/arial.ttf", 32)
# 先测量文字宽度,方便让文字居中
bbox = draw.textbbox((0, 0), "Hello Pillow", font=font)
text_width = bbox[2] – bbox[0]
text_height = bbox[3] – bbox[1]
x = (500 – text_width) // 2
y = (200 – text_height) // 2
draw.text((x, y), "Hello Pillow", fill="black", font=font)
img.save("text_centered.png")
新版 Pillow 里
textsize()
已经标记为弃用,推荐使用
textbbox()
或者
textlength()
。如果不小心用了旧方法,控制台上会看到 DeprecationWarning,虽然不影响运行,但终归不够干净。
字体还有一个隐藏的坑:渲染中文时如果指定英文字体,会出现方框或乱码。解决办法很简单,选择一个支持中文的字体,比如 Windows 下的
simhei.ttf
或者
msyh.ttc
,Linux 下用
wqy-microhei.ttc
。另外,
ImageFont.truetype()
需要的是文件路径,不是系统中注册的字体名称,所以跨平台项目最好把字体文件放到项目目录里,然后用相对路径加载。
2.3 像素级操作:自己动手写光栅算法
前面说的都是高层接口,真正理解计算机图形学还要接触像素操作。Pillow 提供了
getpixel()
和
putpixel()
两个方法,但底层效率不算高,原因在于每个像素的取值都涉及一次 Python 函数调用。如果你图省事,可以用
Image.load()
返回的数组对象,它在大多数情况下比
getpixel()
快不少。
下面这个例子是用像素循环给图像做一个渐变背景:
from PIL import Image
width, height = 800, 400
img = Image.new("RGB", (width, height))
pixels = img.load()
for y in range(height):
for x in range(width):
# 从左到右,红色分量逐渐增强
pixels[x, y] = (x * 255 // (width – 1), 0, 128)
img.save("gradient.png")
运行完你会发现,这张图片从左到右由黑色逐步变成红色,非常直观。用
putpixel
画圆也很简单,只要判断像素点到圆心的距离是否小于半径就行:
import math
from PIL import Image
img = Image.new("RGB", (400, 400), "white")
pixels = img.load()
cx = cy = 200
radius = 150
for y in range(400):
for x in range(400):
distance = math.sqrt((x – cx) ** 2 + (y – cy) ** 2)
if distance <= radius:
pixels[x, y] = (0, 0, 255)
img.save("circle.png")
这就是光栅化的基本形态:遍历屏幕上的每个像素,判断它是否属于某个几何形状,再决定颜色。虽然 Pillow 本身已经有画圆的高层方法,但理解这个判断逻辑后,你就能自己扩展出各种复杂图形。
3. 图像处理与合成:批量操作的高级场景
3.1 几何变换:裁剪、缩放、旋转与翻转
Pillow 的几何变换非常顺手,几乎每个绘图脚本都会用到。先说缩放,
resize()
和
thumbnail()
都是改变图像尺寸,但行为不同:
-
resize()
会严格把图片变成你指定的尺寸,不考虑宽高比。
-
thumbnail()
是在保持宽高比的前提下,把图片缩小到指定范围内,只会变小不会变大。
from PIL import Image
im = Image.open("big_photo.jpg")
print("原始尺寸:", im.size)
# 强制缩放成 400×300
im_resized = im.resize((400, 300))
im_resized.save("resized.jpg")
# 保持宽高比,缩放到最大边为 400
im_thumb = im.copy()
im_thumb.thumbnail((400, 400))
im_thumb.save("thumbnail.jpg")
旋转和翻转也很常用。
rotate()
默认不会扩展画布,如果旋转 45 度,图片四个角会被裁剪掉。想要完整显示旋转后的图片,要加上
expand=True
参数,同时可以指定背景填充色。
im_rotated = im.rotate(45, expand=True, fillcolor="white")
im_rotated.save("rotated.jpg")
# 水平翻转
im_flip = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
im_flip.save("flipped.jpg")
注意循环旋转同一个图片对象时要小心累积误差,最好的做法是每次从原始图像拷贝一份再旋转。
3.2 滤镜与增强:ImageFilter 与 ImageEnhance
Pillow 内置了不少滤镜,在
ImageFilter
模块里。最常用的是高斯模糊、轮廓提取和边缘增强。
from PIL import Image, ImageFilter
im = Image.open("photo.jpg")
# 高斯模糊
blurred = im.filter(ImageFilter.GaussianBlur(radius=5))
blurred.save("blurred.jpg")
# 轮廓提取(可以拿来做边缘检测演示)
outline = im.filter(ImageFilter.CONTOUR)
outline.save("outline.jpg")
# 浮雕效果,做文字水印背景挺好用
embossed = im.filter(ImageFilter.EMBOSS)
embossed.save("embossed.jpg")
如果你觉得图片太暗或者太淡,可以用
ImageEnhance
模块调整亮度、对比度、色度和锐度:
from PIL import Image, ImageEnhance
im = Image.open("photo.jpg")
enhancer = ImageEnhance.Brightness(im)
im_bright = enhancer.enhance(1.3) # 亮度提升 30%
enhancer = ImageEnhance.Contrast(im_bright)
im_contrast = enhancer.enhance(1.2)
enhancer = ImageEnhance.Sharpness(im_contrast)
im_sharp = enhancer.enhance(2)
im_sharp.save("enhanced.jpg")
实际批量处理时,这类操作常常被封装在一个循环里,对一批图片统一增强,做成批量滤镜工具。
3.3 图像合成与蒙版:让两张图无缝融合
合成操作在图像处理中特别重要,最常见的是把带透明背景的 PNG 贴到另一张背景图上。Pillow 提供了
paste()
方法,第三参数传入 PNG 的 alpha 通道作为蒙版,就能实现透明融合:
from PIL import Image
background = Image.open("background.jpg").convert("RGBA")
overlay = Image.open("logo.png").convert("RGBA")
# 把 logo 缩小
overlay.thumbnail((200, 200))
# 计算粘贴位置,比如放在右下角
pos = (background.width – overlay.width – 50,
background.height – overlay.height – 50)
# 利用 overlay 自身的 alpha 通道当蒙版
background.paste(overlay, pos, overlay)
# 如果背景需要保存为 JPEG,记得转回 RGB
result = background.convert("RGB")
result.save("composite.jpg")
这里容易翻车的地方有三个:
paste()
可能报错或忽略透明通道,最好先统一转换为
RGBA
。
paste()
的目标位置只接受一个二元组
(x, y)
,不接受四个坐标值那个版本跟
ImageDraw
的矩形参数不一样。
如果做的是两张图之间真正的像素级融合,可以用
Image.composite()
,它的参数是两张图和一个 mask 图,三者尺寸和模式必须完全一致:
from PIL import Image, ImageDraw
im1 = Image.new("RGB", (300, 300), "red")
im2 = Image.new("RGB", (300, 300), "blue")
# 创建一个从中心向外的圆形渐变 mask
mask = Image.new("L", (300, 300), 0)
mask_draw = ImageDraw.Draw(mask)
mask_draw.ellipse((75, 75, 225, 225), fill=255)
result = Image.composite(im1, im2, mask)
result.save("composite_circle.jpg")
Image.composite()
的每个像素相当于根据 mask 的灰度值做线性插值:mask 为 255 的位置完全取 im1,为 0 的位置完全取 im2。理解这个逻辑之后,你还能自由扩展出任意形状的渐变融合。
4. 实战:从零生成一张函数曲线图(模拟图形学绘图管线)
4.1 需求与流程拆解
理论聊了不少,下面我们来一个完整实战:用 Pillow 生成一张带网格、坐标轴的正弦函数曲线图。这看起来像 Matplotlib 的活,但往深了说,它正好体现了一个简单的图形学渲染流程:
世界空间(World Space)
:函数定义域和值域,比如 x 从 0 到 4π,y 从 -1 到 1。
屏幕空间(Screen Space)
:图片的像素坐标系,宽 800,高 450。
坐标映射(Projection)
:把世界坐标映射到像素坐标,核心是一对线性变换函数。
光栅化(Rasterization)
:把连续曲线离散成大量采样点,再连成折线,这就是所有绘图库画连续函数的基础原理。
我不直接贴一个调好的“一行代码画图”的工具,而是把底层逻辑拆开给你看,这样你之后需要画任意自定义坐标系时,都能自己写出来。
4.2 坐标映射与曲线光栅化核心代码
先定义两个坐标转换函数,把数学上的
(x, y)
变成图像上的像素
(px, py)
。注意图像坐标系 y 向下,所以要在 y 方向做一次翻转映射,否则正弦函数会倒过来显示。
import math
from PIL import Image, ImageDraw
width, height = 800, 450
img = Image.new("RGB", (width, height), "white")
draw = ImageDraw.Draw(img)
# 世界坐标范围
x_min, x_max = 0, 4 * math.pi
y_min, y_max = -1.2, 1.2
def world_to_pixel(x, y):
px = (x – x_min) / (x_max – x_min) * (width – 1)
py = (y_max – y) / (y_max – y_min) * (height – 1)
return int(px), int(py)
# 1. 绘制网格线
for gx in range(x_min, x_max + 1):
if gx % 1 == 0:
px, _ = world_to_pixel(gx, y_min)
draw.line((px, 0, px, height – 1), fill="#E0E0E0", width=1)
for gy in range(-1, 2):
_, py = world_to_pixel(x_min, gy)
draw.line((0, py, width – 1, py), fill="#E0E0E0", width=1)
# 2. 绘制坐标轴
x_axis_py = world_to_pixel(x_min, 0)[1]
draw.line((0, x_axis_py, width – 1, x_axis_py), fill="black", width=2)
y_axis_px = world_to_pixel(0, y_min)[0]
draw.line((y_axis_px, 0, y_axis_px, height – 1), fill="black", width=2)
# 3. 正弦曲线:密集采样,连成折线
points = []
for i in range(width * 2):
x = x_min + (x_max – x_min) * i / (width * 2 – 1)
y = math.sin(x)
px, py = world_to_pixel(x, y)
points.append((px, py))
if len(points) > 1:
draw.line(points, fill=(255, 0, 0), width=2)
img.save("sin_plot.png")
print("图像已生成")
这段代码里最核心的思路就是“折线逼近曲线”:把函数曲线看成无数密集的小线段,采样点越多,曲线越平滑。理论上直接用所有采样点调用一次
draw.line(points…)
比逐条线段画更快,Pillow 会对首尾相连的点自动连线。
还有一个值得注意的地方:我在网格里控制循环变量用了
range
,但由于
x_min
和
x_max
带 π,直接用
range(x_min, x_max)
会报类型错误。实际写的时候最好用整数索引循环,然后换算成 x 坐标值,避免浮点数比较的精度问题。
4.3 进阶:用像素操作实现曼德博集合
如果你觉得画函数曲线还不过瘾,我们来个真正的分形渲染——曼德博集合。这东西在计算机图形学里是经典案例:一个极其简单的迭代公式,却能在二维平面上生成无限复杂的图案,非常适合用来理解“逐像素着色”的概念。
曼德博集合的判断规则很简单:复平面上的点
c
,从
z=0
开始不断迭代
z = z² + c
,如果迭代若干次后
z
的模长不超过 2,就认为这个点属于集合。我们要做的就是把图片的每个像素映射到复平面的坐标区域,然后迭代计算并涂色。
直接循环
putpixel
会慢到你想砸电脑,所以我用列表先存储全部像素,最后用
putdata()
一次性写入图像,速度能提升好几倍。
from PIL import Image
width, height = 800, 600
max_iter = 100
x_min, x_max = -2.2, 0.8
y_min, y_max = -1.2, 1.2
pixels = []
for py in range(height):
for px in range(width):
# 像素坐标 -> 复平面坐标
c_real = x_min + (x_max – x_min) * px / (width – 1)
c_imag = y_min + (y_max – y_min) * py / (height – 1)
z_real, z_imag = 0.0, 0.0
iter_count = 0
while z_real * z_real + z_imag * z_imag <= 4.0 and iter_count < max_iter:
# z = z^2 + c
z_real, z_imag = (
z_real * z_real – z_imag * z_imag + c_real,
2 * z_real * z_imag + c_imag,
)
iter_count += 1
if iter_count == max_iter:
pixels.append((0, 0, 0)) # 集合内部为黑色
else:
# 简单着色:迭代次数映射到灰度
gray = int(iter_count * 255 / max_iter)
pixels.append((gray, gray, 255))
img = Image.new("RGB", (width, height))
img.putdata(pixels)
img.save("mandelbrot.png")
这个例子把前面说的像素操作发挥到了极致。你可能会注意到迭代次数的映射用了蓝色调,看起来更漂亮。实际渲染时间取决于
max_iter
和图像尺寸,800×600 下大约需要几秒到十几秒,已经完全可以接受了。如果想更快,可以换成 numpy 向量化计算,但那就是另一个故事了。
5. 常见问题与排查技巧实录
5.1 颜色与 Alpha 通道的坑
Pillow 操作中关于透明通道的报错和异常现象绝对能排进新手问题前三名。最常见的现象是:一张带透明背景的 PNG 图片用
save()
保存成 JPG 后,透明区域变成了黑色。这是因为 JPEG 格式本身不支持透明度,Pillow 在转换时会把 RGBA 图像的 alpha 通道简单丢弃。
正确做法是在保存前显式转换:
img = Image.open("with_alpha.png").convert("RGBA")
# 做一些透明合成操作…
background = Image.new("RGB", img.size, "white")
background.paste(img, (0, 0), img.split()[3])
background.save("no_alpha.jpg")
上面这种“先铺白色背景,再把原图贴上去”的做法能保留透明区域的视觉白底效果。如果你直接
.convert("RGB")
,透明像素的 RGB 分量是本来的颜色,但 alpha 信息没了,最终看到的可能是黑色或奇怪的边缘颜色。
另一个与 alpha 有关的坑出现在
Image.blend()
和
Image.composite()
里:两张图的模式和尺寸必须一致,否则会直接抛
ValueError
。我在写合成脚本时习惯先做一次断言:
assert im1.mode == im2.mode, f"模式不一致: {im1.mode} vs {im2.mode}"
assert im1.size == im2.size, f"尺寸不一致: {im1.size} vs {im2.size}"
5.2 字体与文字溢出问题的排查
关于字体的排查,我总结过一套组合拳。中文字体乱码,先用
fonts
文件夹下的系统字体列表确认 TTF 文件路径是否存在,然后检查编码,Pillow 的
text()
默认按 Unicode 处理,一般不会出现编码问题,但如果你读取的文本文件编码不对,或者用了
bytes
字符串,就会显示成乱码。
文字位置不对是我经常遇到的问题。Pillow 绘图坐标的锚点(anchor)默认是左上角,但不同字体、不同字号下文字的边距不同,直接用
(0, 0)
写文字会导致视觉上上下偏。解决办法是用
textbbox()
准确测量文字包围盒,或者指定
anchor="mm"
让锚点定位在文字中心。后者在需要多行文字居中对齐时非常好用。
绕开字体文件路径的跨平台问题,有一个取巧方案:把字体文件放进项目目录,用
pathlib
定位:
from pathlib import Path
from PIL import ImageFont
font_path = Path(__file__).parent / "fonts" / "SourceHanSansSC-Regular.otf"
font = ImageFont.truetype(str(font_path), 28)
5.3 性能优化:Pillow 绘图慢怎么办
Pillow 本身是 C 语言实现的,但 Python 层的循环却很慢。如果你需要处理超大尺寸图片,或者对每个像素做复杂计算,前面提到的
putdata()
是明智之选。另一个技巧是用
ImageDraw.line()
一次性画多段线,而不是在循环里反复调用
draw.point()
。
举个例子,我以前写过一个像素扩散模拟,在 1000×1000 的图片里逐帧更新每个像素,一开始用
putpixel
,渲染一帧要好几秒;后来改成维护一个
pixel_list
,最后一帧用
putdata()
批量写,时间立刻降到 0.3 秒。如果你觉得
putdata()
不够自由,还可以把 PIL 的
Image
对象转成字节数组,用
struct
或者
array
模块操作极快。
还有一种常见的性能问题是
resize()
大图缩略图时,如果使用默认的
resample=Image.BICUBIC
,处理几十兆的图片会比较慢。可以用
resample=Image.LANCZOS
换取质量,但如果只是预览,选
Image.NEAREST
会更快。
最后提醒一下,当你在循环中反复打开同一张图片做裁剪时,建议先
copy()
一份到内存里操作,避免反复读磁盘。
5.4 安装与环境问题速查
Pillow 的安装问题层出不穷,我整理一个速查表,遇到类似问题直接对着看:
|
ModuleNotFoundError: No module named 'PIL' |
没安装 Pillow,或安装到了其他 Python 环境 |
检查当前解释器,执行
pip install Pillow |
|
安装报错
Could not find a version… |
pip 版本太老或 Python 版本不兼容 |
升级 pip:
python -m pip install –upgrade pip |
|
SecurityError: … 或 JPEG 相关的报错 |
系统缺少 JPEG/PNG 支持库 |
Linux 先安装
libjpeg-dev 、 zlib1g-dev ,再重装 Pillow |
| 导入后版本号很低,比如 7.x | 还是老 PIL 或旧版 Pillow |
pip uninstall PIL -y 后重装, pip install -U Pillow |
| 写中文文本时方框 | 字体不支持中文 |
换成中文字体文件,如
simhei.ttf 、 msyh.ttc |
|
paste() 后透明区域是黑色 |
mode 不是 RGBA,或当成 JPEG 保存 | 统一转 RGBA,保存时转回 RGB |
这里我想多提一句“PIL”和“Pillow”的关系。PyPI 上的
PIL
已经停止维护很多年了,现在你在
pip install PIL
时看到的大多是历史版本,不要安装。正确的包名是
Pillow
,但导入时仍然写作
from PIL import xxx
,因为 Pillow 延续了 PIL 的命名空间。只要记住这个规则,环境问题基本就躲开了一半。
在实际工作中,我还会给批量处理脚本加一个定时输出,比如每处理 1000 张图片打印一次进度。别小看这个习惯,遇到图片数量多、单张几十兆的场景,它能帮你及时发现问题,而不是让程序默默跑到一半卡住。
做图像和图形学相关的小工具,Pillow 给我最大的感觉就是“透明”——一切都围绕像素和坐标展开,没有太多魔法。你画一条线,就是真的在一张像素网格上逐点落定颜色;你写一个分形,就是真的在逐像素做迭代。这种贴近底层的特质,对想深入图形学的人来说其实是很好的学习路径。万一你在实践过程中踩到了这篇文章里没提到的坑,不妨先回去检查一下模式、坐标和尺寸这三个基础量,八成问题都出在那里。



