Python+OCR实现图片指定区域文字识别与批量重命名
简介:OCR(光学字符识别)技术能够将图片中的文字转化为可编辑的文本,是文档数字化和自动化处理的核心基础。在实际工程中,单纯调用OCR接口识别整张图片往往面临表格干扰、文本顺序混乱、性能开销大等问题。更高效的做法是先定位并裁切目标区域,再做图像预处理,最后通过OCR提取关键字段,结合Python脚本实现批量文件重命名。这种方案在发票归档、证件扫描件整理、快递面单留存等场景中广泛适用。本文从技术原理出发,介绍PaddleOCR等工具的选择与搭配,讲解图像裁剪、灰度化、放大、二值化等预处理对识别率的提升,并给出完整的Python批量处理代码,帮助读者快速落地一套可靠的图片文字识别与自动命名流程。 写这篇东西的起因挺实在的:领导扔给我几百张扫描出来的JPG单据,每张图片的右上角都印着一串业务编号,要求按这串编号把文件名全部改掉。手改肯定不现实,所以干脆写了个Python脚本——裁切指定区域、OCR识别文字、批量重命名,一条龙跑完。
今天这篇文章,就把这个方案的完整落地过程拆开讲清楚。它解决的核心问题非常具体:在图片的某个固定位置提取文字,并把识别结果作为新的文件名。这个能力在发票归档、证件扫描件整理、快递面单留存、化验单录入这类场景里非常常用,只要你的图片内容版式相对固定,这套代码基本可以照搬。
读这篇文章的人,我默认你:会用Python写基础脚本;装了Python但没用过OCR相关库;有大批量图片要处理,但不想一个个手动重命名。如果你满足这三点,下面内容可以直接照着操作。
1. 需求拆解与整体方案选型
1.1 核心难点不只是"识别文字"
先把需求拆开。很多人第一反应是:这不就是调用一下OCR吗?其实不然,"识别图片中的文字"和"识别图片中指定位置的文字"是两个难度完全不同的事情。
整张图识别的问题在于:扫描件里常有表格线、印章、手写体、二维码,这些都会干扰识别;整张识别的文本顺序是乱的,你要从一堆字符串里再筛选需要的字段,逻辑更复杂;识别耗时也更高,批量处理时成倍放大成本。
如果目标是"只关心固定位置的那一小段文字",那正确的做法是先按坐标把目标区域裁切出来,再对裁切后的局部图片做识别。这样做有三个明显好处:
- 排除其他区域文字的干扰,识别结果干净;
- OCR输入只有一小块,速度更快;
- 结果只用做一次简单的清洗就能用于文件名。
所以整体方案的顺序很关键:先裁剪,再识别,最后重命名。裁剪的坐标来源于对图片版式的观察和标定,识别用OCR引擎,重命名用Python标准库的os.rename或pathlib。
1.2 技术选型:OCR引擎的选择与对比
OCR引擎是这套方案的核心依赖。我在实际开发中同时测试过多种方案,这里把结论直接给大家:
| 方案 | 准确率(中文) | 依赖复杂度 | 批量速度 | 推荐场景 |
|---|---|---|---|---|
| PaddleOCR | 高 | 需要安装paddlepaddle,体积较大 | 快,支持批处理 | 扫描件、单据、中文为主 |
| Tesseract + pytesseract | 中 | 需要额外安装tesseract可执行文件和中文字库 | 中等 | 英文为主、轻量场景 |
| 云OCR API(如百度/腾讯) | 很高 | 需要联网、申请key | 受网络限制 | 精度要求极高且允许联网 |
| EasyOCR | 中高 | torch依赖较重 | 中等 | 不想用Paddle全家桶时 |
个人推荐:如果处理的全是中文单据,无脑选PaddleOCR;如果只是偶尔处理几张英文图片,或者对离线安装体积有要求,可以退而求其次用Tesseract。
有人担心PaddleOCR依赖太大不好装,这个我在第2节会给出完整安装步骤,实测在Windows和Linux上都能顺利装完。核心思路是:先装CPU版paddlepaddle,再装paddleocr,避免默认拉取GPU版导致环境冲突。
1.3 整体流程设计
把方案串起来,流程大致是这样:
- 准备一批JPG图片,放到同一个输入目录;
- 打开第一张图,人工确定目标文字所在的矩形区域坐标(left, top, right, bottom);
- 代码循环读取每张图片,按同一组坐标裁切;
- 对裁切图做预处理(灰度化、放大、二值化等);
- 送入OCR引擎识别,得到文本;
- 清洗文本中的空白和非法字符;
- 用rename将原文件重命名为"识别文字.jpg";
- 遇到重名文件自动追加后缀,防止覆盖。
这套流程看起来简单,但每一环都有可优化的点。尤其第4步的预处理,别看它不起眼,对识别准确率的影响可能比换引擎还大。后面我会专门用一节讲这块。
2. 环境准备与依赖安装
2.1 基础环境:Python版本与虚拟环境
写代码之前先把环境搭好。我用的是Python 3.9到3.11之间都跑通过,建议不要低于3.8,因为新版PaddleOCR对老版本的支持已经很少了。
强烈建议用虚拟环境,不要直接装到系统Python里。原因很现实:PaddlePaddle的依赖版本比较敏感,直接全局安装容易和项目里其他库打架。我的做法是:
python -m venv ocr_env
# Windows激活
ocr_env\Scripts\activate
# Linux / macOS激活
source ocr_env/bin/activate
2.2 安装图像处理与OCR依赖
激活虚拟环境后,依次安装如下依赖:
pip install pillow
pip install paddlepaddle==2.6.0
pip install paddleocr==2.7.0
这里有个特别容易踩的坑:paddlepaddle和paddleocr的版本必须匹配。如果直接pip install paddleocr而不指定版本,它可能拉取新版paddleocr,进而要求新版paddlepaddle,两者组合反而容易出问题。我的建议是固定版本号,实测最稳的组合是paddlepaddle 2.6.0 + paddleocr 2.7.0。
如果还需要用Tesseract做备选,再装:
pip install pytesseract
注意,pytesseract只是Python封装,真正的tesseract可执行文件需要单独下载安装,Windows上可以从GitHub的ub-mannheim/tesseract仓库下载安装包,安装时记得勾选中文语言包。这一步经常被忽略,导致运行时报"tesseract is not installed or it's not in your PATH"。
2.3 验证OCR引擎是否可用
装完之后不要急着写业务代码,先跑一个冒烟测试:
from paddleocr import PaddleOCR
ocr = PaddleOCR(use_angle_cls=True, lang='ch', show_log=False)
result = ocr.ocr('test.jpg', cls=True)
for line in result[0]:
print(line[1][0], line[1][1])
如果能正确打印出图片里的文字和置信度,说明PaddleOCR可以正常工作。首次运行时会自动下载检测模型、方向分类模型和识别模型,需要联网,下载一次之后会缓存在本机。
我在真实项目中用到的PaddleOCR参数就这几个:use_angle_cls开启方向分类,lang='ch'指定中文模型,show_log=False关闭日志避免刷屏。后面还会提到,在批量场景里可以传入多个图片路径做批处理,进一步提速。
2.4 准备测试图片并标定坐标
在开始写正式代码前,有一件最容易被人忽略的事:确认目标区域的坐标。
坐标的获取方式很多,最常见的两种:
- Windows上直接用系统自带"画图"打开图片,把鼠标移到目标文字的左上角和右下角,状态栏会显示像素坐标,记录下来即可。
- macOS上用"预览",打开"显示"菜单里的"显示标记工具栏",也可以看到坐标信息;或者用Python简单打印图片尺寸后根据比例估算。
还有一个更可靠的土办法:写一个几十行的小脚本,把图片显示出来,用鼠标在图像上点击两个点,打印坐标。这个办法在图片很多且版式不统一时特别好用,后面4.3节我会给出示例代码。
标定坐标时有几个细节要注意:
- 坐标单位是像素,和图片本身的像素尺寸有关,同一模板图片坐标可以复用;
- 如果原图是A4扫描件,300 DPI下分辨率约2480x3508,和150 DPI的扫描件坐标换算要小心,不能直接套用;
- 尽量让矩形区域比文字本身略大一圈,留出边距,避免文字被裁掉一半导致识别失败。
3. 核心代码实现与关键细节
3.1 图片读取与按区域裁切
用Pillow读取图片并裁切区域的代码非常直接:
from PIL import Image
img = Image.open('scan_001.jpg')
# (left, top, right, bottom)
crop_box = (120, 180, 620, 240)
crop_img = img.crop(crop_box)
crop_img.save('crop_001.png')
这里有个值得强调的细节:对于OCR输入,我建议先裁切,再做灰度化、二值化等处理,顺序不要反。如果先整图灰度化再裁切也可以,但没有必要;Pillow的crop操作本身不会修改像素数据,先裁切后处理的逻辑更清晰,也便于调试。
裁切后的图片建议保存成PNG而不是JPG。因为JPG是压缩格式,会把边缘轮廓压出很多噪点,而PNG无损,OCR对细节更友好。调试阶段把裁切图保存下来,也能很直观地检查坐标有没有偏。
3.2 图像预处理:提升识别率的四个操作
这是整个方案里回报率最高、也最容易被忽视的一环。我的经验是:预处理做得好不好,对中文识别率的影响甚至可以到10个百分点以上。
针对"固定位置文字识别"这个场景,我常用的预处理组合拳是:
- 灰度化:把彩色图转成单通道,减少颜色干扰。
- 放大2到3倍:PaddleOCR对高度在16到64像素左右的文字识别最稳,如果你的目标区域里文字高度只有十来个像素,直接识别容易翻车,放大之后明显改善。这里用最近邻插值效果不好,建议用BICUBIC。示例代码:
crop_gray = crop_img.convert('L')
w, h = crop_gray.size
crop_scaled = crop_gray.resize((w * 2, h * 2), Image.BICUBIC)
- 二值化:文字和背景对比度不够时,用二值化把背景压成纯白。Pillow自带point函数可以传阈值,但不灵活。我一般用OpenCV做Otsu阈值:
import cv2
import numpy as np
arr = np.array(crop_scaled)
_, binary = cv2.threshold(arr, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
- 去噪:扫描件经常有底纹噪点,可以用高斯模糊或者中值滤波来清除。注意别过度,模糊过头会把笔画也糊掉。
需要说明的是,不是每张图都需要完整跑完这四步。我的建议是:先直接用原裁切图试一次,看识别结果怎么样;如果效果不理想,再逐步加预处理。盲目上全套预处理反而可能让清晰的图片变差。
3.3 OCR识别与文本清洗
把预处理好的图片送入PaddleOCR:
from paddleocr import PaddleOCR
ocr = PaddleOCR(use_angle_cls=True, lang='ch', show_log=False)
result = ocr.ocr(preprocessed_img, cls=True)
if result and result[0]:
# 取置信度最高的一行,或者第一行
line = max(result[0], key=lambda x: x[1][1])
raw_text = line[1][0].strip()
else:
raw_text = ''
识别返回的是一个列表,每个元素包含一个文本框坐标和对应的识别文本及置信度。由于我们已经裁切到非常小的区域,一般只会有一行文本,所以直接取置信度最高的一行即可。
拿到原始文本后,文件名清洗是很多人容易忘的步骤。因为识别结果里常见的问题是带空格、多余标点,甚至可能是全角字符,这些字符有些是Windows文件名的非法字符,不处理会直接报错。我的清洗函数长这样:
import re
def clean_filename(text):
# 去掉首尾空白、中间的连续空白
text = re.sub(r'\s+', ' ', text).strip()
# 去掉Windows非法字符:\ / : * ? " < > |
text = re.sub(r'[\\/:*?"<>|]', '_', text)
# 如果出现点号开头(隐藏文件),去掉点号
if text.startswith('.'):
text = text.lstrip('.')
return text
这一步要尽量保守,只做安全替换,不要试图"智能"纠正识别错的字。因为纠错逻辑一旦写坏,把对的东西改错,比识别错更麻烦。
3.4 文件重命名与重名冲突处理
重命名用os.rename还是pathlib的replace?我个人更喜欢pathlib的写法,它更现代,而且异常信息更友好:
from pathlib import Path
def rename_file(src_path, new_name):
src = Path(src_path)
target = src.with_name(new_name + src.suffix)
if target.exists():
# 自动加序号,避免覆盖
idx = 1
while True:
candidate = src.with_name(f'{new_name}_{idx}{src.suffix}')
if not candidate.exists():
target = candidate
break
idx += 1
src.replace(target)
print(f'{src.name} -> {target.name}')
这个函数做了两个防御:一是检查目标文件是否已存在,存在就自动追加_1、_2;二是用replace而不是os.rename,因为replace在Windows和类Unix下行为更一致,还能覆盖目标文件(这里不希望覆盖,所以已经用exists判断阻止了)。
顺便提一句,很多人会忘记保留原扩展名。代码里我用src.suffix保留了.jpg,这样输出文件仍然是一个可预览的图片。如果你业务上明确不要后缀,可以去掉。
3.5 完整批量版代码
把上面的模块拼起来,一个可落地的批量版本大概是这样的:
import re
import cv2
import numpy as np
from pathlib import Path
from PIL import Image
from paddleocr import PaddleOCR
# 配置:按实际图片标定
CROP_BOX = (120, 180, 620, 240) # left, top, right, bottom
INPUT_DIR = Path('./scans')
# 如果想保留原文件,改为复制到 OUTPUT_DIR
OUTPUT_DIR = INPUT_DIR
ocr = PaddleOCR(use_angle_cls=True, lang='ch', show_log=False)
def clean_filename(text):
text = re.sub(r'\s+', ' ', text).strip()
text = re.sub(r'[\\/:*?"<>|]', '_', text)
return text.lstrip('.')
def process_one(img_path):
img = Image.open(img_path)
crop = img.crop(CROP_BOX)
# 预处理:放大 + 灰度 + 二值化
crop_gray = crop.convert('L')
w, h = crop_gray.size
crop_scaled = crop_gray.resize((w * 2, h * 2), Image.BICUBIC)
arr = np.array(crop_scaled)
_, binary = cv2.threshold(arr, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# 调试用:保存预处理结果
# cv2.imwrite('debug.png', binary)
result = ocr.ocr(binary, cls=True)
if not result or not result[0]:
print(f'[跳过] {img_path.name}: 未识别到文字')
return
line = max(result[0], key=lambda x: x[1][1])
raw_text = line[1][0]
new_name = clean_filename(raw_text)
rename_file(img_path, new_name)
def rename_file(src_path, new_name):
src = Path(src_path)
target = OUTPUT_DIR / (new_name + src.suffix)
if target.exists():
idx = 1
while True:
candidate = OUTPUT_DIR / f'{new_name}_{idx}{src.suffix}'
if not candidate.exists():
target = candidate
break
idx += 1
# 如果 OUTPUT_DIR 与 INPUT_DIR 不同,用 shutil.copy2 保留原文件
if OUTPUT_DIR != INPUT_DIR:
import shutil
shutil.copy2(src, target)
else:
src.replace(target)
print(f'{src.name} -> {target.name}')
if __name__ == '__main__':
for p in sorted(INPUT_DIR.glob('*.jpg')):
try:
process_one(p)
except Exception as e:
print(f'[失败] {p.name}: {e}')
这个版本在我处理几百张扫描单据时跑得很稳。实际使用中,唯一需要微调的是预处理组合和坐标。每一行都有注释,方便你按自己的情况改。
4. 批量实战与参数调优
4.1 目录结构与备份策略
开始批量处理之前,我强烈建议先把源文件复制一份到备份目录。虽然代码里做了重名保护,但程序跑起来后出了问题再想恢复,如果没有备份,就只能靠回收站碰运气了。
常用的目录组织方式:
project/
scans/ # 待处理JPG
done/ # 处理完成后移动到这里(可选)
debug/ # 调试图片输出
ocr_env/ # 虚拟环境
如果需要"处理完但保留原文件",处理逻辑就改成:读取原图,重命名副本到done目录,而不是原地rename。这个扩展很简单,只需要改动rename_file函数里的目标路径。
4.2 识别准确率调优的几个方向
如果测试时发现识别率不理想,我建议按如下顺序排查调优:
- 看预处理后的裁切图是否清晰。把预处理结果保存到debug目录,肉眼检查文字是否完整、笔画是否粘连、背景是否干净。这是最直观也最有效的第一步。
- 调整放大倍数。文字高度小于16像素时,优先把放大倍数调到3;文字高度大于64像素时,考虑缩小到1到1.5倍,超出模型训练范围反而可能掉精度。
- 调整二值化策略。OTSU是全局阈值,如果目标区域里有印章或阴影,可以换成自适应阈值cv2.adaptiveThreshold,blockSize取31或51。
- 关闭方向分类。PaddleOCR的use_angle_cls=True在多方向文本场景有用,但固定版式的扫描件里,方向分类偶尔会把正常文字旋转后识别,字段格式严格时可以考虑关掉试一下。
这里要说一个反直觉的经验:不要盲目追求高置信度才采用。置信度只是模型"自认为"的把握程度,高分不代表一定对,低分也不代表一定错。我更建议结合字段的格式特征做校验,比如业务编号通常是有固定模式的,可以用正则约束:识别结果必须匹配某个前缀加数字,不匹配就告警人工处理。
4.3 区域坐标如何稳定标定
坐标是整个流程中"最脆弱"的环节。同一个模板下,扫描件只要位置偏移几个像素,裁切区域就可能错位。针对这个问题,我提供两种方式。
第一种,简单粗暴:通过鼠标点击交互程序标定,只针对单张或少量图片。示例:
import cv2
img = cv2.imread('scan_001.jpg')
points = []
def click_callback(event, x, y, flags, param):
if event == cv2.EVENT_LBUTTONDOWN:
points.append((x, y))
print(f'({x}, {y})')
cv2.imshow('img', img)
cv2.setMouseCallback('img', click_callback)
cv2.waitKey(0)
cv2.destroyAllWindows()
点击两个点(左上角和右下角),打印出的坐标就是CROP_BOX。
第二种,更可靠:如果图片之间位置有轻微偏移,就不要用固定坐标,而是先做一些对齐预处理,比如用边缘检测找到表格线,或者用模板匹配定位一个锚点图形,再根据锚点动态推算文字区域坐标。这个方案复杂度高一些,但批量生产环境里值得做。
我实际处理扫描件时,先人工标定了一个锚点(通常是单据标题或二维码位置),然后以锚点的相对偏移裁剪,抗偏移能力比裸坐标强很多。如果你的图片来自打印机输出而非扫描仪,一般不存在这个问题,固定坐标就够了。
5. 常见问题与排查实录
5.1 paddlepaddle与paddleocr版本不匹配
这是我在实际环境中遇到最多的问题。症状通常是:安装后运行代码,一下在import阶段报错,一下在ocr.ocr()时提示缺少某些模块、版本冲突之类的信息。
排查步骤:
- 先看看装了什么版本,用pip show paddlepaddle和pip show paddleocr;
- 确认paddlepaddle是CPU版,如果是GPU版但机器没有对应CUDA,也会出幺蛾子;
- 重新安装固定版本:
pip uninstall paddlepaddle paddleocr -y
pip install paddlepaddle==2.6.0
pip install paddleocr==2.7.0
5.2 识别结果为空或乱码
空结果通常不是OCR引擎坏了,而是预处理把文字搞没了,或者裁切区域不对。优先检查:
- 裁切图有没有内容;
- 预处理后的二值图是否文字清晰;
- 是否文字颜色是浅色,被二值化之后和背景融为一体。
乱码则大概率是语言模型选错了。PaddleOCR里lang='ch'是中文,lang='en'是英文,如果图片里中英混排,建议保持'ch',因为中文模型通常也兼容英文识别。Tesseract场景下,中文乱码多半是没装chi_sim语言包,或者没在image_to_string里指定lang='chi_sim'。
5.3 文件名重命名报权限错误或名称不合法
Windows下常见两个问题:
- 文件名过长,Windows路径默认限制260个字符,识别结果如果是一长串文字,容易超限。代码里可以做截断:new_name = new_name[:50]之类的;
- 文件名以空格或句点结尾,Windows不允许,需要strip掉末尾的空格和句点;
- 重命名时目标文件正在被Excel/看图软件打开,会导致PermissionError,捕获异常并打印即可定位。
5.4 图片倾斜导致对应区域偏移
这个场景通常在手机拍照件里出现,扫描仪输出的图片基本不会有。倾斜图片要先做倾斜校正再裁剪。简单方案是用OpenCV的最小外接矩形:
import cv2
import numpy as np
def deskew(image):
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
coords = np.column_stack(np.where(gray > 0))
angle = cv2.minAreaRect(coords)[-1]
if angle < -45:
angle = -(90 + angle)
else:
angle = -angle
h, w = image.shape[:2]
center = (w // 2, h // 2)
M = cv2.getRotationMatrix2D(center, angle, 1.0)
return cv2.warpAffine(image, M, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE)
那个angle判断的几个if分支很多人抄了会用错,这里解释一下:minAreaRect返回的角度范围是[-90, 0),需要根据文字是横排还是竖排转换成正的角度。实际项目里可以输出调试图确认。
5.5 问题速查表
| 现象 | 大概率原因 | 处理方式 |
|---|---|---|
| import paddleocr报错 | 版本不匹配 | 固定版本重装 |
| 识别结果全空 | 预处理把文字抹掉 | 查看二值图并调整阈值 |
| 中文全是乱码 | 语言模型选错/没装语言包 | 使用lang='ch'或安装chi_sim |
| 重命名报PermissionError | 文件被占用 | 关闭程序或改输出目录 |
| 部分图片识别错位 | 版式偏移 | 改用锚点定位动态计算区域 |
| 识别出的文字有多余空格 | 模型输出噪声 | 用clean_filename清洗 |
6. 离开项目后的几点体会
这项目我后面又迭代过两版,有些体会值得说说。
第一,固定坐标方案只适合"模板极度稳定"的场景。一旦图片来源多、版式变化大,就别硬扛坐标了,把锚点定位做起来,前期多花两小时,后期能省大量手工排查时间。
第二,批量处理一定要留日志。我后来在代码里加了CSV输出:每张图片原始文件名、识别文本、置信度、处理结果。这个日志在出问题时是唯一能证明"哪些处理过、哪些被跳过"的依据,强烈建议加上。
第三,这个脚本稍加改造就能扩展成很多玩法。比如:把OCR结果写入Excel或数据库,形成图片台账;对接定时任务,自动扫描某个文件夹的新增图片并重命名;反向操作,根据文件名搜索图片内容,做一个简易的本地图片检索工具;将图片分类归档到不同目录,按识别出的月份或类别移动。
从技术角度看,图片文字识别加批量重命名的组合并不算高深,但它在实际工作中解决的是"大量琐碎且容易出错的人工操作"。把这套流程跑顺之后,我再看到成堆的扫描件,第一反应已经不是打开文件夹挨个改名字了,而是先问自己一句话:这图片里有没有哪段文字是唯一可用来命名的?如果有,那就写个脚本。
更多推荐


所有评论(0)