简介:本资源面向图像处理初学者与需要批量矫正图片方向的开发者,聚焦利用PaddleOCR底层能力解决图片旋转问题。包内共45个文件,以19个py脚本与17个pyc编译文件为主,另含3个onnx模型、2张jpg与1张png示例图、1个ttf字体及readme.md说明文档,压缩包约20.3MB,结构围绕图片旋转矫正与文字识别流程组织。资源涵盖图片加载、旋转角度检测、自动矫正及批量处理等模块,脚本中可见角度检测与OCR预测相关实现,可帮助读者理解如何借助PaddleOCR的图像分析能力判断未知旋转角度,并完成矫正后文字检测识别的验证。已有348人学习下载,适合希望掌握图片旋转矫正、提升扫描件与历史图片处理效率的读者参考。
1. 图片旋转了 90 度,PaddleOCR 为什么还能读出字
做票据识别、证件归档、扫描件入库的同行,大概率都遇到过这种场景:手机横着拍了一张发票,或者扫描仪进纸时方向放反了,图片在相册里看着是正的,一进 OCR 流程就整段整段地识别失败。更玄学的是,同一张图你手动转正再喂进去,识别率立刻从 30% 跳到 95%。很多人第一反应是去调 PaddleOCR 的det_db_thresh、rec_score_thresh,调了半天没效果,因为问题根本不在识别模型,而在图片本身的方向。
这篇笔记就围绕「利用 PaddleOCR 解决图片旋转问题」这条线,把方向检测、角度分类、旋转校正、批量落地这几件事讲透。适合正在做文档数字化、票据批量识别、扫描件预处理的工程师,也适合刚接触 PaddleOCR、被旋转图片坑过一轮的新手。读完你应该能自己搭一条「先判方向、再转正、后识别」的流水线,并且知道哪些参数不能乱动、哪些坑我踩过。
2. 先搞清楚 PaddleOCR 到底管不管方向这件事
2.1 PaddleOCR 的三个模型各管什么
PaddleOCR 的完整推理链路其实是三段:文本检测(det)、方向分类(cls)、文本识别(rec)。很多人只用了 det + rec,把 cls 关掉了,结果就是旋转图片直接翻车。方向分类模型干的事很单一——判断一个已经裁出来的文本框是 0 度还是 180 度,它不负责判断整张图是 90 度还是 270 度。
这里有个关键认知:PaddleOCR 自带的 cls 模型只解决「倒置」问题,不解决「横竖」问题。一张图整体旋转 90 度,检测模型可能连文本框都框不出来,因为 DB 检测对文字排列方向是有假设的。所以「利用 PaddleOCR 解决图片旋转问题」这件事,实际上要分两层来做:
| 层级 | 问题类型 | 解决手段 |
|---|---|---|
| 整图方向 | 0/90/180/270 度 | 方向分类模型或图像预处理 |
| 文本框方向 | 0/180 度 | PaddleOCR 内置 cls |
| 小角度倾斜 | ±15 度以内 | 检测框角度回归 + 透视变换 |
我一般会先跑一个整图方向判断,把图转正,再交给 PaddleOCR 的 det + cls + rec 三段流程。这样 cls 只需要处理它擅长的 180 度翻转,整图 90 度的问题在更前面就解决掉了。
2.2 为什么不能只靠识别结果反推方向
有人会想:我先把图按四个方向各识别一遍,哪个方向识别置信度高就用哪个。这个思路理论上可行,但代价是推理次数翻四倍,批量场景下完全不可接受。而且遇到文字很少的图(比如一张只有两行字的收据),四个方向的置信度可能都差不多,反而容易选错。
更稳的做法是用一个轻量的方向分类模型先做一次判断。PaddleOCR 生态里可以自己训一个四分类模型,也可以直接用现成的图像方向分类方案。下面这段代码是我常用的整图方向判断骨架,基于 PaddleOCR 的 cls 模型做扩展:
import cv2 import numpy as np from paddleocr import PaddleOCR # 初始化时开启方向分类,use_angle_cls=True 是关键 ocr = PaddleOCR(use_angle_cls=True, lang='ch', show_log=False) def detect_image_orientation(img_path): """ 先用检测模型找出所有文本框,再根据文本框的宽高比 粗略判断整图是否被旋转了 90 度 """ img = cv2.imread(img_path) h, w = img.shape[:2] # 只跑检测,拿到文本框坐标 result = ocr.ocr(img_path, det=True, rec=False, cls=False) if not result or not result[0]: return 0 # 没检测到文本,不做旋转 boxes = result[0] # 统计所有文本框的宽高比 ratios = [] for box in boxes: box = np.array(box) box_w = np.linalg.norm(box[0] - box[1]) box_h = np.linalg.norm(box[1] - box[2]) if box_h > 0: ratios.append(box_w / box_h) if not ratios: return 0 avg_ratio = np.mean(ratios) # 正常横排文字,文本框宽高比通常大于 1 # 如果平均宽高比明显小于 1,说明文字是竖着的,图被转了 90 度 if avg_ratio < 0.7: return 90 return 0这段代码的逻辑说明:use_angle_cls=True是让 PaddleOCR 在识别阶段处理 180 度翻转,但整图 90 度的问题它管不了。所以我先用det=True, rec=False只跑检测,拿到所有文本框,然后算每个框的宽高比。横排文字的框通常是扁的(宽 > 高),如果检测出来的框普遍是竖的(宽 < 高),那基本可以判定整图被旋转了 90 度或 270 度。
参数说明:avg_ratio < 0.7这个阈值是我在票据和文档场景下调出来的经验值。如果你的场景里有很多竖排文字(比如古籍、某些表格),这个阈值要往上调,否则会误判。lang='ch'按你的实际语种改,英文场景用en检测速度更快。
2.3 方向分类模型和图像预处理的选型
整图方向判断有两条路:一是训一个四分类模型(0/90/180/270),二是用传统图像方法做预处理。我两种都用过,说下适用边界。
四分类模型适合:图片量大、方向分布均匀、有标注资源。训一个 ResNet18 级别的分类器,几千张标注图就能到 95% 以上的准确率。缺点是你要维护标注数据,而且遇到新类型的文档(比如突然来了一批竖版海报)可能要重新补数据。
传统图像方法适合:快速上线、没有标注资源、方向类型单一。常用的手段是投影法——把图片二值化后,分别计算水平和垂直方向的像素投影方差,文字行方向的投影方差会明显更大。这个方法对纯文字文档有效,对图文混排的票据就一般了。
我一般会先用传统方法做个快速判断,把明显旋转的图筛出来,剩下模棱两可的再走模型。这样既省标注成本,又能覆盖大部分场景。
3. 用 PaddleOCR 搭一条旋转校正流水线
3.1 最小可跑通的四步流程
先给一个能直接跑的最小流程,后面再逐段拆开讲。整个流水线分四步:读图 → 判断方向 → 旋转校正 → 送入 PaddleOCR 识别。
import cv2 import numpy as np from paddleocr import PaddleOCR ocr = PaddleOCR(use_angle_cls=True, lang='ch', show_log=False) def rotate_image(img, angle): """按指定角度旋转图片,angle 取 0/90/180/270""" if angle == 0: return img h, w = img.shape[:2] center = (w // 2, h // 2) # 90 和 270 度旋转后宽高会互换,这里用 transpose 更直接 if angle == 90: return cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE) elif angle == 180: return cv2.rotate(img, cv2.ROTATE_180) elif angle == 270: return cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE) return img def estimate_angle_by_projection(img): """ 投影法估计整图旋转角度 原理:文字行方向的像素投影方差更大 """ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 二值化,文字为白,背景为黑 _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) # 水平投影和垂直投影的方差 h_proj = np.sum(binary, axis=1) v_proj = np.sum(binary, axis=0) h_var = np.var(h_proj) v_var = np.var(v_proj) # 水平投影方差大,说明文字是横排的,图方向正常 if h_var >= v_var: return 0 else: return 90 def ocr_with_rotation(img_path): """完整的旋转校正 + 识别流程""" img = cv2.imread(img_path) # 第一步:投影法粗判 angle = estimate_angle_by_projection(img) # 第二步:如果粗判为 90 度,再确认是 90 还是 270 if angle == 90: rotated_90 = rotate_image(img, 90) rotated_270 = rotate_image(img, 270) # 用检测框宽高比来区分 90 和 270 score_90 = check_text_orientation(rotated_90) score_270 = check_text_orientation(rotated_270) angle = 90 if score_90 >= score_270 else 270 # 第三步:旋转校正 corrected = rotate_image(img, angle) # 第四步:送入 PaddleOCR 识别 result = ocr.ocr(corrected, cls=True) return result, angle def check_text_orientation(img): """用检测框宽高比打分,分数越高说明方向越正""" result = ocr.ocr(img, det=True, rec=False, cls=False) if not result or not result[0]: return 0 ratios = [] for box in result[0]: box = np.array(box) box_w = np.linalg.norm(box[0] - box[1]) box_h = np.linalg.norm(box[1] - box[2]) if box_h > 0: ratios.append(box_w / box_h) return np.mean(ratios) if ratios else 0逻辑说明:estimate_angle_by_projection用投影方差做粗判,这一步很快,不需要跑模型。如果粗判结果是 90 度,再用check_text_orientation分别对旋转 90 度和 270 度的图跑一次检测,看哪个方向的文本框宽高比更正常。最后用rotate_image做校正,再走完整的识别流程。
参数说明:cv2.threshold里的THRESH_OTSU是自动阈值,适合光照均匀的扫描件。如果图片光照不均,建议先做自适应直方图均衡化(CLAHE)再二值化。check_text_orientation里没有设阈值,直接比大小,因为这一步只是二选一,不需要绝对判断。
3.2 把方向分类模型接进来
投影法在纯文字文档上够用,但遇到票据、表格、图文混排就不太稳。这时候可以训一个四分类模型接进来。下面是一个基于 PaddleClas 的四分类训练配置示例:
# 方向分类模型训练配置(基于 PaddleClas 的轻量骨干) # 数据目录结构: # dataset/ # train/ # 0/ # 正常方向 # 90/ # 180/ # 270/ # val/ # 0/ # 90/ # 180/ # 270/ import paddle from paddle.vision.transforms import Compose, Resize, Normalize, ToTensor # 输入尺寸用 224x224,方向分类不需要太大分辨率 train_transform = Compose([ Resize((224, 224)), Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ToTensor() ]) # 骨干网络选 MobileNetV3,推理速度快,适合批量场景 # 如果准确率不够再换 ResNet18 model = paddle.vision.models.mobilenet_v3_small(pretrained=True) # 替换最后的分类头为 4 分类 model.classifier[3] = paddle.nn.Linear(1024, 4)逻辑说明:方向分类是个很简单的任务,不需要大模型。MobileNetV3-small 在 224x224 输入下,单张推理时间在 CPU 上大概 10-20ms,完全能接受。数据增强不要做随机旋转,因为旋转会改变标签含义——这是方向分类和普通图像分类最大的区别。
参数说明:Resize((224, 224))是标准输入,如果你的图片长宽比很极端(比如很长的票据),建议先 padding 成正方形再 resize,否则文字会被压扁。学习率用 0.001,batch size 32,训 20 个 epoch 基本就收敛了。
3.3 批量处理的工程化细节
单张图跑通之后,批量场景要考虑三件事:并发、缓存、失败重试。
import os from concurrent.futures import ThreadPoolExecutor from functools import lru_cache @lru_cache(maxsize=128) def cached_orientation(img_path, mtime): """带缓存的方判判断,mtime 用于缓存失效""" img = cv2.imread(img_path) return estimate_angle_by_projection(img) def batch_process(img_dir, output_dir, max_workers=4): """批量处理目录下所有图片""" os.makedirs(output_dir, exist_ok=True) img_files = [f for f in os.listdir(img_dir) if f.lower().endswith(('.jpg', '.png', '.jpeg'))] def process_one(fname): img_path = os.path.join(img_dir, fname) try: mtime = os.path.getmtime(img_path) angle = cached_orientation(img_path, mtime) img = cv2.imread(img_path) corrected = rotate_image(img, angle) out_path = os.path.join(output_dir, fname) cv2.imwrite(out_path, corrected) return fname, angle, 'ok' except Exception as e: return fname, -1, str(e) with ThreadPoolExecutor(max_workers=max_workers) as executor: results = list(executor.map(process_one, img_files)) # 统计失败项 failed = [r for r in results if r[2] != 'ok'] return results, failed逻辑说明:lru_cache做方向判断的缓存,避免同一张图重复计算。ThreadPoolExecutor做并发,max_workers不要设太大,PaddleOCR 推理本身会占 CPU,设 4 左右比较稳。失败项单独收集,方便后续排查。
参数说明:max_workers=4是我在 8 核机器上的经验值,如果你的机器核数少,降到 2。缓存 key 里带mtime是为了图片被替换后缓存能失效,这个细节很多人会漏。
4. 旋转校正里的避坑与排查
4.1 坑一:投影法把竖排文字误判成旋转
现象:一批竖版排版的文档(比如某些证书、竖排标签),投影法判断为 90 度,转正后反而识别不了。
原因:投影法假设文字是横排的,遇到真正的竖排文字,垂直投影方差会更大,被误判为需要旋转。
解决:在投影法之前加一个判断——如果检测到的文本框本身就是竖长的,且数量占比超过 60%,就不做旋转。或者直接上四分类模型,让模型学竖排文字的特征。
4.2 坑二:旋转后坐标没跟着转,框画歪了
现象:图片转正了,识别也对了,但可视化的时候检测框位置全错。
原因:PaddleOCR 返回的坐标是相对于输入图片的,你在外面做了旋转,坐标没有做对应的变换。
解决:如果只是要识别结果,不用管坐标。如果要可视化,旋转后的坐标要按同样的角度变换回去。90 度旋转的坐标变换公式是(x, y) -> (h - y, x),注意宽高互换。
4.3 坑三:cls 模型对 90 度旋转无能为力
现象:开了use_angle_cls=True,但 90 度旋转的图还是识别失败。
原因:前面说过,PaddleOCR 的 cls 模型只做 0/180 度二分类,它不处理 90 度。
解决:整图 90 度的问题必须在送入 PaddleOCR 之前解决,不要指望 cls 模型。这是最常见的认知误区,我见过不少同行在这里卡了很久。
4.4 坑四:小角度倾斜被当成旋转问题处理
现象:图片只倾斜了 5-10 度,但用整图旋转去校正,转完反而更歪。
原因:整图旋转只能处理 90 度的整数倍,小角度倾斜需要的是仿射变换或透视变换。
解决:小角度倾斜交给 PaddleOCR 的检测框角度回归,或者用霍夫变换检测文本行角度后做仿射校正。不要用 90 度旋转的逻辑去处理小角度。
4.5 坑五:批量处理时内存爆了
现象:处理几千张图时,程序跑着跑着 OOM。
原因:PaddleOCR 实例在并发场景下会重复加载模型,或者图片没有及时释放。
解决:PaddleOCR 实例全局初始化一次,不要在每个线程里 new。图片处理完手动del img,或者用生成器逐张读。max_workers不要超过 CPU 核数。
5. 一个提高方向判断准确率的小技巧
前面讲的投影法和四分类模型,各有各的边界。我在实际项目里最后收敛到一个组合策略:先用投影法做快速筛选,只对「投影法置信度低」的图片走模型判断。这样既省了模型推理的开销,又补上了投影法的短板。
具体做法是给投影法加一个置信度分数:
def estimate_angle_with_confidence(img): """ 返回 (angle, confidence) confidence 越高说明投影法越可信 """ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) h_proj = np.sum(binary, axis=1).astype(np.float32) v_proj = np.sum(binary, axis=0).astype(np.float32) h_var = np.var(h_proj) v_var = np.var(v_proj) # 置信度 = 两个方差差异的相对大小 total = h_var + v_var if total == 0: return 0, 0.0 confidence = abs(h_var - v_var) / total angle = 0 if h_var >= v_var else 90 return angle, confidence def smart_orientation(img, model_predict_fn, threshold=0.3): """ 组合策略:投影法置信度高于阈值直接用, 否则走模型 """ angle, conf = estimate_angle_with_confidence(img) if conf >= threshold: return angle, 'projection' else: # model_predict_fn 是你训好的四分类模型推理函数 return model_predict_fn(img), 'model'逻辑说明:confidence是两个投影方差差异占总和的比例。如果文字方向很明确,这个值会接近 1;如果图片里文字很少或者图文混排,这个值会很低。threshold=0.3是我在票据场景下调出来的,低于这个值就交给模型。
参数说明:threshold需要根据你的场景调。纯文档场景可以设 0.2,票据场景建议 0.3-0.4。模型推理函数model_predict_fn返回 0/90/180/270 中的一个。
这个组合策略在我经手的几个批量项目里,把方向判断的准确率从纯投影法的 85% 左右提到了 97% 以上,同时模型调用量只有全量走模型的 30% 左右。省下来的推理时间在万张级别的批量任务里很可观。
最后说个我自己的习惯:每次上线新的方向判断逻辑之前,先拿 200 张「已知方向」的图跑一遍混淆矩阵,看清楚 90 和 270 之间有没有系统性混淆。这两个方向在投影法里经常打架,提前发现比上线后返工强。希望帮到你。
本文还有配套的精品资源,点击获取