
简介本资源面向图像处理初学者与需要批量矫正图片方向的开发者聚焦利用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_clsTrue 是关键 ocr PaddleOCR(use_angle_clsTrue, langch, show_logFalse) def detect_image_orientation(img_path): 先用检测模型找出所有文本框再根据文本框的宽高比 粗略判断整图是否被旋转了 90 度 img cv2.imread(img_path) h, w img.shape[:2] # 只跑检测拿到文本框坐标 result ocr.ocr(img_path, detTrue, recFalse, clsFalse) 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_clsTrue是让 PaddleOCR 在识别阶段处理 180 度翻转但整图 90 度的问题它管不了。所以我先用detTrue, recFalse只跑检测拿到所有文本框然后算每个框的宽高比。横排文字的框通常是扁的宽 高如果检测出来的框普遍是竖的宽 高那基本可以判定整图被旋转了 90 度或 270 度。参数说明avg_ratio 0.7这个阈值是我在票据和文档场景下调出来的经验值。如果你的场景里有很多竖排文字比如古籍、某些表格这个阈值要往上调否则会误判。langch按你的实际语种改英文场景用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_clsTrue, langch, show_logFalse) 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, axis1) v_proj np.sum(binary, axis0) 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, clsTrue) return result, angle def check_text_orientation(img): 用检测框宽高比打分分数越高说明方向越正 result ocr.ocr(img, detTrue, recFalse, clsFalse) 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(pretrainedTrue) # 替换最后的分类头为 4 分类 model.classifier[3] paddle.nn.Linear(1024, 4)逻辑说明方向分类是个很简单的任务不需要大模型。MobileNetV3-small 在 224x224 输入下单张推理时间在 CPU 上大概 10-20ms完全能接受。数据增强不要做随机旋转因为旋转会改变标签含义——这是方向分类和普通图像分类最大的区别。参数说明Resize((224, 224))是标准输入如果你的图片长宽比很极端比如很长的票据建议先 padding 成正方形再 resize否则文字会被压扁。学习率用 0.001batch size 32训 20 个 epoch 基本就收敛了。3.3 批量处理的工程化细节单张图跑通之后批量场景要考虑三件事并发、缓存、失败重试。import os from concurrent.futures import ThreadPoolExecutor from functools import lru_cache lru_cache(maxsize128) 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_workers4): 批量处理目录下所有图片 os.makedirs(output_dir, exist_okTrue) 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_workersmax_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_workers4是我在 8 核机器上的经验值如果你的机器核数少降到 2。缓存 key 里带mtime是为了图片被替换后缓存能失效这个细节很多人会漏。4. 旋转校正里的避坑与排查4.1 坑一投影法把竖排文字误判成旋转现象一批竖版排版的文档比如某些证书、竖排标签投影法判断为 90 度转正后反而识别不了。原因投影法假设文字是横排的遇到真正的竖排文字垂直投影方差会更大被误判为需要旋转。解决在投影法之前加一个判断——如果检测到的文本框本身就是竖长的且数量占比超过 60%就不做旋转。或者直接上四分类模型让模型学竖排文字的特征。4.2 坑二旋转后坐标没跟着转框画歪了现象图片转正了识别也对了但可视化的时候检测框位置全错。原因PaddleOCR 返回的坐标是相对于输入图片的你在外面做了旋转坐标没有做对应的变换。解决如果只是要识别结果不用管坐标。如果要可视化旋转后的坐标要按同样的角度变换回去。90 度旋转的坐标变换公式是(x, y) - (h - y, x)注意宽高互换。4.3 坑三cls 模型对 90 度旋转无能为力现象开了use_angle_clsTrue但 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, axis1).astype(np.float32) v_proj np.sum(binary, axis0).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, threshold0.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如果图片里文字很少或者图文混排这个值会很低。threshold0.3是我在票据场景下调出来的低于这个值就交给模型。参数说明threshold需要根据你的场景调。纯文档场景可以设 0.2票据场景建议 0.3-0.4。模型推理函数model_predict_fn返回 0/90/180/270 中的一个。这个组合策略在我经手的几个批量项目里把方向判断的准确率从纯投影法的 85% 左右提到了 97% 以上同时模型调用量只有全量走模型的 30% 左右。省下来的推理时间在万张级别的批量任务里很可观。最后说个我自己的习惯每次上线新的方向判断逻辑之前先拿 200 张「已知方向」的图跑一遍混淆矩阵看清楚 90 和 270 之间有没有系统性混淆。这两个方向在投影法里经常打架提前发现比上线后返工强。希望帮到你。本文还有配套的精品资源点击获取