ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

OpenCV投影技术详解:从OCR字符分割到车牌识别实战应用

OpenCV投影技术详解:从OCR字符分割到车牌识别实战应用 做OCR或者车牌识别项目时十有八九会撞上同一个问题图像里的文字虽然拍下来了但电脑不知道哪一块是一个字、哪一块是背景。字符必须一个个切好后面不管接模板匹配还是接神经网络才有得玩。OpenCV图像处理里的投影技术就是干这件事最经典的方案。它不依赖任何训练样本不需要大数据集几十行代码就能把一行文字切成单个字符把文本区域从复杂背景里抠出来。这篇文章我会从投影的原理讲起给出可以直接抄的Python代码再聊几个只有实际跑过才知道的坑。投影技术准确说叫投影轮廓projection profile在OpenCV生态里并没有封装成一个单独的函数但配合NumPy两行就能算出来。正因为实现简单、速度极快、效果稳定它一直活跃在车牌识别、印刷体OCR、表格识别这些场景的第一线。下面按我自己的使用经验逐步展开。1. 投影技术到底在解决什么问题1.1 一个绕不开的分割需求很多图像处理任务难点往往不在识别而在切分。比如拍了一张文档照片你想把里面的文字提出来做识别。第一步不是让OCR直接上而是要先回答几个问题文字在哪里有几行每一行有哪些字符没有准确答案后面所有识别都是白费。这时候深度学习讲的是训练目标检测模型定位文字但经典图像处理里有一个更朴素也更直接的办法把图像按行、按列统计目标像素的数量然后根据统计曲线的形状来切分。这就是投影技术。用我当年做车牌识别时举个例车牌定位出来以后图像里是粤B12345这样的字符但电脑并不知道该从哪里下刀把它们分开。垂直投影一算每个字符在列方向会堆出一个很高的白色像素柱字符之间的缝隙则是低谷。波形长什么样字符就长什么样直接按波谷下刀即可。整个过程没有任何学习纯靠几何统计却是当时最可靠的一步。1.2 投影的数学本质投影的操作说起来很简单。水平投影把二值图像沿着水平方向累计也就是统计每一行里有多少个白色像素得到一个长度为图像高度的数组。这个数组画成曲线后有内容的行会产生明显的波峰空行则是低谷。垂直投影同理统计每一列里有多少个白色像素得到长度为图像宽度的数组。用来切分左右排列的字符。为什么强调二值图像因为投影统计的是目标像素的数量不是灰度本身。如果直接用灰度图统计背景颜色、光照渐变都会混进结果里波峰波谷完全没法看。所以标准流程一定是先灰度化再二值化让目标区域变成255、背景变成0然后统计255的数量。这里有个细节经常让人栽跟头二值化之后到底目标应该是白色还是黑色OpenCV的threshold默认把大于阈值的置为255但很多场景比如蓝底白字的车牌需要反相操作用cv2.THRESH_BINARY_INV让字符变白、背景变黑。投影函数本身不管正反它只统计非零像素所以二值化方向搞反投影曲线会直接翻过来。这个坑我在第5节还会专门讲。1.3 投影技术的核心优势有人可能问现在目标检测这么成熟为什么还要记着一个老掉牙的投影法因为它是典型的四两拨千斤。第一速度极快NumPy向量化操作在一张1080P图像上算垂直投影就是几毫秒的事而一个轻量级检测模型跑一次前向推理至少几十毫秒。第二完全可解释投影曲线任何一处异常你都能画出来看模型输出你却很难说清它为什么把两个字符框在一起。第三不需要训练换一张字体完全不同、分辨率完全不同的图像投影代码一行都不用改模型却可能要重新训练。所以我的看法是投影永远不会被淘汰它和深度学习并不是替代关系。很多工业项目里先用投影做粗定位、再用模型做细分类又稳又快又省钱。理解投影等于给你的工具箱添了一把最基础的尺子。2. 水平投影与垂直投影从零实现和可视化2.1 前置处理灰度化与二值化写投影函数之前先把图准备好。标准预处理三件套读图、转灰度、二值化。import cv2 import numpy as np img cv2.imread(text.png) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # Otsu自动阈值适合光照相对均匀的图像 _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU)注意这里用了THRESH_BINARY_INV目的是让文字变成白色、背景变成黑色。如果你读入的图像本身是白底黑字这个反转是必须的如果是黑底白字就不要加INV。判断方法很简单打印binary数组看255集中在图像内容区还是背景区。如果你的图像光照不均匀Otsu全局阈值经常出问题比如纸面一半亮一半暗亮的半边文字被并进背景。这时候改用cv2.adaptiveThreshold更稳binary cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, blockSize15, C5 )块大小blockSize一般取奇数15到25之间比较保险C是减去的常数太小容易把轻微的纸张纹理也划成前景太大又会让细笔画断掉。这个参数后期要多试几次我没有一次性就能调准的诀窍但是有个经验先试C5看笔画是否完整再往大或往小微调。2.2 投影核心代码准备好二值图之后投影本身只需要两行# 水平投影统计每一行的非零像素数 h_proj np.count_nonzero(binary, axis1) # 垂直投影统计每一列的非零像素数 v_proj np.count_nonzero(binary, axis0)h_proj的形状是(rows,)v_proj的形状是(cols,)。就这两行水平、垂直投影都出来了。可能有人会用np.sum(binary 255, axis1)这也行但np.count_nonzero直接统计非零值少一次比较运算速度略快。另外要注意如果二值化后前景是255那么binary 255没问题但如果前景像素不是255比如某些流程里只做了阈值没归一化count_nonzero依然有效因为只要不是0就算目标。从这个角度看count_nonzero更鲁棒。2.3 三种实现方式对比除了NumPy写法OpenCV本身也提供了投影相关的操作。我把常见三种实现方式放在一起对比大家按需选择。实现方式核心代码优点缺点NumPy count_nonzeronp.count_nonzero(binary, axis1)向量化速度最快代码最简洁需要NumPy环境OpenCV reducecv2.reduce(binary, 0, cv2.REDUCE_SUM)纯OpenCV依赖适合C环境注意结果形状是单行/单列需要reshape阈值判别要额外处理显式循环遍历行/列用cv2.countNonZero求和逻辑直观方便调试最慢大图下要避免使用如果用的是C和OpenCVcv::reduce更顺手大概长这样cv::Mat binary; cv::Mat v_proj; cv::reduce(binary, v_proj, 0, cv::REDUCE_SUM, CV_32S);Python下我建议直接用第一种没有理由绕弯子。2.4 把投影曲线画出来看写代码过程中最有价值的习惯就是把自己的中间结果可视化。投影是曲线画出来一眼就能看出问题。import matplotlib.pyplot as plt plt.figure(figsize(10, 6)) # 原图 plt.subplot(2, 2, (1, 2)) plt.imshow(binary, cmapgray) plt.title(binary) # 水平投影横轴是行索引纵轴是每行的白点数 plt.subplot(2, 2, 3) plt.plot(h_proj) plt.xlabel(row) plt.ylabel(count) plt.title(horizontal projection) # 垂直投影 plt.subplot(2, 2, 4) plt.plot(v_proj) plt.xlabel(col) plt.ylabel(count) plt.title(vertical projection) plt.tight_layout() plt.show()怎么看这张图以一行文字为例水平投影里文字所在的行范围对应一个宽宽的波峰波峰之外投影值接近0垂直投影里每个字符对应一个波峰字符间隙对应波谷。如果投影曲线上波峰连成一整片、没有清晰波谷大概率是图像倾斜或者字符粘连如果波峰里布满毛刺一般是噪声没有滤干净。这些特征在曲线上都非常直观比盯着像素强多了。3. 文本行检测与字符分割投影的经典落地3.1 用水平投影切出每一行文本投影技术最经典的实战就是文档OCR里的版面分析。以一段印刷体文字为例第一步要切行。思路对整页图像做水平投影找到所有波峰区间每个区间就是一行的上下边界。我会写一个通用的小函数输入投影数组输出所有满足条件的连续区间def find_regions(proj, min_h5): 找到投影曲线中所有连续非零区间。 proj: 一维数组 min_h: 区间最小长度太短的区间视为噪声 返回: [(start, end), ...]区间左闭右开 regions [] in_region False start 0 for i, v in enumerate(proj): if v 0 and not in_region: in_region True start i elif v 0 and in_region: in_region False if i - start min_h: regions.append((start, i)) start 0 # 处理投影末尾仍在区间内的情况 if in_region and len(proj) - start min_h: regions.append((start, len(proj))) return regions row_regions find_regions(h_proj, min_h10)这个函数的核心逻辑很简单从左到右扫描遇到大于0的值标记区间开始遇到等于0的值区间结束把长度足够的区间记录下来。min_h用来过滤掉个别噪声造成的孤立小峰。拿到行区间后每行都裁剪一次for i, (r_start, r_end) in enumerate(row_regions): line_bin binary[r_start:r_end, :] # 此时line_bin就是一行文字的图像可以做后续识别或继续垂直切分实际处理时行与行之间如果存在公式、图片、页眉页脚投影曲线会呈现出不同高度的波峰配合行高的先验知识例如某个字号的行高约30像素可以做进一步合并或过滤。这一步叫版面分析的粗糙版但已经能解决大部分规则文档的切行问题。3.2 行内字符再切分行切出来之后下一步是把一行切成单个字符。对line_bin做垂直投影再复用find_regions每个区间就是一个字符的左右边界v_proj_line np.count_nonzero(line_bin, axis0) char_regions find_regions(v_proj_line, min_h2) for idx, (c_start, c_end) in enumerate(char_regions): char_img line_bin[:, c_start:c_end] # char_img 就是单字符图像如果一行是标准印刷体、等宽字体你很快会发现切出来的字符宽度非常整齐这就是投影做字符分割的美妙之处。整个过程加起来不到30行代码不需要任何模型鲁棒性在同场景下甚至比很多检测模型好。但这里藏着一个所有做OCR的人都要理解的真相投影切分本质上是按空隙切它天然假设每个字符之间有空隙。如果两个字符贴在一起、笔画相交它们变成一个波峰投影就失败了。印刷体一般没问题手写体和扭曲字符才是投影的短板这个在第4节详细说。3.3 波峰区间的边界判断策略find_regions用的是最朴素的零值判断。但真实图像里投影值很少会严格等于0噪声、轻微倾斜、纸张纹理都会让本该是0的位置变成一个小正数。所以实际项目中我一般会把判断条件改得更灵活一些。策略判定方式适用场景零值判断proj[i] 0二值化干净、无噪声的理想图像绝对阈值proj[i] thresh如thresh5噪声较小、目标像素数量明确的场景相对阈值proj[i] max(proj) * ratio如ratio0.1多尺度目标波峰高度差异大差分法找proj[i-1] - proj[i]符号变化的点倾斜小、边界平滑可精准定位上升沿/下降沿相对阈值是实战中最推荐的因为它不依赖固定的像素数量对图像缩放、分辨率变化有天然的适应性。比如原来是1080P的图投影最大值为300后来换成4K的图最大值变成700零值判断不需要改绝对阈值可能要跟着调但相对阈值完全不用动。写一个改进版def find_regions_relative(proj, min_h5, ratio0.1): thresh max(proj) * ratio regions [] in_region False start 0 for i, v in enumerate(proj): if v thresh and not in_region: in_region True start i elif v thresh and in_region: in_region False if i - start min_h: regions.append((start, i)) start 0 if in_region and len(proj) - start min_h: regions.append((start, len(proj))) return regions3.4 中文、标点与手写倾斜带来的麻烦印刷体英文用投影切分很舒服但切中文时要多留个心眼。第一左右结构的汉字比如林张好本身内部就有一道明显的垂直空隙垂直投影会在一个字内部形成双峰如果阈值没设好一个字会被切成两半。解决思路设定字符宽度的先验范围如果两个相邻区间宽度都特别小、且和平均字符宽度差很远就合并或者先算所有区间的平均宽度把明显过窄的相邻区间合并成一个字符。第二标点符号。逗号、句号、引号的白点数量比汉字少很多投影峰值也低。用相对阈值切分时这些标点很可能低于阈值被直接过滤掉。我的处理办法是先根据字高设定一个较小的min_h和较低的ratio尽量让标点也形成区间识别之后再根据语义或位置合并而不是在分割阶段就把它们删掉。第三手写体和倾斜文本。手写字符忽大忽小、字间距忽宽忽窄投影曲线的波形不像印刷体那么规整硬按波谷切很容易切坏。文字整体倾斜时投影曲线会被糊掉波峰变宽、波谷变浅。这时候必须先做旋转校正让文字水平投影才有意义。倾斜校正的方法在第4.3节讲。我个人的经验是投影适合场景固定、排版规整的文档如果项目里是自由书写、透视畸变严重的照片别硬用投影优先考虑透视校正加形态学处理或者直接走深度学习检测。4. 车牌字符切分与粘连处理投影的进阶场景4.1 车牌识别的标准投影切割流程车牌识别是投影技术最有代表性的应用之一。以国内常见的蓝底白字车牌为例定位出车牌区域后图像是蓝底、白色字符。预处理流程# 假设plate是已定位的车牌图像 plate_gray cv2.cvtColor(plate, cv2.COLOR_BGR2GRAY) _, plate_bin cv2.threshold(plate_gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU)这里二值化不用INV因为Otsu会自动把亮度高的一类分出来白色字符自然变成255。然后垂直投影切分v_proj_plate np.count_nonzero(plate_bin, axis0) plate_chars find_regions_relative(v_proj_plate, min_h5, ratio0.1)正常车牌会切出7个区间对应7个字符最后一位汉字或字母可能因为粗细不同宽度略有差异。切完以后每个区间配上牌照号的先验知识首字符是省份简称、第二位是字母等就能交给识别模块了。但实际跑起来没这么顺利。车牌字符有几个特殊之处第一个汉字比如粤笔画密集而且左右结构明显垂直投影往往会在内部出现波谷可能被切成两段字符1特别窄和旁边字符的空隙可能被噪声糊住字符京包含的左上点结构也会产生奇怪的小波峰。我通常在切分后加一个简单的后处理计算所有区间的宽度中位数如果某个区间宽度小于中位数的40%并且它和相邻区间的间距很小就把它们合并如果某个字符显然比中位数宽很多再检查是不是两个字符粘连按中位数宽度切一刀。4.2 粘连字符投影分割失败的典型场景两个字符之间本来应该有空隙但拍照模糊、分辨率低、或者字符笔画延伸导致二值化后两个字符连在一坨。垂直投影上的表现是两个波峰之间没有降到0甚至没有明显的凹槽。在OCR里这叫字符粘连是投影技术的头号天敌。对付粘连我从简单到复杂给出三个方案。方案一形态学分离。对二值图做一次轻微的腐蚀比如3x3核迭代1次把连接处的细缝腐蚀断。代价是字符本身的笔画也会变细识别前要再做一次膨胀恢复。这个方法只对接触面积小的粘连有效如果两个字符重叠了一大片腐蚀到字符都残了也分不开。kernel np.ones((3, 3), np.uint8) separated cv2.erode(plate_bin, kernel, iterations1)方案二基于先验宽度的强制切割。先看整块字符区域估算平均字符宽度w_avg然后当检测到一个大波峰时按照w_avg的位置在波峰内部切分。这个方案比较暴力但是工程上非常实用。关键是怎么定切割点一般把大区间的起始位置加上k*w_avg作为切分点k取1、2、3……同时要避免把本来就是一个宽字符的区域误切。可以先算出该区间宽度与w_avg的比值只有比值接近整数时才切否则判定为单字符。方案三连通域分析帮忙找弱连接点。用cv2.connectedComponentsWithStats把整个粘连区域拆成若干个连通块观察哪些连通块之间有细小的桥接像素桥接处就是切割点。这个思路比硬切精细代码量也更大适合项目锦上添花。我的建议是如果你的粘连问题是偶发情况用方案二加手动调参最快如果粘连频繁出现回到图像采集端去改善——提高分辨率、加强光照、减少运动模糊远比事后补救划算。4.3 先校正倾斜再谈投影投影算法有个隐含假设目标区域的排列和图像坐标轴对齐。如果文本歪了水平投影依然会把文字行整体投影成一个横条但行与行之间的低谷会被填平字符的波峰也会互相掺和导致切分全部乱套。我的经验是任何投影处理之前先判断图像是不是歪的。判断方法有两种。方法一轮廓法。对二值图找最小外接矩形看矩形的旋转角度。这个方法对整体文本块倾斜有效但如果图像里有多行式、横幅式的复杂版面它只能测出整体趋势。contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cnt max(contours, keycv2.contourArea) rect cv2.minAreaRect(cnt) angle rect[2] # 旋转角度方法二Hough直线检测。对边缘图做Hough变换找到最显著直线簇的平均角度。这个方法适合文档里的文本行因为文本行本身就构成一条条长直线。找到角度后用cv2.getRotationMatrix2D做仿射变换把图转正再投影切分就顺畅了。edges cv2.Canny(gray, 50, 150) lines cv2.HoughLinesP(edges, 1, np.pi / 180, threshold100) # 统计所有直线角度取众数作为倾斜角注意旋转校正会引入插值插值会轻微模糊边缘可能让字符边缘多出一些灰色过渡带。旋转完最好重新二值化一次再用投影。5. 投影的五个常见坑噪声、阈值与预处理5.1 二值化方向搞反这是最常见的低级错误但几乎每个项目都会遇到一次。做个简单测试把二值图打印出来数一数图像中文字区域是1255还是0。如果文字是黑色、背景是白色而你的投影统计的是非零像素那投影结果会把背景当成目标波峰波谷完全反着来后面全部白做。我的自查习惯是先直接cv2.imshow或者plt.imshow看一眼二值图确认文字区域颜色。然后给投影画曲线如果波峰数量和肉眼看到的行数/字符数正好对得上再往下走。这个检查30秒不到能省一晚上的排查时间。5.2 噪声毛刺干扰波峰波谷二值图如果带着椒盐噪声投影曲线上会多出很多细细的毛刺。毛刺造成的直接后果是波谷处被顶起来原本该断开的位置连上了或者不该出现的虚假小波峰被当成一个对象。处理顺序很重要。先做空间滤波再做投影而不是改了投影函数去适应噪声# 先中值滤波去源噪声 gray_denoised cv2.medianBlur(gray, 5) # 再做形态学开运算去除细小噪点 binary cv2.morphologyEx(binary, cv2.MORPH_OPEN, np.ones((3, 3), np.uint8))medianBlur对椒盐噪声特别有效开运算对小噪点非常有效。注意开运算的核不要过大3x3或5x5足够太大会削掉笔画边缘。做完这两步投影曲线会干净很多波谷能真正掉到0附近。5.3 二值化阈值导致断笔或笔画过粗投影切分依赖字符形状的完整性。如果阈值太高细笔画被当成背景一个日字可能断了横横竖竖投影变成几段小峰如果阈值太低噪声和笔画粘连在一起波谷被填平。这里没有银弹只有经验顺序先试Otsu全局阈值观察笔画光照不均就换自适应阈值微调blockSize和C如果笔画边缘断裂可以考虑在二值化之前先做一次光照校正比如原图减背景估计图。我用过一个很土的技巧把灰度图用高斯模糊大核比如50x50得到背景估计然后用gray - background 128做差这样能极大缓解光照不均匀之后二值化效果显著提升。5.4 投影对乱序区域无能为力投影只统计数量不统计位置。同一个波峰区间里可能存在左右两块完全不相干的独立区域比如排版里的分栏文本。垂直投影会把两栏文字当成一堆交错的波峰切出来是完全错乱的块。这时候必须升级工具。我的建议是投影先判断是否有多栏版面的嫌疑具体做法是水平投影出现多个高度接近、间隔规律的波峰群确认之后改用连通域或者形态学膨胀把分栏区域分割成独立块再对每个块单独做投影。换句话说投影适合处理规则排列的目标乱序区域应该交给连通域分析。5.5 大图上循环性能太差有人会把投影写成嵌套循环逐行逐列统计像素。这在算法课上没问题但放到工程里就是灾难。一张4000x3000的图嵌套循环跑一次可能要几十秒产品根本没法用。务必用向量化方案# 用NumPy一次性算完毫秒级 v_proj np.count_nonzero(binary, axis0) # 如果非要用OpenCV api记住reduce的结果要reshape v_proj_cv cv2.reduce(binary, 0, cv2.REDUCE_SUM).reshape(-1)同样是瓶颈写代码之前想一想这个函数是不是在循环里被反复调用。如果是视频流里每帧都做投影向量化是唯一能扛住的选择。6. 让投影更鲁棒的几个实用技巧6.1 投影曲线先平滑再找波谷前面讲滤波是针对图像本身的另一种思路是直接平滑投影曲线。投影曲线本质上是一维信号拿一个滑动窗口把毛刺抹掉波谷位置会更稳定。def smooth_proj(proj, window5): kernel np.ones(window, dtypenp.float32) / window return np.convolve(proj, kernel, modesame)窗口太大也不行会把字符间的真实空隙也抹平。经验值窗口取字符平均宽度的1/5左右最多千万不要超过字符宽度的一半。平滑曲线会让波峰位置偏移几个像素所以如果对分割边界精度要求很高比如要精准切到字符边缘最好在平滑后的曲线上定位大致区间回到原始投影曲线上精确定位边界。6.2 相对阈值永远比绝对阈值好用这个我在第3.3节表格里提过再展开说。绝对阈值写thresh5换一张图可能就要改成thresh20每换一批数据就调一次参迟早崩溃。相对阈值是拿当前投影曲线的最大值做归一化等于让算法自己适应每张图的明暗和分辨率变化。当然相对阈值也有失灵的时候当图像里既有粗体标题又有细体正文时标题的峰值是正文的好几倍正文可能全部低于max * ratio被误杀。这种情况下建议分段处理先把大峰值区域切出来剩余区域单独算阈值再切相当于做了一次两级分割。6.3 形态学预处理投影之前先清场形态学操作是投影的好搭档。开运算去噪点闭运算补断笔这两个准备动作做完投影切分几乎不会遇到虚假波峰和断裂波峰的问题。# 开运算先腐蚀再膨胀去孤立小噪点 binary cv2.morphologyEx(binary, cv2.MORPH_OPEN, np.ones((3, 3), np.uint8)) # 闭运算先膨胀再腐蚀补字符内部小洞、断缝 binary cv2.morphologyEx(binary, cv2.MORPH_CLOSE, np.ones((5, 5), np.uint8))闭运算对中文字符特别有效因为汉字结构复杂内部经常有细小空洞投影统计时这些空洞会拉低局部峰值闭运算能把空洞在连通性上补掉。但核别太大否则相邻字符也会被连起来反而造成粘连。6.4 投影与连通域的组合拳投影负责粗定位连通域负责细分割这个组合能解决大量实际问题。具体做法先用投影找出候选区域比如一行文本再对候选区域做连通域分析把每个字符或每个独立连通块精准地框出来。num_labels, labels, stats, centroids cv2.connectedComponentsWithStats(binary) for i in range(1, num_labels): x, y, w, h, area stats[i] # 过滤掉面积过小的噪声连通域 if area 20: continue # x,y,w,h 就是每个独立区块的包围盒投影擅长处理相邻但有空隙的对象连通域擅长处理形状连通但不规则的对象。两者结合几乎能覆盖所有规则文本切分场景。我实际写表格识别工具时就是先投影框出表格行列再用连通域把单元格里的内容拆成字段。6.5 表格线检测投影的另一半江山很多人不知道投影除了切字符还是检测表格线的利器。表格线在图像里就是横平竖直的长直线水平方向上某一行全是白色像素投影值会特别大垂直方向上同理。做法对二值图分别做水平投影和垂直投影投影值特别大、并且连续长度接近图像宽度的位置就是横线和竖线的候选位置。把这些位置记录下来横线和竖线一相交表格结构就出来了。h_proj np.count_nonzero(binary, axis1) # 表格横线所在行投影值接近图像宽度 line_rows np.where(h_proj int(binary.shape[1] * 0.8))[0]检测到表格线后把线所在的行列置为0生成掩码再用投影识别表格内的文字这样文字不会和表格线搅在一起。这套流程我做表格识别项目时天天用速度快到可以实时处理视频帧。6.6 积分投影比普通投影更抗噪的变体如果你遇到的场景噪声非常顽固普通投影曲线乱成一团可以试试积分投影Integral Projection。它统计的不是当前行/列的像素总数而是累积量相当于把投影曲线做了一次积分波峰会变得又宽又稳对小毛刺不敏感。实现上并不复杂v_proj_integral np.cumsum(np.count_nonzero(binary, axis0))用np.cumsum做完累积后原本的噪声毛刺被累积过程抹平了但其代价是波峰波谷的边界会变模糊。积分投影适合用来找大致区域不适合精确切分。我通常先用积分投影确定目标的行/列范围再用普通投影精切两个配合使用效果很好。写到这投影技术这个主题基本讲透了。我在实际项目里摸爬滚打下来最大的体会是投影算法看起来简单真正用好却需要你对二值化、形态学、连通域这些基础操作有足够深的理解。它不是一个调包就完事的魔法而是一个需要配合图像整体预处理才能发挥威力的工具。下次你遇到切分类任务不妨先画一张投影曲线看看很多时候答案已经写在波形里了。
返回列表