ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

NumPy实现高效图片旋转判断的示例代码

NumPy实现高效图片旋转判断的示例代码 前言「图片旋转判断」指的是给一张扫描件、翻拍照片或手机随手拍的文档图判断它是不是歪了、大概歪了多少度从而决定要不要转正。这不是一个能被百分百求解的问题——没有文字语义信息时「歪」与「故意斜着构图」在数值上无法区分所以实际做法都是给出一个候选角度再交由后续步骤验证。而「高效」两个字说的是实现的路径选择。用 Python 的for循环去遍历几百万个像素每一步都要经过解释器而 NumPy 把整块数据放在连续内存里用编译好的 C 循环一次算完。这个差别不是常数级的语法糖而是「每个像素一次解释器开销」与「整块数组一次函数调用」的区别。需要说明依赖NumPy 与 Pillow 都是第三方库pip install numpy Pillow不是标准库。NumPy 版本迭代较快不同版本对 Python 版本有要求安装时以官方文档为准。本文侧重讲「为什么向量化更快」与「旋转判断怎么想」代码按 Python 3.8 给出思路具体 API 细节以官方文档为准。一、高效在哪向量化 vs 逐像素循环先看两种写法在结构上的差别。逐像素循环不推荐# 适用于 Python 3.8需先 pip install Pillowfrom PIL import Imageimg Image.open(page.png).convert(L)px img.load()width, height img.sizecount 0for y in range(height):for x in range(width):if px[x, y] 128: # 逐点判断count 1print(count)这段代码每个像素都要从像素访问对象取值、做一次比较、进入一次if分支、可能累加一次。这些动作都由解释器逐条执行开销大致正比于像素个数。向量化写法推荐# 适用于 Python 3.8需先 pip install numpy Pillowimport numpy as npfrom PIL import Imagearr np.asarray(Image.open(page.png).convert(L), dtypenp.uint8)count int((arr 128).sum())print(arr.shape, count)这里arr 128一次比较整块数组得到一个同形状的布尔数组.sum()把布尔数组里True即 1加起来。整个运算在 C 层按连续内存一趟跑完Python 层只发生了几次调用。像素越多两种路径的差距越明显——但具体差多少与机器、数据规模、内存带宽都有关不要凭印象给数字需要就自己用timeit测。顺带一个常被忽略的点NumPy 的这些算子大多数是单线程的它靠的是向量化和内存局部性而不是多核并行不要想当然地以为「用了 NumPy 就等于吃满了所有 CPU 核心」。二、把图片读成数组形状与轴的含义要判断旋转先得把像素变成能算的数。Pillow 的Image对象可以被 NumPy 直接读成数组# 适用于 Python 3.8需先 pip install numpy Pillowimport numpy as npfrom PIL import Imagegray Image.open(page.png).convert(L)arr np.asarray(gray, dtypenp.uint8)print(arr.shape) # 推演输出(高, 宽)print(arr.ndim) # 推演输出2 —— 灰度图是二维先高行后宽列形状的顺序是「高在前、宽在后」这决定了axis的含义数组形状axis0沿哪个方向规约axis1沿哪个方向规约灰度图arr(H, W)沿高度方向 → 得到每列的和沿宽度方向 → 得到每行的和彩色图arr(H, W, 3)沿高度 →(W, 3)沿宽度 →(H, 3)搞清楚这张表很重要行投影与列投影的区分就是旋转判断的基础。# 适用于 Python 3.8需先 pip install numpy Pillowimport numpy as npfrom PIL import Imagearr np.asarray(Image.open(page.png).convert(L), dtypenp.uint8)ink arr 128 # 假设深色是内容文字row_profile ink.sum(axis1) # 长度 高第 i 个元素表示第 i 行有多少内容点col_profile ink.sum(axis0) # 长度 宽第 j 个元素表示第 j 列有多少内容点print(row_profile.shape, col_profile.shape)三、旋转判断的思路投影法投影法的直觉非常朴素文字排版时行与行之间是成规律排列的。把内容点沿水平方向累加会得到一串「有内容的行」和「空白的行」交替出现的曲线。当文字方向与坐标轴对齐时这种起伏最剧烈一旦整页歪了投影曲线就会被抹平。于是判断逻辑可以是计算行投影观察它的「起伏程度」例如标准差再按某个候选角度旋转一下或反过来比较行、列两个方向的投影看哪种方向更「有结构」起伏最强的那个方向就作为候选的正方向。# 适用于 Python 3.8需先 pip install numpy Pillowimport numpy as npfrom PIL import Imagedef ink_profiles(arr, threshold128):返回 (行投影, 列投影)每行 / 每列的内容点数。ink arr thresholdreturn ink.sum(axis1), ink.sum(axis0)def guess_orientation(arr, threshold128, min_ink10):粗判内容是否更贴合「横向排版」。返回 0 或 90。row_profile, col_profile ink_profiles(arr, threshold)if int(row_profile.sum()) min_ink:return 0 # 几乎没有内容无从判断# 行方向的起伏比列方向更明显 → 认为文字行是水平的row_flatness row_profile.std()col_flatness col_profile.std()return 0 if row_flatness col_flatness else 90这里必须坦白这是一个启发式规则不是通用答案。它假设「深色是内容」「文字成行排列」遇到满版图片、表格、竖排文字就会失灵。因此工程上通常把它当作「候选生成器」再交给 OCR 或版式分析去验证而不是直接拿它当结论。四、主轴法用协方差矩阵找方向投影法判断的是「要不要转 90 度」而主轴法能给出一个连续的角度适合判断轻微的倾斜。思路是把内容点看成散落在平面上的点集找这个点集的主轴方向——也就是点分布最分散的那个方向。这正好是主成分分析PCA在做的事对点集做中心化算协方差矩阵取最大特征值对应的特征向量作为主轴再求它与横轴的夹角。# 适用于 Python 3.8需先 pip install numpyimport numpy as npdef main_axis_angle(points):points: 形状 (N, 2) 的坐标数组返回主轴相对横轴的夹角度。pts np.asarray(points, dtypenp.float64)if pts.shape[0] 2:return 0.0centered pts - pts.mean(axis0) # 中心化cov np.cov(centered, rowvarFalse) # 2x2 协方差矩阵eigvals, eigvecs np.linalg.eigh(cov) # 对称矩阵用 eighmain eigvecs[:, np.argmax(eigvals)] # 最大特征值对应的方向return float(np.degrees(np.arctan2(main[1], main[0])))配合np.nonzero就能从图片里取出内容点坐标# 适用于 Python 3.8需先 pip install numpy Pillowimport numpy as npfrom PIL import Imagearr np.asarray(Image.open(scan.png).convert(L), dtypenp.uint8)ys, xs np.nonzero(arr 128) # 返回两个一维数组行下标、列下标points np.column_stack((xs, ys)) # 拼成 (N, 2)每行是一个坐标点angle main_axis_angle(points)print(round(angle, 2)) # 推演输出主轴相对横轴的夹角度注意几点坐标里的y是向下增长的所以算出来的角度符号与日常直觉相反顺时针为正主轴只给出「方向」不区分正反差 180 度是同一条直线也不告诉你文字朝上还是朝下。要真正把文档转正还得结合「哪一边内容更多」「文字基线在下方」这类额外信息。此外先对图像做下采样缩小若干倍再取点能显著减少参与特征分解的点数对判断结果影响通常很小。常见坑点1. 用 Python 循环逐像素统计。❌ 为了「看得清楚」写两层for几百万像素逐点判断。 ✅ 用布尔数组 .sum()这类向量化写法让运算在 C 层一趟完成。2. 把axis记反行投影和列投影搞混。❌ 以为sum(axis0)是「每行求和」。 ✅ 对(H, W)数组axis0是沿高度方向规约得到的是每列的和。3. 忘了np.asarray读出来的数组可能是只读的。❌ 直接用np.asarray(pil_img)的结果做原地修改报数组不可写。 ✅ 需要写时用np.array(...)拷一份或对结果显式.copy()。4. 在uint8上做累加结果溢出回绕。❌ 对uint8数组直接.sum()之外的加减步骤255 1 变成 0。 ✅ 参与中间计算的数组先.astype(np.int32)或np.float64升位。5. 以为用了 NumPy 就会自动多核并行。❌ 期待一个np.sum吃满所有 CPU 核心。 ✅ 多数算子走的是单线程 C 循环靠的是向量化与内存局部性别把并行想当然。6. 把启发式的判断当成确定结论。❌ 投影法判出 90 度就直接转图交付遇到满版图片彻底失灵。 ✅ 只把结果当候选用 OCR 置信度或版式规则做二次验证。7. 大图不做下采样就对全部内容点做特征分解。❌ 上千万个点直接喂进协方差计算内存和耗时都吃不消。 ✅ 先缩图或抽样再取点主轴方向对采样密度通常不敏感。总结问题结论「高效」体现在哪向量化替代逐点解释器循环整块连续内存一次算完数组形状灰度图(高, 宽)彩色图(高, 宽, 通道)axis0/axis1分别为「每列和」与「每行和」投影法比较行、列投影的起伏粗判是否转了 90 度主轴法中心化 → 协方差 →eigh取最大特征向量 → 求夹角结果性质启发式候选需二次验证主轴不区分正反提速手段向量化、升位防溢出、下采样降点数一句话收尾旋转判断的核心是「把像素变成投影或点集再用向量化运算找规律」NumPy 的高效来自「整块数据一次算完」而不是「循环写得更短」而判断结果永远只是候选——它需要后一步来确认。
返回列表