直接用 Image.open() 读取验证码图片会失败,因 Pillow 无法自动推断无文件头或缺失 Content-Type 的二进制流格式,须用 io.BytesIO 包装 resp.content 后传入。

为什么直接用 Image.open() 读取验证码图片会失败?
很多验证码图像是 Web 返回的二进制流(比如 requests.get(url).content),直接传给 Image.open() 会报 UnidentifiedImageError。Pillow 不会自动推断格式,尤其当响应头缺失 Content-Type 或图像无文件头时。
- 必须显式用
io.BytesIO()包装二进制数据,再传给Image.open() - 如果是 base64 编码字符串,先用
base64.b64decode()解码成 bytes - 避免用
open(path, 'rb')后直接传 file object——某些 PNG/JPEG 损坏时 Pillow 仍可能报错,BytesIO更可控
import io
from PIL import Image
import requests
<p>resp = requests.get('<a href="https://www.php.cn/link/ae26e93e0eba7ce2ab8bccb5a801313a">https://www.php.cn/link/ae26e93e0eba7ce2ab8bccb5a801313a</a>')
img = Image.open(io.BytesIO(resp.content)) # ✅ 正确</p><h1>img = Image.open(resp.content) # ❌ 报错</h1>灰度化 + 二值化时,threshold 值怎么选才不丢笔画?
验证码常见干扰是浅色噪点或细线,但设太高阈值(如 200)会让细字体断裂;设太低(如 100)又留太多噪点。不能靠经验固定值。
- 先转
L模式(灰度),再用image.point(lambda x: 0 if x < threshold else 255, '1')二值化 - 推荐用局部自适应阈值:调用
image.filter(ImageFilter.MedianFilter())去椒盐噪点,再用ImageOps.autocontrast()拉伸对比度,最后用 Otsu 方法(需额外 opencv)或试几个值观察效果 - 对纯色背景验证码,可先用
image.convert('RGB').getcolors()统计主色,把背景色设为阈值下界
ImageEnhance.Contrast 和 ImageEnhance.Sharpness 调多少才不会放大噪点?
增强对比度或锐化确实能提升字符边缘,但过度使用会让噪点变粗、粘连。
-
Contrast的 factor > 1.5 就容易过曝,建议 1.1–1.3 区间试探;factor < 1 会降低对比,一般不用 -
Sharpnessfactor 在 1.0–1.8 之间较安全;超过 2.0 后高频噪声会被显著放大,尤其对 JPG 压缩图 - 更稳妥的做法是:先用
ImageFilter.GaussianBlur(radius=0.5)轻度模糊去毛刺,再锐化,形成“模糊+锐化”组合(类似 USM 锐化)
旋转倾斜校正时,image.rotate() 的 expand 和 fillcolor 怎么配?
有些验证码故意轻微旋转(±5°),直接用 rotate(angle) 会导致边缘裁切或出现黑边,影响后续 OCR。
立即学习“Python免费学习笔记(深入)”;
- 必须设
expand=True,否则图像尺寸不变,内容被截断 - 黑边默认填黑色(0),但若原图是白底黑字,黑边会干扰二值化——应设
fillcolor=255(白色) - 角度估计不准时,不要硬凑整数:用
skimage.transform.hough_line或投影法找基线,再传给rotate();简单场景可用image.histogram()扫描垂直投影峰值偏移估算倾斜角
预处理链里最易被跳过的环节是「确认二值化后是否仍有单像素噪点」——它们在视觉上几乎看不见,却足以让 Tesseract 把 “0” 识别成 “8”。手动用 image.filter(ImageFilter.MinFilter(3)) 过一遍再检查,比反复调阈值更省时间。


















