本节摘要:ImageDraw 是 Pillow 的 2D 绘图模块,可画直线、矩形、椭圆、多边形、弧线,并添加文字。本节详解 Draw 对象、各类形状的坐标参数、颜色表示(命名色/RGB/RGBA),以及字体加载、文字定位与对齐,重点演示水印与标注这两个高频场景。
阅读完本节,你应当能够:
图片处理经常需要"往图上加东西":照片加水印、截图加标注、海报加文字。Pillow 的 ImageDraw 就是干这个的——它在图像上覆盖一层"透明绘图板",所有绘图命令都落在上面,最终与图像合成。
from PIL import Image, ImageDraw img = Image.new("RGB", (400, 300), "white") draw = ImageDraw.Draw(img) draw.line([(50, 50), (350, 50)], fill="red", width=3) img.save("draw_demo.png")
创建 Draw 对象、调用绘图方法、保存——三步完成一次绘图。Draw 对象绑定一个 Image,绘图直接改这张图(原地修改,不是返回新图)。
# 直线/折线 draw.line([(10, 10), (100, 10), (100, 100)], fill="blue", width=2, joint="curve") # 矩形(左上、右下) draw.rectangle([(20, 20), (120, 80)], fill="red", outline="black", width=2) # 椭圆/圆(外接矩形) draw.ellipse([(140, 20), (240, 80)], fill="green", outline="black") # 多边形(顶点列表) draw.polygon([(260, 80), (310, 20), (360, 80)], fill="orange", outline="black") # 弧线(外接矩形 + 起始/结束角度) draw.arc([(20, 100), (120, 200)], start=0, end=180, fill="purple", width=3) # 扇形(带填充的弧) draw.pieslice([(140, 100), (240, 200)], start=0, end=120, fill="cyan", outline="black") # 圆点(单像素点) draw.point((100, 250), fill="black")
坐标规则:所有形状都用"外接矩形"或"顶点列表"描述。矩形、椭圆用 (左上, 右下) 两点,多边形用顶点序列。
draw.rectangle([(0,0),(50,50)], fill="red") # 命名色 draw.rectangle([(60,0),(110,50)], fill=(255, 0, 0)) # RGB 元组 draw.rectangle([(120,0),(170,50)], fill=(255, 0, 0, 128)) # RGBA(半透明) draw.rectangle([(180,0),(230,50)], fill="#FF0000") # 十六进制
💡 关键直觉:RGBA 元组能画半透明。fill=(255,0,0,128) 的红色是 50% 透明——做水印、蒙层时这个能力极好用。注意半透明需要图像本身是 RGBA 模式。
from PIL import Image, ImageDraw, ImageFont img = Image.new("RGB", (600, 200), "white") draw = ImageDraw.Draw(img) # 加载系统字体(Windows 示例) font = ImageFont.truetype("C:/Windows/Fonts/arial.ttf", 40) # Linux 示例: 系统字体目录下的 DejaVuSans 字体 # 画文字(xy 是文字左上角) draw.text((50, 50), "Hello Pillow", fill="black", font=font) # 获取文字边界,用于居中 bbox = draw.textbbox((0, 0), "Hello Pillow", font=font) text_w = bbox[2] - bbox[0] text_h = bbox[3] - bbox[1]
⚠️ 常见坑:中文字体找不到。系统默认字体常不支持中文(出现方框)。中文必须显式指定支持中文的字体文件——Windows 用
msyh.ttc(微软雅黑)、simhei.ttf(黑体),Linux 用 Noto Sans CJK 等。字体文件缺失是"中文变方块"的头号原因。
from PIL import Image, ImageDraw, ImageFont img = Image.open("photo.jpg").convert("RGBA") # 建透明层画水印 watermark = Image.new("RGBA", img.size, (0, 0, 0, 0)) draw = ImageDraw.Draw(watermark) font = ImageFont.truetype("C:/Windows/Fonts/msyh.ttc", 60) text = "© 2026 MySite" # 右下角定位 w, h = img.size bbox = draw.textbbox((0, 0), text, font=font) tw, th = bbox[2]-bbox[0], bbox[3]-bbox[1] draw.text((w - tw - 30, h - th - 30), text, fill=(255, 255, 255, 160), font=font) # 合成 out = Image.alpha_composite(img, watermark) out.convert("RGB").save("watermarked.jpg", quality=92)
水印的关键技巧:先在透明层上画,再 alpha_composite 合成——这样水印半透明、可定位、可复用。
from PIL import Image, ImageDraw img = Image.open("photo.jpg").convert("RGB") draw = ImageDraw.Draw(img) # 框出人脸区域并标注 draw.rectangle([(100, 80), (220, 260)], outline="red", width=4) draw.ellipse([(100, 40), (220, 80)], fill="red") # 标注小标签 font = ImageFont.truetype("C:/Windows/Fonts/arial.ttf", 20) draw.text((110, 46), "Face", fill="white", font=font) img.save("annotated.jpg")
标注在目标检测、数据标注、调试可视化里是刚需——框选 + 小标签这个组合可以复用到任何"给图上打标记"的场景。
from PIL import Image, ImageDraw def rounded_rect(img, radius=20): draw = ImageDraw.Draw(img) draw.rounded_rectangle([(10, 10), (200, 100)], radius=radius, fill="skyblue", outline="navy", width=2) return img img = Image.new("RGB", (220, 120), "white") rounded_rect(img, 25).save("rounded.jpg")
rounded_rectangle 是 Pillow 9.1+ 内置的圆角矩形方法——做按钮、头像框、卡片很常用,不用手动拼弧线。
import textwrap from PIL import Image, ImageDraw, ImageFont def draw_wrapped_text(img, text, box, font, fill="black", line_spacing=8): draw = ImageDraw.Draw(img) x0, y0, x1, y1 = box max_width = x1 - x0 lines = textwrap.wrap(text, width=20) # 按字符数粗分 y = y0 for line in lines: draw.text((x0, y), line, fill=fill, font=font) y += font.size + line_spacing return img img = Image.new("RGB", (400, 300), "white") font = ImageFont.truetype("C:/Windows/Fonts/msyh.ttc", 20) draw_wrapped_text(img, "这是自动换行的示例文字,超过宽度会分多行显示。", (20, 20, 380, 280), font) img.save("wrapped_text.jpg")
自动换行:textwrap 按字符粗分 + 逐行绘制,是最简单的方案。中文按字符换行没问题,英文按词换行要额外处理。
💡 关键直觉:文字定位的痛点是"算尺寸"。textbbox 拿到文字实际宽高,才能居中、右对齐、右下角定位。记住:先算 bbox,再定坐标——别凭感觉写死位置。
绘图能力还能做简单的图表——不依赖 matplotlib 也能画柱状图:
from PIL import Image, ImageDraw, ImageFont def bar_chart(data, labels, title="", size=(400, 300)): img = Image.new("RGB", size, "white") draw = ImageDraw.Draw(img) W, H = size font = ImageFont.truetype("C:/Windows/Fonts/arial.ttf", 14) # 标题 draw.text((10, 10), title, fill="black", font=font) # 柱状区域(留出边距) left, top, right, bottom = 60, 50, W-20, H-40 max_val = max(data) n = len(data) bar_w = (right - left) / n * 0.7 gap = (right - left) / n for i, (val, label) in enumerate(zip(data, labels)): x0 = left + i * gap + (gap - bar_w) / 2 h = (val / max_val) * (bottom - top) draw.rectangle([x0, bottom - h, x0 + bar_w, bottom], fill="steelblue", outline="black") # 数值标签 draw.text((x0, bottom - h - 20), str(val), fill="black", font=font) # 横轴标签 draw.text((x0, bottom + 5), label, fill="black", font=font) return img chart = bar_chart([85, 92, 78, 95, 88], ["A", "B", "C", "D", "E"], "月度销量") chart.save("chart.png")
纯 Pillow 画柱状图:矩形=柱子,textbbox 定位标签。轻量场景(无 matplotlib 依赖)用它最合适,复杂图表还是 matplotlib 专业。
绘图能力的高级应用——生成证书/奖状类模板(含边框、标题、落款):
from PIL import Image, ImageDraw, ImageFont def make_certificate(name, size=(900, 600)): img = Image.new("RGB", size, "ivory") draw = ImageDraw.Draw(img) # 双层边框 draw.rectangle([(20, 20), (size[0]-20, size[1]-20)], outline="darkgoldenrod", width=4) draw.rectangle([(35, 35), (size[0]-35, size[1]-35)], outline="darkgoldenrod", width=2) # 标题 font_title = ImageFont.truetype("C:/Windows/Fonts/msyh.ttc", 72) bbox = draw.textbbox((0, 0), "荣誉证书", font=font_title) tw, th = bbox[2]-bbox[0], bbox[3]-bbox[1] draw.text(((size[0]-tw)//2, 90), "荣誉证书", fill="darkgoldenrod", font=font_title) # 正文 font_body = ImageFont.truetype("C:/Windows/Fonts/msyh.ttc", 36) text = f"兹证明 {name} 同学在本次学习中表现优秀,特发此证。" bbox = draw.textbbox((0, 0), text, font=font_body) tw, th = bbox[2]-bbox[0], bbox[3]-bbox[1] draw.text(((size[0]-tw)//2, 260), text, fill="black", font=font_body) # 落款 font_sign = ImageFont.truetype("C:/Windows/Fonts/msyh.ttc", 28) draw.text((size[0]-260, size[1]-110), "2026年8月", fill="gray", font=font_sign) return img make_certificate("张三").save("certificate.jpg")
证书生成的设计模式:边框(矩形嵌套)→ 标题(大字体居中)→ 正文(居中)→ 落款(右下角)。这套"元素分层 + 定位"模式,可以复用到名片、邀请函、海报、表单等任何图文排版需求——Pillow 不只是图像处理库,也是轻量排版引擎。
把绘图能力组装成一个"简易海报生成器"——背景 + 标题 + 副标题 + 装饰线:
from PIL import Image, ImageDraw, ImageFont def make_poster(title, subtitle, size=(800, 500), bg="white", title_color="black", accent="tomato"): img = Image.new("RGB", size, bg) draw = ImageDraw.Draw(img) draw.rectangle([(0, 0), (size[0], 10)], fill=accent) # 顶部装饰线 font_title = ImageFont.truetype("C:/Windows/Fonts/msyh.ttc", 64) bbox = draw.textbbox((0, 0), title, font=font_title) tw, th = bbox[2]-bbox[0], bbox[3]-bbox[1] draw.text(((size[0]-tw)//2, 140), title, fill=title_color, font=font_title) font_sub = ImageFont.truetype("C:/Windows/Fonts/msyh.ttc", 32) bbox = draw.textbbox((0, 0), subtitle, font=font_sub) tw, th = bbox[2]-bbox[0], bbox[3]-bbox[1] draw.text(((size[0]-tw)//2, 280), subtitle, fill="gray", font=font_sub) draw.rectangle([(size[0]//4, size[1]-20), (size[0]*3//4, size[1]-12)], fill=accent) return img make_poster("Pillow 绘图实战", "从形状到海报,一次学会", accent="steelblue").save("poster.png")
海报生成器的设计模式:装饰线(矩形)→ 标题(大字体居中)→ 副标题(小字体)→ 底部元素。这套"元素组合 + 居中定位"的套路,是生成任何图文视觉(封面、卡片、水印图)的基础。
from PIL import Image, ImageDraw img = Image.new("RGB", (400, 200), "white") draw = ImageDraw.Draw(img) draw.rectangle([(20, 20), (100, 100)], outline="red", width=4) # 描边宽度 draw.rectangle([(120, 20), (200, 100)], fill=(0, 0, 255, 100)) # 半透明 for x in range(220, 380, 20): # 虚线 draw.line([(x, 60), (x+10, 60)], fill="green", width=3) draw.rounded_rectangle([(20, 120), (180, 180)], radius=20, # 圆角 fill="orange", outline="brown", width=2) img.save("draw_styles.jpg")
半透明与圆角是现代 UI 感的关键——做卡片、按钮、标签时最常用。
import textwrap from PIL import Image, ImageDraw, ImageFont def draw_text_block(img, text, box, font, fill="black", line_spacing=10, align="left"): draw = ImageDraw.Draw(img) x0, y0, x1, y1 = box max_width = x1 - x0 chars_per_line = max(1, max_width // font.size) lines = textwrap.wrap(text, width=chars_per_line) y = y0 for line in lines: bbox = draw.textbbox((0, 0), line, font=font) lw = bbox[2] - bbox[0] x = x0 if align == "center": x = x0 + (max_width - lw) // 2 elif align == "right": x = x1 - lw draw.text((x, y), line, fill=fill, font=font) y += font.size + line_spacing return img img = Image.new("RGB", (400, 300), "white") font = ImageFont.truetype("C:/Windows/Fonts/msyh.ttc", 24) draw_text_block(img, "这是一段需要自动换行的说明文字,测试中文字符的换行效果。", (30, 30, 370, 270), font, align="center") img.save("text_block.jpg")
换行三要素:字符宽度估算(font.size)、textwrap 分行、逐行 y 递增。
问:draw 之后图片没变化?
Draw 对象原地修改绑定的 Image——检查是否把 draw 绑定到了错误的 Image(比如绑定了副本却保存原图)。另外 draw 后不需要提交,直接保存 Image 即可。
问:文字怎么居中?
先 bbox = draw.textbbox((0,0), text, font) 得到宽高,再算偏移:x = (W - tw) // 2。垂直居中注意减去 bbox 的 top:y = (H - th) // 2 - bbox[1]。
问:怎么让文字带描边/阴影?
先画偏移 2px 的黑色文字(阴影),再画原文字(前景)——"两次绘制叠阴影"是经典技巧。描边可用 draw.text 的 stroke_width/stroke_fill 参数。
问:字体太大找不到怎么办?
用 ImageFont.truetype("arial.ttf", size) 试系统默认字体;不行就列目录找字体文件(Windows 的 Fonts 目录、Linux 的系统字体目录)。
问:draw.text 的 anchor 参数是什么?
anchor 定义文本对齐锚点:anchor="mm"(中心对齐)、"lt"(左上)、"rb"(右下)等。用 anchor 可以免去手动计算偏移。
ImageDraw 不止画标准形状,还能通过"多边形"自由创作,配合遮罩实现复杂效果:
from PIL import Image, ImageDraw def draw_star(img, cx, cy, outer, inner, fill="gold", points=5): """绘制五角星(多边形顶点计算)""" import math draw = ImageDraw.Draw(img) vertices = [] for i in range(points * 2): r = outer if i % 2 == 0 else inner angle = math.pi / 2 + i * math.pi / points x = cx + r * math.cos(angle) y = cy - r * math.sin(angle) vertices.append((x, y)) draw.polygon(vertices, fill=fill) return img def rounded_image(img, radius=30): """圆角遮罩:用 L 模式 mask 实现透明圆角""" mask = Image.new("L", img.size, 0) draw = ImageDraw.Draw(mask) draw.rounded_rectangle([(0, 0), img.size], radius=radius, fill=255) result = img.convert("RGBA") result.putalpha(mask) # 用 mask 作为透明度 return result img = Image.new("RGB", (200, 200), "white") draw_star(img, 100, 100, 90, 40) img.save("star.png") photo = Image.open("photo.jpg") rounded_image(photo, 40).save("rounded_photo.png")
遮罩绘制的核心:L 模式图当透明度(putalpha)——白色区域显示、黑色透明。**"画 mask → 应用为透明度"**是实现圆角、异形裁剪、渐变遮罩的通用方法,比"抠图"简单可靠得多。
把绘图能力组合成"分享卡片生成器"(社交分享图):背景渐变 + 标题 + 摘要 + 品牌区:
from PIL import Image, ImageDraw, ImageFont def share_card(title, summary, brand="MyApp", size=(1080, 1350)): """生成竖版分享卡片""" img = Image.new("RGB", size, "#f8f9fa") draw = ImageDraw.Draw(img) # 顶部色块(品牌区) draw.rectangle([(0, 0), (size[0], 200)], fill="#2c3e50") # 品牌文字 font_brand = ImageFont.truetype("C:/Windows/Fonts/msyh.ttc", 40) draw.text((60, 70), brand, fill="white", font=font_brand) # 主标题(大字) font_title = ImageFont.truetype("C:/Windows/Fonts/msyh.ttc", 72) bbox = draw.textbbox((0, 0), title, font=font_title) tw, th = bbox[2]-bbox[0], bbox[3]-bbox[1] draw.text(((size[0]-tw)//2, 400), title, fill="#1a1a1a", font=font_title) # 摘要(自动换行) font_body = ImageFont.truetype("C:/Windows/Fonts/msyh.ttc", 36) chars = max(1, (size[0]-120) // font_body.size) import textwrap lines = textwrap.wrap(summary, width=chars) y = 560 for line in lines[:4]: # 最多 4 行 draw.text((60, y), line, fill="#444", font=font_body) y += 52 # 底部装饰线 draw.rectangle([(60, size[1]-80), (size[0]-60, size[1]-70)], fill="#2c3e50") return img card = share_card("Pillow 图像处理实战", "从基础操作到实战项目,一本掌握 Python 图像处理的核心能力。", brand="Python 教程") card.save("share_card.png")
分享卡片的设计模式:品牌区(色块+文字)→ 主标题(居中大字)→ 摘要(自动换行)→ 装饰线收尾。**"分层排版 + 定位计算"**是所有图文卡片(公众号封面、短视频封面、营销图)的通用结构——掌握了这个模式,就能举一反三做各种视觉素材。
绘图在大量元素时的性能与习惯,值得注意:
from PIL import Image, ImageDraw import time # 性能注意:一个 Draw 对象画很多元素比多次新建快 img = Image.new("RGB", (800, 600), "white") draw = ImageDraw.Draw(img) t0 = time.time() for i in range(500): x, y = (i*3 % 780), (i*7 % 560) draw.rectangle([x, y, x+20, y+20], outline="steelblue", width=2) print(f"一个 Draw 画 500 个矩形: {time.time()-t0:.3f}s") # 慢:每次新建 Draw(避免这种写法) t0 = time.time() for i in range(500): d = ImageDraw.Draw(img) # 每次新建 x, y = (i*3 % 780), (i*7 % 560) d.rectangle([x, y, x+20, y+20], outline="tomato", width=2) print(f"500 次新建 Draw: {time.time()-t0:.3f}s")
绘图性能要点:
最佳实践:draw = ImageDraw.Draw(img) 一次创建、全部使用;复杂场景先画到透明层再合成(alpha_composite),避免频繁改像素。
| 症状 | 可能原因 | 解决 |
|---|---|---|
| 画的形状不见了 | 绑定错误的 Image | 确认 draw 绑定目标图 |
| 颜色不对 | 模式不支持 RGBA | 先 convert("RGBA") |
| 文字变方框 | 字体不支持中文 | 指定中文字体 |
| 位置偏移 | bbox 未用/算错 | 用 textbbox 实时计算 |
| 半透明失效 | 图是 RGB 模式 | 转 RGBA 再画 |
排查思路:先确认"画在哪张图"(draw 的绑定对象),再检查"模式与字体"(RGBA/中文),最后验证"坐标计算"。绘图的 80% 问题出在"绑错图"与"字体路径"——这两项先查。
把绘图能力用于"程序化图表生成"——不依赖 matplotlib 的动态图表:
from PIL import Image, ImageDraw, ImageFont def line_chart(data, labels, size=(500, 320), title=""): """折线图:点+线+标签""" img = Image.new("RGB", size, "white") draw = ImageDraw.Draw(img) font = ImageFont.truetype("C:/Windows/Fonts/arial.ttf", 14) W, H = size # 坐标区 left, top, right, bottom = 60, 40, W-30, H-40 draw.line([(left, bottom), (right, bottom)], fill="black", width=2) # X轴 draw.line([(left, top), (left, bottom)], fill="black", width=2) # Y轴 # 数据点 max_val = max(data) n = len(data) points = [] for i, (val, label) in enumerate(zip(data, labels)): x = left + (right-left) * i / max(n-1, 1) y = bottom - (bottom-top) * val / max_val points.append((x, y)) draw.ellipse([x-4, y-4, x+4, y+4], fill="steelblue") # 数据点 draw.text((x-10, bottom+8), label, fill="black", font=font) # X标签 draw.text((x+6, y-20), str(val), fill="gray", font=font) # 数值 # 折线 if len(points) > 1: draw.line(points, fill="steelblue", width=2) return img chart = line_chart([65, 78, 90, 72, 85], ["一", "二", "三", "四", "五"], title="周销量") chart.save("line_chart.png")
程序化图表的通用模式:坐标系(轴线)→ 数据映射(值→坐标)→ 元素绘制(点/线/标签)。"数据→坐标→绘制"的三段式是任何程序化图表(折线/柱状/饼图)的骨架——改映射逻辑即可生成不同类型图表。这个能力在"报表生成、监控大屏、动态图"中非常实用。
把绘图能力组织成"从零生成一张图文卡片"的完整工作流:
from PIL import Image, ImageDraw, ImageFont import textwrap def create_info_card(title, body, footer, size=(800, 500), accent="#2c3e50"): """完整图文卡片:背景+标题+正文+页脚""" img = Image.new("RGB", size, "#f8f9fa") draw = ImageDraw.Draw(img) # 顶部色带 draw.rectangle([(0, 0), (size[0], 16)], fill=accent) # 标题 font_title = ImageFont.truetype("C:/Windows/Fonts/msyh.ttc", 44) draw.text((40, 50), title, fill="#1a1a1a", font=font_title) # 分隔线 draw.line([(40, 120), (size[0]-40, 120)], fill="#ddd", width=2) # 正文(自动换行) font_body = ImageFont.truetype("C:/Windows/Fonts/msyh.ttc", 26) lines = textwrap.wrap(body, width=(size[0]-80)//font_body.size) y = 150 for line in lines[:6]: draw.text((40, y), line, fill="#333", font=font_body) y += 40 # 页脚 font_footer = ImageFont.truetype("C:/Windows/Fonts/msyh.ttc", 20) draw.text((40, size[1]-50), footer, fill="#888", font=font_footer) return img card = create_info_card( "图像处理速查卡", "Pillow 核心操作:打开、保存、裁剪、缩放、旋转、滤镜、绘图、通道、序列。", "© Python 图像处理教程 · 动手优先") card.save("info_card.png")
图文卡片的完整流程:底色 → 色带 → 标题 → 分隔线 → 正文(换行)→ 页脚——"从上到下分层绘制"是图文排版的通用顺序。这个工作流可以生成信息卡、速查卡、通知图、数据摘要图等各种"程序化视觉内容"。
绘图出错时的调试方法:
from PIL import Image, ImageDraw, ImageFont # 1. 用"边界框"调试(可视化文字/形状的位置) def debug_bounds(draw, box, label="", color="red"): draw.rectangle(box, outline=color, width=2) if label: font = ImageFont.truetype("C:/Windows/Fonts/arial.ttf", 14) draw.text((box[0], box[1]-18), label, fill=color, font=font) img = Image.new("RGB", (400, 300), "white") draw = ImageDraw.Draw(img) # 画标题并调试其边界 font = ImageFont.truetype("C:/Windows/Fonts/msyh.ttc", 30) bbox = draw.textbbox((50, 50), "Hello", font=font) debug_bounds(draw, (50, 50, 50 + bbox[2], 50 + bbox[3]), "标题") draw.text((50, 50), "Hello", fill="black", font=font) img.save("debug_draw.jpg")
调试方法:边界框可视化(看元素实际位置)、分步绘制(每步保存)、纯色背景测试(排除干扰)。"看不见的位置就用框画出来"——绘图调试的核心是把"隐形的坐标"变成"可见的框"。
绘图能在图上"加东西",下一节拆开看"图里有什么"——通道操作,把 RGB 拆成三张单色图再重组。
绘图能在图上"加东西",下一节拆开看"图里有什么"——通道操作,把 RGB 拆成三张单色图再重组。
绘图能在图上"加东西",下一节拆开看"图里有什么"——通道操作,把 RGB 拆成三张单色图再重组。