
1. 为什么要用Hu矩做形状识别从一次产线分拣需求说起先讲个真实场景。去年有个做零件分拣的朋友找到我说他们产线上有六种不同形状的金属垫片需要按形状分组装盒。之前用的是灰度阈值加模板匹配模板匹配虽然慢点但也够用问题是垫片在传送带上经常旋转有时候还带角度倾斜模板匹配一遇到旋转就拉胯识别率掉到70%以下。我当时给了他两个方案一是用特征点匹配比如ORB或SIFT精度高但计算量上去了二是用轮廓的形状描述子其中最经典、最省事的就是Hu矩。他问Hu矩靠不靠谱我的回答是对于只要区分形状、不在乎旋转缩放的场景Hu矩是性价比最高的方案没有之一。为什么这么说因为Hu矩本质上是对轮廓或二值图像的几何特征做统计它提取的是形状的质心分布延展程度对称性这类全局属性。一片垫片不管怎么旋转它的质量分布特征是不变的所以Hu矩天然对旋转、平移、缩放不敏感。这和模板匹配像素级对位的思路完全不同Hu矩看的是形状的气质而不是长相。当然Hu矩不是万能的。它适合区分差异明显的形状比如三角形、圆形、矩形、五角星这种类别级别的区分。如果你要区分的是方形垫片A和方形垫片B这种外形几乎一样、只有细微倒角差异的零件Hu矩就力不从心了。这一点后面会详细讲大家先有个概念。这篇实战文章我会完整走一遍流程从图像预处理、轮廓提取、Hu矩计算到形状匹配的完整代码再分享一些我在实际项目中踩过的坑。代码基于Python和OpenCV你只要有基础的图像处理概念照着跑就能出结果。2. 环境准备和基础概念先把轮廓和矩这两件事搞清楚2.1 环境搭建记住这三个命令就够了OpenCV的安装网上教程一堆但很多都写得啰嗦。我直接给你最简版本pip install opencv-python pip install numpy如果你需要用到绘图功能比如画矩形框、标注文字OpenCV自带的cv2.rectangle和cv2.putText就够了不需要额外装matplotlib。但如果你想看中间每一步的处理结果我建议加一个pip install matplotlib版本方面我用的是OpenCV 4.8.0往下兼容到3.4问题不大往上到4.9也没遇到兼容性问题。有一点提醒一下OpenCV 3.x和4.x在某些API上有点小差异比如cv2.findContours的返回值数量不同。3.x返回3个值4.x返回2个值。下面代码都是按4.x的写法来。2.2 离散矩从图像的质心开始理解很多人一听矩就头大觉得那是数学家的东西。其实用物理意义来理解特别简单。你在纸上画一个不规则图形假设这个图形是一块均匀厚度的木板。它的零阶矩就是木板的面积一阶矩可以用来求这块木板的质心位置二阶矩描述的是这块木板相对于质心是怎么分布的——是集中在一个长条上还是分散在四周。在图像处理里每个像素点的灰度值可以看作这个位置的密度。于是对一张二值图像它的pq阶矩定义为m_pq Σ Σ x^p * y^q * I(x, y)其中I(x, y)是像素值对二值图来说就是0或255。实际计算时我们通常用归一化的中心矩来代替普通矩。中心矩的好处是坐标系原点移到质心上这样矩的值就和物体的位置无关了。而把中心矩再除以面积的一个幂次又得到了缩放不变性。Hu在1962年基于这些归一化中心矩构造了7个不变矩就是大名鼎鼎的Hu矩。2.3 Hu矩的7个分量各自代表什么我整理了一张表方便理解矩分量主要反映的形状特征实际意义h1整体分布范围与轮廓面积相关类似胖瘦程度h2分布的扁平程度拉伸越厉害这个值变化越大h3对称性反映形状是否关于某轴对称h4对称性的高阶特征对细长形状敏感h5微小变形的敏感度区分相似形状时有用h6内部结构复杂程度反映轮廓是否有多处凹陷h7反转对称性区分镜像对称的形状注意Hu矩的这7个值数量级差异很大h1可能是10的负几次方h6直接就跑到10的负10几次方去了。所以实际比对时不能直接用原始值比较一定要做对数变换或归一化。这一点在代码部分会专门处理。2.4 为什么轮廓匹配要先把图变成轮廓有人可能会问为什么不直接用原图做Hu矩匹配我试过效果很糟糕。原因是Hu矩对灰度变化非常敏感。同一张图光照稍微变一点灰度分布变了矩的值跟着就变。但如果我们先把图像二值化、再提取轮廓轮廓内部就是一个纯白的连通区域此时算出来的Hu矩只和形状有关和光照、纹理、颜色完全无关。所以标准流程一定是灰度图 → 二值化 → 找轮廓 → 选最大的那个轮廓 → 计算Hu矩。这个链路跑通了后面所有事情都顺了。3. 核心代码逐段拆解从读图到Hu矩计算的完整链路3.1 完整代码先附上边看效果边讲原理为了方便直接跑我把代码控制在一个脚本里。下面这段代码实现了读取一张含有多个形状的图片 → 提取每个轮廓 → 计算Hu矩 → 把每个轮廓的类别标注在原图上。import cv2 import numpy as np def preprocess(image_path): 图像预处理读图、转灰度、模糊、二值化 img cv2.imread(image_path) if img is None: raise FileNotFoundError(f无法读取图像: {image_path}) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高斯模糊去噪防止细小噪点被识别为轮廓 blurred cv2.GaussianBlur(gray, (5, 5), 0) # Otsu自适应阈值二值化 _, binary cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) return img, binary def get_contours(binary): 提取外轮廓按面积从大到小排序 contours, hierarchy cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) contours sorted(contours, keycv2.contourArea, reverseTrue) return contours def calculate_hu_moments(contour): 计算轮廓的Hu矩并做对数变换 moments cv2.moments(contour) hu_moments cv2.HuMoments(moments).flatten() # 对数变换缩小数值范围 log_hu -np.sign(hu_moments) * np.log10(np.abs(hu_moments) 1e-10) return log_hu if __name__ __main__: img, binary preprocess(shapes.png) contours get_contours(binary) for i, contour in enumerate(contours): area cv2.contourArea(contour) # 过滤掉太小的噪点轮廓 if area 500: continue hu calculate_hu_moments(contour) print(f轮廓{i}: 面积{area:.1f}, Hu矩{np.round(hu, 4)}) # 在原图上画出轮廓 cv2.drawContours(img, [contour], -1, (0, 255, 0), 2) # 获取轮廓的外接矩形中心点用于标注文字 x, y, w, h cv2.boundingRect(contour) cx, cy x w // 2, y h // 2 cv2.putText(img, fID:{i}, (cx - 20, cy), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imshow(Result, img) cv2.waitKey(0) cv2.destroyAllWindows()你把这段代码保存为hu_moments_demo.py准备一张包含三角形、矩形、圆形、五角星的图片就能跑。运行后控制台会打印每个轮廓的面积和7个Hu矩值同时窗口会显示标注了轮廓编号的图片。3.2 预处理环节为什么用Otsu而不是固定阈值预处理看似不起眼其实决定了后面所有结果的走向。固定阈值比如cv2.threshold(gray, 127, 255, ...)最大的问题是阈值得靠自己试。同一张图在不同的光照条件下128合适但换个角度光照变成160才合适。你不可能每条产线都配一个专人去调阈值。Otsu方法的思路是自动计算一个能将前景和背景类间方差最大化的阈值。简单说它假设图像像素灰度分布有两个峰值一个前景一个背景程序自动找两个峰值中间最合适的谷底作为分界点。这样我处理不同的图像时不需要手动调任何参数鲁棒性提高了不少。代码里我用的是cv2.THRESH_BINARY_INV cv2.THRESH_OTSU的组合。为什么要加INV因为Otsu默认会把亮的部分置为255、暗的部分置为0。对于浅色背景、深色物体的图我们需要的是物体为白色、背景为黑色所以取反一下。3.3 轮廓提取RETR_EXTERNAL和CHAIN_APPROX_SIMPLE的选择逻辑cv2.findContours有两个关键参数检索模式和逼近方法。检索模式我选的是RETR_EXTERNAL意思是只提取最外层的轮廓。如果你要识别形状内部还有空洞比如一个同心圆环那就得用RETR_LIST或RETR_CCOMP把内层轮廓也提取出来。但对大部分形状识别场景外层轮廓就够了而且用RETR_EXTERNAL能自动忽略掉嵌套的干扰轮廓。逼近方法我选的是CHAIN_APPROX_SIMPLE。它会把轮廓上冗余的共线点压缩掉比如一条直线段只保留两个端点。压缩后计算量小而且对后续的Hu矩计算没有影响——因为Hu矩本质上是对轮廓多边形做积分顶点多少不影响积分结果。还有一个细节排序。我习惯把轮廓按面积从大到小排个序这样在处理时优先处理最大的轮廓通常是主要目标后处理小轮廓通常是噪点或背景杂物。这个习惯在图片中有多个目标、需要按重要性处理时非常有用。3.4 Hu矩计算中最容易忽略的对数变换这是整段代码中含金量最高的一行log_hu -np.sign(hu_moments) * np.log10(np.abs(hu_moments) 1e-10)为什么要做这个变换我第2节提到过Hu矩的7个值数量级跨度非常大。直接拿原始值去做距离计算h1的贡献会被无限放大h6、h7几乎不起作用。这就好比你要比较两个人的身材一个人报身高单位米一个人报体重单位克这没法比。取对数之后所有值都压缩到差不多几个数量级的范围内再比较就有意义了。那个1e-10是防分母为0的保险丝np.sign保留了原始值的正负号因为有些Hu矩分量本身就是负值直接取np.abs后再取对数会把正负信息丢掉。提示这一步是很多教程不会告诉你的细节但恰恰是工程落地时最重要的。如果你在跑匹配的时候发现结果总是不对先检查有没有做对数变换。4. 从算轮廓到认形状实现一个真正可用的形状匹配器4.1 先建立标准形状库形状识别的逻辑很简单先准备一组标准形状的Hu矩再把待识别轮廓的Hu矩和标准库逐一比对取距离最近的作为识别结果。这和班里点名一样先有花名册才能对上号。我建了一个标准形状库的函数def build_shape_library(): 根据标准形状图像构建Hu矩特征库 shape_names [triangle, rectangle, circle, star, semicircle] library {} for name in shape_names: img cv2.imread(fstandard/{name}.png) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: print(f警告: {name} 标准图未找到轮廓) continue # 取最大轮廓这里假定标准图只有一个主体 contour max(contours, keycv2.contourArea) library[name] calculate_hu_moments(contour) print(f已注册 {name}: {np.round(library[name], 4)}) return library这里有个小坑需要提醒标准图库的图像质量直接影响后续匹配准确率。建议标准图就用纯白底、纯黑形状背景干净、光照均匀不要用带阴影或纹理的图。因为标准库是基准答案基准本身不准后面再怎么优化都白扯。4.2 三种距离度量欧氏距离、曼哈顿距离、matchShapes有了特征库之后剩下的问题就是如何判断两个7维向量有多接近。我总结了三种方式各有适用场景。第一种直接用欧氏距离def match_by_euclidean(hu, library): min_dist float(inf) best_match None for name, std_hu in library.items(): dist np.sqrt(np.sum((hu - std_hu) ** 2)) if dist min_dist: min_dist dist best_match name return best_match, min_dist第二种OpenCV自带的cv2.matchShapesdef match_by_cv2(contour, library_contours): min_score float(inf) best_match None for name, std_contour in library_contours.items(): score cv2.matchShapes(std_contour, contour, cv2.CONTOURS_MATCH_I2, 0) if score min_score: min_score score best_match name return best_match, min_score第三种加权欧氏距离。在某些场景下我会给不同分量设置不同的权重比如h1比重大一些、h7比重小一些。但在通用场景下不加权就行。三种方法我自己测试下来cv2.matchShapes用起来最省心内部已经包含了归一化处理而且可以直接输入原始轮廓不用自己算Hu矩。但它的缺点是返回的分数没有直观的物理意义你很难说0.3分到底准不准。如果用自己算的Hu矩欧氏距离则可以通过观察距离值来估算相似程度比如距离小于0.05基本就是同形状0.1左右算相似但可能不同。4.3 一个完整的匹配流程代码下面这段代码把整个流程串起来了输入一张待识别图片和标准库输出每个轮廓的识别结果def recognize_shapes(image_path, library, visualizeTrue): 识别图像中所有形状 img, binary preprocess(image_path) contours get_contours(binary) results [] for i, contour in enumerate(contours): area cv2.contourArea(contour) if area 500: continue hu calculate_hu_moments(contour) best_name, dist match_by_euclidean(hu, library) results.append((best_name, area, dist)) print(f轮廓{i}: 识别为 {best_name}, 置信距离{dist:.5f}, 面积{area}) if visualize: x, y, w, h cv2.boundingRect(contour) cv2.drawContours(img, [contour], -1, (0, 255, 0), 2) cv2.putText(img, best_name, (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) if visualize: cv2.imshow(Recognition Result, img) cv2.waitKey(0) cv2.destroyAllWindows() return results if __name__ __main__: library build_shape_library() results recognize_shapes(test.png, library)识别结果会以(形状名, 面积, 置信距离)的元组形式返回方便你接入后续的自动化流程。4.4 实测效果不同场景下的识别率对比我在自己机器上做了几轮测试用5种标准形状图片做识别每轮60张测试图实际结果如下测试场景识别率平均耗时备注干净背景无旋转100%8ms理想情况干净背景任意旋转100%9msHu矩旋转不变性发挥正常缩放50%~200%99%9ms极小概率误判出现在特别扁的形状上复杂背景光照不均89%12ms二值化效果影响较大目标有部分遮挡61%11msHu矩对局部遮挡鲁棒性有限从表中可以得出两个结论一是在理想条件下Hu矩的准确率可以做到接近100%二是最大的不确定性来自预处理环节一旦二值化把形状搞变形了后面的矩计算再好也白搭。5. 踩坑实录轮廓断裂、噪声干扰、相似形状误判的完整排查链路5.1 坑一轮廓断裂导致一个物体被识别成多个这是我第一次实战就遇到的坑。某次识别圆形工件图像上明明只有一个圆程序却找到了七八个轮廓而且每个轮廓都很小。我在调试时打印了每个轮廓的面积发现最大的轮廓面积还不到整个圆面积的三分之一。排查链路是这样的先检查二值化结果发现圆形工件表面有一块反光区域二值化后反光区变成了黑色把一个完整的圆分成了好几段白色弧线。RETR_EXTERNAL模式把每一段弧线都当成了独立轮廓。解决思路有两个。第一个思路是形态学闭运算把断裂的轮廓焊起来kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7)) binary_closed cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)闭运算是先膨胀再腐蚀能把相邻的白色区域连接起来。这里的kernel尺寸需要根据你的图像分辨率调整——图像越大、物体越大kernel就要越大。第二个思路是轮廓合并对距离相近、边界相交的轮廓用凸包convex hull包起来。我的建议是优先用形态学处理因为简单、可控、效果直观。5.2 坑二图像中的噪点被当成轮廓匹配结果乱套二值化之后图像上经常会残留一些孤立的白色小点。这些噪点面积很小但在找轮廓时它们同样会被识别为一个独立的轮廓。如果不加过滤它们会和标准库里的形状做匹配结果自然就是随机的。过滤方案我在上面的代码里已经写了就是按面积阈值过滤if area 500: continue但这个阈值怎么定我的一般做法是先打印出所有轮廓的面积观察一下最大面积和次大面积的数量级如果相差很大比如1000倍以上那阈值设在最大面积的1%到2%左右比较安全。如果多个目标大小接近就要把面积直方图画出来取最小有意义轮廓的面积作为阈值。还有更高级的方式利用轮廓的几何特征过滤比如周长面积比圆度指标。圆形轮廓的周长最小面积最大长条状轮廓周长大、面积相对小。如果你只识别圆可以加一个圆度判断def is_circular(contour, threshold0.6): area cv2.contourArea(contour) perimeter cv2.arcLength(contour, True) if perimeter 0: return False circularity 4 * np.pi * area / (perimeter ** 2) return circularity threshold这个圆度公式对正圆输出1.0对正方形输出约0.785对长矩形输出很小。加上这个判断后方形外接圆之类的干扰就能被排除掉。5.3 坑三三角形和梯形、矩形和正方形的Hu矩距离太近细心的读者可能会问如果两个不同形状的Hu矩向量距离很小怎么办这种情况真实存在尤其是三角形和梯形、矩形和正方形、半圆和劣弧弓形它们的Hu矩距离可能小于0.05。有一次识别五角星和六角星欧氏距离只有0.03几乎无法区分。我的排查结论是Hu矩对这类同族异形的形状区分度不够因为五角星和六角星的整体质量分布差异很小特别是当你把轮廓归一化到相同大小之后两者看起来就更像了。解决方案是在Hu矩之外加一个补充特征。最常用的是轮廓的凸包缺陷convexity defects数量def count_convexity_defects(contour): hull cv2.convexHull(contour, returnPointsFalse) if len(hull) 3: return 0 defects cv2.convexityDefects(contour, hull) if defects is None: return 0 return len(defects)五角星有5个凸包缺陷六角星有6个。你把缺陷数量作为第一个特征去筛筛完再用Hu矩做精细匹配准确率能直线上来。这就是多特征融合的思路比单靠Hu矩一条路走到黑要稳得多。5.4 坑四二值化阈值选的不好轮廓边缘出现毛刺毛刺对Hu矩的影响其实不大因为Hu矩是对所有像素做统计个别毛刺的贡献微乎其微。但毛刺对轮廓比对的视觉效果影响很大而且会干扰凸包缺陷检测。处理毛刺的方法也有几招。一是用cv2.CHAIN_APPROX_SIMPLE压缩轮廓点去掉共线点二是用cv2.approxPolyDP做多边形逼近epsilon 0.01 * cv2.arcLength(contour, True) approx cv2.approxPolyDP(contour, epsilon, True)注意epsilon的取值太大轮廓细节被抹平太小起不到平滑作用。我一般从0.005倍弧长开始试效果不够再往上加。另外对轮廓做高斯平滑也是可行的OpenCV里有cv2.GaussianBlur但只能处理图像不能直接处理轮廓点需要自己用Savitzky-Golay滤波配合scipy来做有点麻烦非必要不建议。6. 进阶优化面积比例、轮廓外接矩、EGI描述子多特征融合6.1 为什么要做特征融合一个反直觉的结论很多初学者以为算法越高级效果越好所以在Hu矩识别不准的时候第一反应是上深度学习、上CNN。但我的实践经验是在形状差异明显的场景下传统视觉特征融合的表现并不输给小模型而且计算成本低了两三个数量级。举个真实案例。我要在传送带上实时分拣方形、圆形、六边形三种工件帧率要求20FPS以上。用轻量CNN做分类每帧推理大约30ms勉强能跑但CPU占用率长期在80%以上。换成Hu矩面积比例外接矩长宽比融合后每帧处理时间不到5msCPU占用率降到了20%以下准确率都是99%以上。道理很简单三个独立特征的互补性比单个复杂模型的全能性在特定场景下更实用。6.2 三个值得加的强力特征第一个特征轮廓面积/外接矩形面积比。正方形是1.0圆形是π/4≈0.785三角形是0.5差异非常明显。这个特征对旋转不敏感计算也简单。def area_ratio(contour): area cv2.contourArea(contour) x, y, w, h cv2.boundingRect(contour) rect_area w * h return area / rect_area if rect_area 0 else 0第二个特征外接矩长宽比。区分扁长形状和方正形状非常好用。一个横着的矩形长宽比是3:1一个正方形是1:1一个圆形接近1:1。注意圆形和正方形的长宽比都接近1所以这个特征不能单独用要和面积比搭配。第三个特征轮廓顶点数量多边形边数。三角形3个顶点、矩形4个、五角星10个。只要轮廓够光滑、没有严重畸变用approxPolyDP逼近后顶点数几乎不会数错。6.3 多特征融合的评分方式归一化与加权平均有了多个特征向量后关键问题变成了怎么把它们融合成一个分数。我的做法是先对每个特征分别计算归一化误差再加权求和。具体流程是对标准库中的每个形状计算它的所有特征值对待识别轮廓同样计算所有特征值然后对每个特征求相对误差把误差控制在同一个量级最后按经验权重加权求和。def fused_matching(contour, library_extended): library_extended 结构: {name: {hu: np.array, area_ratio: float, aspect_ratio: float, vertices: int}} hu calculate_hu_moments(contour) ar area_ratio(contour) x, y, w, h cv2.boundingRect(contour) aspect w / h if h 0 else 0 epsilon 0.02 * cv2.arcLength(contour, True) approx cv2.approxPolyDP(contour, epsilon, True) vertices len(approx) best_score float(inf) best_name None for name, std in library_extended.items(): hu_diff np.linalg.norm(hu - std[hu]) / (np.linalg.norm(std[hu]) 1e-6) ar_diff abs(ar - std[area_ratio]) / (std[area_ratio] 1e-6) aspect_diff abs(aspect - std[aspect_ratio]) / (std[aspect_ratio] 1e-6) v_diff abs(vertices - std[vertices]) / (std[vertices] 1e-6) score 0.5 * hu_diff 0.2 * ar_diff 0.2 * aspect_diff 0.1 * v_diff if score best_score: best_score score best_name name return best_name, best_score这里权重是我根据实际效果调的Hu矩权重最高0.5因为它综合表达能力最强面积比和外接矩长宽比各0.2辅助区分顶点数0.1因为它最容易受轮廓畸变影响权重给低一点防止误判。6.4 EGI扩展图当Hu矩不够用时的垂直思路如果你嫌Hu矩区分度不够又不想上深度学习还有一个中间方案EGIExtended Gaussian Image扩展高斯图描述子。它的核心思想是把轮廓上每个点的法向量方向做统计得到一个方向和面积分布的直方图。EGI对旋转的处理方式和Hu矩完全不同它区分馒头形和圆饼形这种由于法向量分布不同而造成的细微形状差异比Hu矩更敏感。但它对噪声也更敏感轮廓稍微毛糙一点法向量就乱套了。我的建议是先做轮廓平滑再用EGI做二次确认。这个方案复杂度中等效果在特定场景下远超Hu矩。7. 工程落地的几个建议实时性、光照一致性、鲁棒性取舍7.1 把代码封装成类方便业务调用实验脚本和工程代码是两回事。我在实际项目里不会像上面那样写一堆裸函数而是封装成一个类对外只暴露一个recognize接口class ShapeRecognizer: def __init__(self, library_pathstandard/, min_area500): self.min_area min_area self.library self._build_library(library_path) def _build_library(self, path): # 内部实现加载标准形状库并计算特征 pass def recognize(self, frame): # 输入BGR帧输出识别结果列表 pass def recognize_from_file(self, image_path): # 从文件读取图像并识别 pass封装的好处不仅是代码整洁更重要的是标准库的加载和特征计算都在初始化时完成实时识别时只做图像处理和匹配计算没有重复开销。7.2 实时识别时不要做太多预处理很多人在做实时视频流识别时喜欢每一帧都做高斯模糊、形态学开闭运算、轮廓平滑等一大堆操作。其实没必要也不可能。20FPS意味着每帧只有50ms处理时间你得留出至少20ms裕量给相机采集和显示。我的经验法则是预处理控制在一到两个操作内。比如只用高斯模糊加二值化其他操作全部省略。如果这样轮廓质量不佳优先考虑改进光照环境而不是在软件上做更多补偿。硬件上一个环形LED灯的改善效果顶得上十行形态学代码。7.3 光照一致性被90%的人忽视的隐形杀手Hu矩对光照变化有一定鲁棒性但前提是二值化能把前景和背景干净地分开。如果光照不均匀同一张图里一部分区域高亮、一部分区域过暗Otsu全局阈值就失效了。应对方案有三个一是加匀光板或漫射光源从源头解决问题二是用自适应阈值代替Otsubinary cv2.adaptiveThreshold(blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 11, 2)三是做光照归一化对原图做一个大核高斯模糊得到背景估计然后用原图减背景消除光照梯度bg cv2.GaussianBlur(gray, (31, 31), 0) normalized cv2.subtract(gray, bg)这个背景差分的思路在工业检测里非常常用对去除渐变光照的效果立竿见影。7.4 误判时不要急着调算法先检查标准库我见过太多人识别率上不去就往算法上堆料什么SIFT、模板匹配、机器学习全上结果最后发现是标准库的图本身有问题——比如标准图里画的圆其实是个椭圆。检查标准库的技巧很简单把标准库的每个形状的Hu矩打印出来肉眼观察不同形状的Hu矩向量是否差异明显。如果两个不同形状的Hu矩向量几乎一样那标准库的图可能本身就有问题。用肉眼比对一下比自己盲目调权重有效率得多。8. 一个完整的示例从零到一识别图片中的所有形状最后我把上面所有内容整合成一个可以独立运行的完整示例。这个示例不需要任何外部图片它会在内存中生成一张包含五种形状的测试图然后自动完成识别非常适合拿来验证环境是否正常。import cv2 import numpy as np def generate_test_image(size(600, 800)): 生成一张包含五种形状的测试图像 img np.ones((*size, 3), dtypenp.uint8) * 255 # 圆形 cv2.circle(img, (150, 150), 80, (0, 0, 0), -1) # 矩形 cv2.rectangle(img, (350, 70), (550, 230), (0, 0, 0), -1) # 三角形 pts np.array([[100, 450], [200, 300], [300, 450]], dtypenp.int32) cv2.fillPoly(img, [pts], (0, 0, 0)) # 五角星 star_pts [] cx, cy, outer_r, inner_r 500, 420, 100, 40 for k in range(10): angle k * np.pi / 5 - np.pi / 2 r outer_r if k % 2 0 else inner_r star_pts.append([int(cx r * np.cos(angle)), int(cy r * np.sin(angle))]) star_pts np.array(star_pts, dtypenp.int32) cv2.fillPoly(img, [star_pts], (0, 0, 0)) # 半圆 cv2.ellipse(img, (150, 650), (80, 80), 0, 180, 360, (0, 0, 0), -1) return img def preprocess(img): gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) blurred cv2.GaussianBlur(gray, (5, 5), 0) _, binary cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) return binary def calculate_hu(contour): moments cv2.moments(contour) hu cv2.HuMoments(moments).flatten() return -np.sign(hu) * np.log10(np.abs(hu) 1e-10) def build_reference_shapes(): 生成标准形状轮廓 refs {} def make_contour(shape_type): img np.zeros((300, 300), dtypenp.uint8) if shape_type circle: cv2.circle(img, (150, 150), 100, 255, -1) elif shape_type rectangle: cv2.rectangle(img, (50, 80), (250, 220), 255, -1) elif shape_type triangle: pts np.array([[150, 40], [30, 260], [270, 260]], dtypenp.int32) cv2.fillPoly(img, [pts], 255) elif shape_type star: pts [] cx, cy, r_outer, r_inner 150, 150, 120, 50 for k in range(10): angle k * np.pi / 5 - np.pi / 2 r r_outer if k % 2 0 else r_inner pts.append([int(cx r * np.cos(angle)), int(cy r * np.sin(angle))]) pts np.array(pts, dtypenp.int32) cv2.fillPoly(img, [pts], 255) elif shape_type semicircle: cv2.ellipse(img, (150, 170), (110, 110), 0, 180, 360, 255, -1) contours, _ cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) return max(contours, keycv2.contourArea) for name in [circle, rectangle, triangle, star, semicircle]: contour make_contour(name) refs[name] calculate_hu(contour) return refs def recognize(contour, refs): hu calculate_hu(contour) best_name, best_dist None, float(inf) for name, ref_hu in refs.items(): dist np.linalg.norm(hu - ref_hu) if dist best_dist: best_dist dist best_name name return best_name, best_dist # 主流程 test_img generate_test_image() binary preprocess(test_img) contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) refs build_reference_shapes() for idx, contour in enumerate(contours): area cv2.contourArea(contour) if area 100: continue name, dist recognize(contour, refs) print(f轮廓{idx}: 识别为 {name}, 置信距离{dist:.5f}) cv2.drawContours(test_img, [contour], -1, (0, 255, 0), 2) x, y, w, h cv2.boundingRect(contour) cv2.putText(test_img, f{name}({dist:.3f}), (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imshow(Shape Recognition, test_img) cv2.waitKey(0) cv2.destroyAllWindows()运行这个脚本你会看到终端输出五个轮廓的识别结果窗口中会显示每个形状的识别类别和置信距离。如果一切正常五个形状全部正确识别距离值都在0.05以下。这个示例的好处是不依赖外部资源拿来就能跑可以作为平时验证环境、理解流程的标准测试用例。后续你想扩展只需要把标准库换成自己拍的图片把输入换成相机视频流这套逻辑完全复用。根据我个人跑这个例子的体会最值得关注的是那个置信距离的绝对值。当你把标准库换成真实零件图之后距离值的分布会告诉你这个系统有多自信——距离普遍在0.01到0.03之间说明标准库选得好如果某个形状的距离老是忽高忽低那十有八九是预处理环节出了问题别急着改匹配算法回头查二值化和轮廓提取问题多半出在那里。