1. 从“找轮廓”到“读特征”:为什么特征区域分析是个分水岭
刚接触 OpenCV 那会儿,我判断一个区域“像什么”,基本靠肉眼看轮廓图。画个矩形框,数数像素点,觉得差不多了就交差。直到有一次做工业质检的模拟项目,需要从一堆金属件图像里区分合格品和瑕疵品,我才发现光靠轮廓根本不够——两个区域的面积几乎一样,但一个是正常纹理,一个是裂纹,肉眼在二值图上完全分不出来。
这就是“求特征区域”和“求特征区域的多种特征值”之间的差距。前者是找到目标在哪,后者是搞清楚目标到底是什么状态。OpenCV 求特征区域的多种特征值,说白了就是:先用轮廓检测或连通域分析把感兴趣区域(ROI)抠出来,然后对每个区域计算一组数值指标——面积、周长、圆形度、矩形度、长宽比、凸包缺陷、Hu 矩、颜色均值、纹理方差等等——用这些数值组合来判断区域的性质。
这套方法能解决的问题非常具体:工业上的缺陷检测、医学图像里的细胞计数与分类、农业里的果实分级、交通场景中的车辆类型区分、文档处理中的字符切分。适合谁看?有一定 OpenCV 基础、能写 Python 或 C++、做过轮廓查找但不知道怎么进一步量化的开发者。如果你还在用cv2.findContours之后只画个框就结束,那这篇内容就是给你补上“后半程”的。
我自己的体会是,特征值计算本身不难,难的是选哪些特征、怎么组合、阈值怎么定。下面我把这几年踩过的坑和总结的套路,按实际项目流程拆开讲。
2. 特征区域提取的底层逻辑与方案选型
2.1 先搞清楚“区域”是怎么来的
OpenCV 里拿到特征区域,主流就两条路:基于边缘的轮廓提取和基于像素的连通域分析。两者出来的东西看起来都是“一块区域”,但底层数据结构完全不同,后续能算的特征也不一样。
cv2.findContours返回的是轮廓点集,本质是一系列二维坐标点的序列。它的优势是边界信息精确,适合算周长、凸包、拟合椭圆、Hu 矩。缺点是如果边缘断裂,轮廓就断了,而且它不直接告诉你区域内部的像素分布。
cv2.connectedComponentsWithStats返回的是标签图和统计信息,每个连通域自带面积、外接矩形、质心。它的优势是区域完整性好,适合算像素级统计量,比如颜色均值、灰度方差。缺点是对边界形状的描述不如轮廓精细。
我的经验是:如果关注形状,走轮廓;如果关注内容,走连通域;如果两者都要,先连通域标记,再对每个标签提取轮廓。这个组合拳在实际项目里最稳。
2.2 预处理决定了特征值的可信度
很多人特征值算不准,问题不在计算函数,而在预处理。我见过太多人直接对原图findContours,结果噪声边缘全被当成区域,算出来的面积分布完全没法看。
标准流程应该是:灰度化 → 滤波去噪 → 二值化 → 形态学清理 → 再提取区域。每一步都有讲究。
滤波选高斯还是中值?高斯适合高斯噪声,中值适合椒盐噪声。工业图像里椒盐噪声多,我一般用cv2.medianBlur,核大小 3 或 5。二值化用 Otsu 还是自适应?光照均匀用 Otsu,光照不均用自适应高斯。形态学开运算去小噪点,闭运算填小孔洞,核大小根据目标尺寸定,一般是目标短边的 1/20 到 1/10。
注意:形态学操作会改变区域面积和形状。如果你要算精确面积,形态学核要尽量小,或者记录下操作前后的缩放关系。我吃过这个亏,开运算核给到 7,小目标直接消失了。
2.3 特征值体系的分类框架
我把常用特征值分成四类,方便按需选取:
| 类别 | 代表特征 | 适用场景 | 计算依赖 |
|---|---|---|---|
| 几何特征 | 面积、周长、长宽比、矩形度 | 形状筛选、尺寸分级 | 轮廓点集 |
| 形状特征 | 圆形度、凸度、Hu矩 | 类型区分、姿态识别 | 轮廓点集 |
| 统计特征 | 灰度均值、方差、颜色均值 | 材质区分、缺陷检测 | 掩膜+原图 |
| 纹理特征 | 局部二值模式、灰度共生矩阵 | 表面粗糙度、纹理分类 | 灰度图+区域掩膜 |
选特征的原则是:类间差异大、类内方差小、计算量可接受。不要一上来就算几十个特征,先可视化几个关键特征的分布,看能不能分开,再决定加不加。
2.4 为什么不用深度学习一步到位
有人会问,现在分割网络这么成熟,为什么还要手算特征值?我的回答是:数据量小、可解释性要求高、部署环境受限的场景,传统方法仍然是首选。工业产线上一个工控机,跑个轻量分割模型可能勉强,但跑 OpenCV 特征计算绰绰有余。而且特征值有明确的物理意义,出了问题能追溯,不像黑盒模型调参靠玄学。
当然,如果类别特别多、形态变化极大,传统特征确实吃力。这时候可以用特征值做初筛,再用小模型做精分类,这是折中方案。
3. 核心特征值的计算细节与实操要点
3.1 面积与周长:最基础但最容易算错
面积用cv2.contourArea,周长用cv2.arcLength。看起来简单,但有两个坑。
第一个坑:contourArea返回的是轮廓围成的多边形面积,不是像素计数。如果轮廓是锯齿状的,面积会偏小。想要精确像素面积,用连通域的area字段,或者对掩膜做cv2.countNonZero。
第二个坑:arcLength的第二个参数closed必须设True,否则周长少算最后一段。我见过有人设False,结果圆形度算出来大于 1,完全不合理。
圆形度公式是4 * pi * area / (perimeter ** 2),完美圆形为 1,越不规则越接近 0。这个特征对区分圆形零件和方形零件非常有效。
3.2 矩形度与长宽比:筛选规则形状的利器
矩形度是区域面积与外接矩形面积之比,反映区域填充矩形的程度。长宽比是外接矩形的宽高比。这两个特征配合使用,能快速筛掉明显不符合要求的区域。
cv2.boundingRect返回正矩形,cv2.minAreaRect返回最小面积旋转矩形。如果你的目标可能旋转,必须用后者。我做过一个零件分拣项目,零件在传送带上随机旋转,用正矩形算长宽比完全失效,换成旋转矩形后立刻稳定。
实操心得:
minAreaRect返回的角度在不同 OpenCV 版本里定义不同,4.5 之前是 [-90, 0),之后是 (0, 90]。跨版本迁移时一定要验证角度方向,否则长宽比会颠倒。
3.3 凸包与凸度:检测凹陷和破损
凸包是能包围区域的最小凸多边形,用cv2.convexHull计算。凸度是区域面积与凸包面积之比,完美凸形为 1,有凹陷就小于 1。
这个特征在缺陷检测里特别好用。比如检测药片缺角,正常药片凸度接近 1,缺角药片凸度明显下降。再配合cv2.convexityDefects找凹陷位置,能定位缺陷在哪。
计算凸度时要注意:凸包面积用contourArea算,区域面积也用contourArea,两者单位一致。如果区域本身有孔洞,凸度会偏小,这时候要先用cv2.fillPoly填充孔洞再算。
3.4 Hu 矩:旋转缩放平移不变的神器
Hu 矩是七个由中心矩归一化组合而成的量,对旋转、缩放、平移都不变。cv2.HuMoments直接返回这七个值。它们的数量级差异很大,通常取对数后再用。
Hu 矩适合做形状匹配。cv2.matchShapes就是基于 Hu 矩的,返回值越小越相似。我做过一个手写字符模拟识别,用 Hu 矩匹配数字 0 到 9,简单场景下准确率能到 85% 以上,而且计算极快。
但 Hu 矩不是万能的。它对形状的细微变化不敏感,两个不同字母可能 Hu 矩很接近。而且第七个 Hu 矩数值极不稳定,实际用的时候我一般只取前六个。
3.5 颜色与灰度统计:区域内容的直接描述
几何特征描述形状,统计特征描述内容。对每个区域生成掩膜,然后用cv2.mean或cv2.meanStdDev算均值和标准差。
颜色均值能区分不同颜色的目标,比如分拣红苹果和青苹果。灰度方差能反映纹理粗糙度,方差大说明区域内有明暗变化,可能是缺陷或纹理。
注意:颜色空间的选择很关键。RGB 对光照敏感,HSV 更稳。我一般转 HSV 后取 H 和 S 通道的均值,V 通道单独处理。如果是灰度图,直接算均值和方差即可。
3.6 纹理特征:局部二值模式的轻量方案
纹理特征里,局部二值模式(LBP)是性价比最高的。它计算简单,对光照变化有一定鲁棒性。OpenCV 有cv2.textureLBP或者自己实现也不难。
思路是:对区域内每个像素,比较其与周围 8 个邻居的灰度,大于等于记为 1,小于记为 0,组成一个 8 位二进制数,统计直方图作为纹理描述。直方图的分布能区分平滑表面和粗糙表面。
LBP 的缺点是旋转不变性差。如果目标会旋转,要用旋转不变 LBP 或者均匀模式 LBP。我一般用均匀模式,把 256 维降到 59 维,计算量小很多,效果损失不大。
4. 完整实操流程:从图像到特征值表
4.1 环境与数据准备
我用 Python 演示,依赖就三个:opencv-python、numpy、pandas。安装命令:
pip install opencv-python numpy pandas数据准备方面,我建议先用 OpenCV 自带的示例图或者自己生成模拟图。下面这段代码生成一张带多个形状的测试图,方便验证:
import cv2 import numpy as np canvas = np.zeros((600, 800, 3), dtype=np.uint8) canvas[:] = (255, 255, 255) cv2.circle(canvas, (150, 150), 80, (0, 0, 0), -1) cv2.rectangle(canvas, (300, 80), (450, 220), (0, 0, 0), -1) cv2.ellipse(canvas, (650, 150), (90, 50), 30, 0, 360, (0, 0, 0), -1) cv2.circle(canvas, (200, 420), 70, (0, 0, 0), -1) cv2.rectangle(canvas, (400, 350), (520, 500), (0, 0, 0), -1) cv2.imwrite("test_shapes.png", canvas)这张图里有圆、矩形、椭圆,还有两个形状相似但尺寸不同的目标,适合测试特征值的区分能力。
4.2 预处理与区域提取
拿到图像后,按标准流程走:
img = cv2.imread("test_shapes.png") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高斯滤波,核大小 5 blurred = cv2.GaussianBlur(gray, (5, 5), 0) # Otsu 二值化,注意反转,让目标为白色 _, binary = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) # 形态学开运算,去噪点 kernel = np.ones((3, 3), np.uint8) cleaned = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel, iterations=1) # 查找轮廓 contours, _ = cv2.findContours(cleaned, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) print(f"找到 {len(contours)} 个轮廓")这里RETR_EXTERNAL只取外轮廓,适合目标不嵌套的场景。如果有孔洞需要分析,用RETR_CCOMP或RETR_TREE。
4.3 逐区域计算多种特征值
核心循环来了。对每个轮廓,我计算面积、周长、圆形度、矩形度、长宽比、凸度、Hu矩前六个、灰度均值、灰度方差。代码:
import pandas as pd features_list = [] for i, cnt in enumerate(contours): area = cv2.contourArea(cnt) if area < 50: # 过滤太小区域 continue perimeter = cv2.arcLength(cnt, True) # 圆形度 circularity = 4 * np.pi * area / (perimeter ** 2) if perimeter > 0 else 0 # 外接矩形 x, y, w, h = cv2.boundingRect(cnt) rect_area = w * h extent = area / rect_area if rect_area > 0 else 0 aspect_ratio = w / h if h > 0 else 0 # 最小面积旋转矩形 rect = cv2.minAreaRect(cnt) (cx, cy), (rw, rh), angle = rect rot_aspect = max(rw, rh) / min(rw, rh) if min(rw, rh) > 0 else 0 # 凸包与凸度 hull = cv2.convexHull(cnt) hull_area = cv2.contourArea(hull) solidity = area / hull_area if hull_area > 0 else 0 # Hu 矩 moments = cv2.moments(cnt) hu = cv2.HuMoments(moments).flatten() hu_log = -np.sign(hu) * np.log10(np.abs(hu) + 1e-10) # 灰度统计 mask = np.zeros(gray.shape, dtype=np.uint8) cv2.drawContours(mask, [cnt], -1, 255, -1) mean_val, std_val = cv2.meanStdDev(gray, mask=mask) features_list.append({ "id": i, "area": area, "perimeter": perimeter, "circularity": circularity, "extent": extent, "aspect_ratio": aspect_ratio, "rot_aspect": rot_aspect, "solidity": solidity, "hu1": hu_log[0], "hu2": hu_log[1], "hu3": hu_log[2], "hu4": hu_log[3], "hu5": hu_log[4], "hu6": hu_log[5], "gray_mean": mean_val[0][0], "gray_std": std_val[0][0], }) df = pd.DataFrame(features_list) print(df.to_string())跑完这张测试图,你会得到一张特征值表。圆形区域的圆形度接近 1,矩形区域的矩形度接近 1,椭圆的长宽比明显大于 1。这就是特征值的威力——把视觉印象变成了可比较的数字。
4.4 特征值的归一化与筛选
原始特征值量纲不同,面积可能是几千,圆形度是 0 到 1。直接拿来做分类会被大面积特征主导。我一般做 Z-score 归一化:
from sklearn.preprocessing import StandardScaler feature_cols = ["area", "circularity", "extent", "aspect_ratio", "solidity", "gray_mean", "gray_std"] scaler = StandardScaler() df_scaled = df.copy() df_scaled[feature_cols] = scaler.fit_transform(df[feature_cols])归一化之后,可以用方差过滤或相关系数矩阵去掉冗余特征。比如aspect_ratio和rot_aspect高度相关,留一个就行。
4.5 可视化验证:画出来比看数字快
算完特征值,我习惯画两张图:一张是原始轮廓叠加编号,一张是特征值散点图。散点图用面积做 X 轴,圆形度做 Y 轴,不同颜色代表不同类别。如果类别在散点图上能分开,说明特征选对了。
vis = img.copy() for i, cnt in enumerate(contours): area = cv2.contourArea(cnt) if area < 50: continue M = cv2.moments(cnt) cx = int(M["m10"] / M["m00"]) cy = int(M["m01"] / M["m00"]) cv2.putText(vis, str(i), (cx, cy), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imwrite("contours_labeled.png", vis)这张图能帮你快速发现哪些区域被漏掉、哪些被误检。我经常在这步发现预处理参数需要调整。
5. 常见问题与排查技巧实录
5.1 特征值分布重叠严重怎么办
这是最常见的问题。两个类别的面积范围重叠,或者圆形度都在 0.7 到 0.9 之间,单靠一个特征分不开。
解决办法有三个方向。第一,增加特征维度。面积分不开,加上长宽比可能就分开了。第二,换特征。圆形度分不开,试试凸度或 Hu 矩。第三,组合特征。用两个特征的比值或乘积构造新特征,有时候有奇效。
我做过一个螺丝和垫片的分拣模拟,面积和圆形度都重叠,但凸度差异明显——螺丝有螺纹凹陷,凸度低;垫片是圆的,凸度高。换成凸度后准确率从 70% 跳到 95%。
5.2 旋转导致特征值不稳定
正矩形算出来的长宽比,目标一转就变了。这时候必须用minAreaRect。但minAreaRect的角度定义有版本差异,我建议自己统一角度到 [0, 90):
def normalize_angle(angle): while angle < 0: angle += 90 while angle >= 90: angle -= 90 return angle另外,Hu 矩本身对旋转不变,但前提是轮廓点集完整。如果旋转导致边缘断裂,Hu 矩也会漂。这时候要先做形态学闭运算把边缘连上。
5.3 小区域特征值噪声大
面积小于 100 像素的区域,周长和圆形度波动极大。我的做法是设一个最小面积阈值,低于阈值的直接丢弃。阈值怎么定?看你的目标最小尺寸。如果最小目标面积是 500,阈值设 200 到 300 比较安全。
如果小目标必须保留,那就先上采样图像再处理。cv2.resize放大 2 倍,特征值稳定性会好很多,代价是计算量增加。
5.4 灰度均值受光照影响大
同一类目标,光照强的地方灰度均值高,光照弱的地方低。直接用灰度均值分类,光照一变就失效。
解决方案是用相对值代替绝对值。比如用区域灰度均值减去全图灰度均值,或者用区域灰度均值除以全图均值。这样光照整体变化时,相对值保持稳定。
更好的方案是转 HSV 空间,用 H 和 S 通道。H 通道对光照不敏感,能反映真实颜色。但 H 通道在低饱和度时噪声大,要配合 S 通道做掩膜。
5.5 特征值计算速度慢
如果区域数量上千,逐个算 Hu 矩和灰度统计会明显变慢。优化方向:第一,用cv2.connectedComponentsWithStats一次性拿到面积和质心,只对候选区域算复杂特征。第二,灰度统计用积分图加速。第三,Hu 矩计算前先用cv2.approxPolyDP简化轮廓,点数少了计算快。
我实测过,1000 个区域,全量算 15 个特征大概 2 到 3 秒。优化后能压到 0.5 秒以内,满足产线实时要求。
5.6 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方向 |
|---|---|---|---|
| 轮廓数量异常多 | 噪声未清理干净 | 可视化二值图 | 加大滤波核或形态学核 |
| 面积明显偏小 | 轮廓锯齿或孔洞 | 对比掩膜像素数 | 用连通域面积或填充孔洞 |
| 圆形度大于 1 | 周长参数错误 | 检查 arcLength 的 closed | 设为 True |
| 长宽比随旋转变化 | 用了正矩形 | 检查是否用 minAreaRect | 换旋转矩形并统一角度 |
| 灰度均值不稳定 | 光照变化 | 对比不同光照下数值 | 用相对值或 HSV 空间 |
| 特征值区分度低 | 特征选择不当 | 画散点图看分布 | 增加或更换特征 |
| 计算速度慢 | 区域多或轮廓复杂 | 计时各步骤 | 简化轮廓或用连通域统计 |
6. 特征值组合策略与实战调参经验
6.1 先做特征重要性排序,再定阈值
拿到特征表后,不要急着定阈值。先用简单的统计方法看哪个特征区分度最高。我常用的是类间均值差除以类内标准差,值越大越好。
def discriminative_power(feature, labels): classes = np.unique(labels) if len(classes) != 2: return None m1 = feature[labels == classes[0]].mean() m2 = feature[labels == classes[1]].mean() s1 = feature[labels == classes[0]].std() s2 = feature[labels == classes[1]].std() return abs(m1 - m2) / (s1 + s2 + 1e-10)这个值大于 1.5 的特征,基本可以单独用。小于 0.5 的,考虑丢掉或组合。
6.2 阈值不是拍脑袋,用 ROC 曲线找最优
单个特征的阈值,可以用 ROC 曲线找最佳切点。sklearn.metrics.roc_curve直接给出来。最佳阈值通常是约登指数最大的点。
如果不想引入 sklearn,手动扫一遍也行:从最小值到最大值取 100 个点,每个点算准确率,取最高的。我一般用这个方法,简单直接。
6.3 多特征组合:投票法与加权评分
单特征不够,就多特征投票。比如圆形度大于 0.85 投一票,凸度大于 0.9 投一票,矩形度小于 0.7 投一票,三票中两票就判为圆形。
加权评分更灵活:每个特征归一化后乘权重,求和,再定总分阈值。权重怎么定?按区分度来,区分度高的权重大。我一般让权重和区分度成正比,归一化到和为 1。
6.4 一个完整的调参案例
模拟场景:区分合格齿轮和破损齿轮。合格齿轮轮廓完整,破损齿轮有缺口。
我选了三个特征:凸度、圆形度、Hu矩第二分量。先算区分度,凸度最高,权重给 0.5;圆形度次之,给 0.3;Hu矩给 0.2。归一化后加权求和,扫阈值,找到准确率最高的切点。
实测下来,凸度单独用准确率 88%,三特征加权后 96%。提升明显,但计算量只增加了不到 20%。
经验:特征不是越多越好。超过 5 个特征后,边际收益递减,而且过拟合风险增加。我一般控制在 3 到 5 个。
6.5 在线调参的小技巧
实际部署时,不可能每次都重新训练阈值。我的做法是留一个配置文件,把特征权重和阈值写进去,现场微调。再做一个简单的可视化界面,实时显示每个区域的特征值和判定结果,方便快速定位问题。
如果条件允许,加一个“学习模式”:操作员手动标注几个样本,系统自动更新阈值。这个功能在产线换型时特别省事。
7. 从特征值到决策:一个可复用的代码框架
7.1 框架设计思路
我把整个流程封装成一个类,输入图像,输出每个区域的特征值和分类结果。类的方法包括预处理、区域提取、特征计算、分类决策。这样换项目时只需要改参数和特征列表,不用重写逻辑。
class RegionFeatureAnalyzer: def __init__(self, min_area=100, feature_names=None): self.min_area = min_area self.feature_names = feature_names or [ "area", "circularity", "extent", "aspect_ratio", "solidity" ] self.scaler = None self.classifier = None def preprocess(self, 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) kernel = np.ones((3, 3), np.uint8) cleaned = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) return gray, cleaned def extract_regions(self, binary): contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) return [c for c in contours if cv2.contourArea(c) >= self.min_area] def compute_features(self, cnt, gray): area = cv2.contourArea(cnt) perimeter = cv2.arcLength(cnt, True) circularity = 4 * np.pi * area / (perimeter ** 2) if perimeter > 0 else 0 x, y, w, h = cv2.boundingRect(cnt) extent = area / (w * h) if w * h > 0 else 0 aspect_ratio = w / h if h > 0 else 0 hull = cv2.convexHull(cnt) hull_area = cv2.contourArea(hull) solidity = area / hull_area if hull_area > 0 else 0 return { "area": area, "circularity": circularity, "extent": extent, "aspect_ratio": aspect_ratio, "solidity": solidity, } def analyze(self, img): gray, binary = self.preprocess(img) contours = self.extract_regions(binary) results = [] for cnt in contours: feats = self.compute_features(cnt, gray) results.append(feats) return results这个框架跑起来,你只需要改feature_names和compute_features里的计算项,就能适配不同项目。
7.2 批量处理与结果导出
实际项目往往要处理成百上千张图。加一个批量循环,结果存 CSV:
import os import glob analyzer = RegionFeatureAnalyzer(min_area=100) all_results = [] for img_path in glob.glob("images/*.png"): img = cv2.imread(img_path) feats = analyzer.analyze(img) for f in feats: f["source"] = os.path.basename(img_path) all_results.append(f) df_all = pd.DataFrame(all_results) df_all.to_csv("all_features.csv", index=False)有了这张大表,后续做统计分析、训练分类器、定阈值都方便。
7.3 结果验证与迭代
导出结果后,我一般做三件事:第一,随机抽几张图,把特征值标在图上,肉眼核对。第二,画特征分布直方图,看有没有异常值。第三,用简单分类器(如决策树)跑一下,看特征是否够用。
决策树的好处是能直接输出特征重要性,告诉你哪个特征最有用。如果决策树深度很浅就能分开,说明特征选得好。如果树很深还分不开,说明特征不够,需要加。
7.4 部署时的注意事项
部署到实际环境,有几个点要特别注意。第一,图像尺寸要固定。不同分辨率的图,面积特征完全不可比。要么统一缩放,要么用相对面积。第二,光照要稳定。如果现场光照变化大,灰度特征要慎用,或者加光照补偿。第三,异常处理要完善。图像读取失败、轮廓为空、特征计算除零,这些都要有兜底逻辑。
我踩过最大的坑是:开发时用固定光源,部署时现场有自然光干扰,灰度均值全乱。后来加了 HSV 空间和相对灰度,才稳定下来。这个教训告诉我,实验室环境和现场环境的差距,往往比想象中大。
8. 特征值扩展方向与个人实操体会
8.1 从手工特征到自动特征
手工特征的上限是明确的。如果类别多、形态复杂,手工特征组合会变得极其繁琐。这时候可以考虑用特征值做初筛,再用一个小型神经网络做精分类。输入就是特征向量,网络很浅,训练快,部署也轻量。
另一个方向是用自编码器学习区域的压缩表示,再在隐空间做分类。这个方案对数据量要求高一些,但能捕捉手工特征难以描述的细微差异。
8.2 时序特征:视频场景的扩展
如果输入是视频,特征值可以按帧计算,形成时序序列。比如面积随时间的变化、质心位移速度、圆形度的波动。这些时序特征能判断运动状态,比如区分静止目标和运动目标,或者检测目标形变。
我做过一个传送带上的目标跟踪模拟,用面积变化率判断目标是否被遮挡,用质心位移判断运动方向,效果比单纯用跟踪算法更稳。
8.3 多区域关系特征
单个区域的特征值之外,区域之间的关系也能作为特征。比如两个区域的距离、面积比、相对角度。这些关系特征在装配检测、计数场景里很有用。
计算关系特征时,注意区域编号的稳定性。findContours返回的顺序不保证一致,最好按质心坐标排序后再计算关系。
8.4 我个人在实际操作中的体会
干了这么多年,我最大的体会是:特征值计算是手段,不是目的。不要为了算特征而算特征,要先想清楚要区分什么,再倒推需要什么特征。我见过有人算了二十几个特征,结果分类效果还不如三个精心挑选的。
另一个体会是:可视化永远是最快的调试手段。数字看不出的问题,画出来一目了然。我每次调参,第一步都是把中间结果画出来看,比盯着控制台输出高效得多。
最后,阈值要留余量。实验室里卡得刚刚好的阈值,到现场往往就失效了。我一般把阈值往宽松方向调 10% 到 20%,宁可漏检一点,也不要误检太多。漏检可以补,误检会拖垮整个流程。
这个方向后续还可以往轻量化部署走,比如把特征计算用 C++ 重写,或者用 OpenCV 的 GPU 模块加速。如果区域数量特别大,还可以考虑并行计算,每个区域独立算特征,多线程跑满 CPU。这些扩展我都在不同项目里试过,效果因场景而异,但核心思路不变:先明确要什么,再选特征,最后调阈值。