简介:提供一套使用Python与OpenCV训练自定义物体识别模型的完整资源,面向希望摆脱预设模型、针对自身场景识别任意物体的学习者和开发者。内容覆盖数据收集、预处理、特征提取、模型训练、验证调优及实时检测全流程。资源包共709个文件,其中663张jpg样本与28张png素材用于正负样本训练,10个xml标注文件提供标签参考,另有exe训练工具、bat一键脚本、txt说明及docx图文教程,整体仅16.46MB,便于快速下载。已有6863人浏览学习。借助docx手把手教程和可直接调用的opencv_traincascade等工具,使用者能较快掌握LBP、Haar级联训练及自定义物体实时识别方法,并理解正负样本制作、参数调节等难点,适合计算机视觉入门者或有OpenCV基础、需落地特定物体识别项目的开发者。
1. 用 Python 和 OpenCV 训练一个识别任意物体的检测器
很多人拿到物体识别需求,第一反应是上 YOLO、跑深度学习,但这个项目走的是另一条更轻的路:用 Python 和 OpenCV 自己搭一个“自定义训练器”。你只要给某个物体拍几十张照片,再收集一批背景图,就能训练出一个在摄像头画面里实时框出目标的检测器。它跑的是 HOG 特征 + SVM 分类器 + 滑动窗口这套经典视觉管线,不依赖 GPU,普通笔记本就能跑,训练一次一分钟上下。适合两类人:样本量少、喂不动深度模型,但又想快速验证物体可检测性的开发者;以及刚接触 Opencv 物体检测、想搞懂完整流程的学习者。
2. 项目全景:为什么 OpenCV + HOG + SVM 这套组合够用
2.1 技术选型:传统视觉方案在自定义物体识别上的优势
这个项目的核心不是算法多先进,而是流程完整。OpenCV 提供图像处理、特征提取、机器学习推理一整套能力,Python 绑定让我们直接用 cv2 读帧、预处理、算描述子。HOG(Histogram of Oriented Gradients,方向梯度直方图)统计图像局部区域的梯度方向分布,对轮廓、纹理、边缘变化特别敏感,行人检测经典方案 HOG + SVM 走的就是这条路。
为什么不用 YOLO 或 SSD?拆这个项目时最大的感受是,传统方案的启动成本低到让人意外。训练 YOLO 要打标注框、整理成 VOC 或 COCO 格式、调一堆训练参数,新手第一次跑通往往折腾一个周末。这个项目的标注方式是“背景里放目标拍照”,程序自动按尺寸裁剪正样本,负样本从不存在目标的背景图里随机裁,数据准备几十行代码就完成。加上 SVM 收敛速度快,几百个样本一秒内训练完,试错成本几乎为零。
它也有边界:HOG 对形变大的物体(猫、狗这种姿态多的)效果一般,对光线变化敏感。但如果目标是杯子、书、遥控器、瓶盖这类刚性物体,这个组合的精度在现实场景里完全能打。我的习惯是先拿这套验证可行性,确认目标区域稳定了,再决定要不要升级深度模型做精度优化。
2.2 目录结构与系统流程:一份可照抄的工程骨架
能“跑起来”的工程,目录结构很重要。这个项目拆成五个核心部分:数据采集、特征提取、模型训练、滑动窗口检测、摄像头实时推理。工程骨架长这样:
custom_object_detector/ ├── collect_samples.py # 打开摄像头,按空格保存正样本 ├── build_negatives.py # 从背景图里随机裁负样本 ├── extract_features.py # 遍历样本目录,提取 HOG 特征并保存 ├── train_svm.py # 训练 SVM 分类器,输出模型文件 ├── detect_image.py # 对图片做多尺度滑窗检测 ├── detect_camera.py # 摄像头实时检测 ├── hog_utils.py # HOG 参数定义、特征提取函数 ├── nms.py # 非极大值抑制 ├── models/ # 训练结果保存目录(svm_model.pkl) ├── data/ │ ├── positive/ # 目标物体裁剪图 │ ├── negative/ # 负样本图 │ └── raw_background/ # 不含目标的背景图 └── samples/ # 测试用的图片和视频采集流程:collect_samples.py 对着目标拍照,程序把画面中央区域裁剪出来当正样本;背景图放进 raw_background,build_negatives.py 随机截取小图当负样本;extract_features.py 把每个样本转成固定长度特征向量;train_svm.py 拟合分类器;最后 detect_image.py / detect_camera.py 加载模型,对输入图像做多尺度滑动窗口扫描,窗口算一次 HOG 特征丢给 SVM 打分,高于阈值输出候选框。
最容易被忽视的点:整个管线里图像尺寸是强约束。训练时 HOGDescriptor 的 winSize 决定特征维数,检测时滑动窗口大小必须和 winSize 完全相同,否则特征向量维度对不上直接抛异常。所以工程里统一把目标图片先缩放到 64×64,所有环节都拿这个尺寸说话,避免维度不一致的坑。
3. 核心模块拆解:从样本到可用的检测模型
3.1 数据采集与预处理:正样本、负样本和难例
这个项目的起点不是代码,是数据。正样本收集 100 到 300 张就够,太多反而过拟合到特定背景。采集时开摄像头对准物体,每按一次空格保存当前帧,程序从中央裁剪固定尺寸(64×64)区域。关键技巧:别只拍同一个角度,旋转一点、离远一点、换个光照、放不同桌面,让样本覆盖更多变化,泛化能力会好很多。
import cv2 SAMPLE_SIZE = 64 # 正样本统一裁剪到 64x64 CROP_RATIO = 0.6 # 裁剪中央 60% 区域 cap = cv2.VideoCapture(0) print("按空格键保存当前目标,按 Q 退出") count = 0 while True: ret, frame = cap.read() if not ret: break h, w = frame.shape[:2] cx, cy = w // 2, h // 2 crop_w, crop_h = int(w * CROP_RATIO), int(h * CROP_RATIO) x0, y0 = cx - crop_w // 2, cy - crop_h // 2 crop = frame[y0:y0 + crop_h, x0:x0 + crop_w] preview = cv2.resize(crop, (SAMPLE_SIZE, SAMPLE_SIZE)) cv2.imshow("collect", preview) key = cv2.waitKey(30) & 0xFF if key == ord(' '): cv2.imwrite(f"data/positive/pos_{count:04d}.jpg", preview) count += 1 elif key == ord('q'): break cap.release() cv2.destroyAllWindows()这里裁剪区域取画面中央 60% 区域,采集时把目标物体放屏幕中心就好。CROP_RATIO 可调:物体占画面比例越大,值应越大;反之调小。保存的正样本已统一成 64×64,后续特征提取不需要再处理尺寸。负样本生成思路是“从背景图里随机挖”,一张大背景图能挖出几十张小图:
import cv2, glob, random, os neg_size = 48 # 负样本尺寸 raw_images = glob.glob("data/raw_background/*.jpg") os.makedirs("data/negative", exist_ok=True) for idx, img_path in enumerate(raw_images): img = cv2.imread(img_path) h, w = img.shape[:2] for k in range(20): # 每张背景图随机裁 20 次 rh = random.randint(neg_size, h - neg_size) rw = random.randint(neg_size, w - neg_size) x = random.randint(0, w - rw) y = random.randint(0, h - rh) neg = img[y:y+rh, x:x+rw] neg = cv2.resize(neg, (64, 64)) cv2.imwrite(f"data/negative/neg_{idx:04d}_{k:02d}.jpg", neg)负样本数量和多样性直接决定误检率。一开始用 50 张背景图、每张裁 10 张,结果检测器在摄像头画面里到处乱框,把桌面纹理都当成目标。后来把负样本提到 400 张,并加入“难例”——第一次训练后误检出的图片,重新放进负样本集再训一轮,误检立刻压下去了。这个过程叫难负样本挖掘(Hard Negative Mining),是这个项目里性价比最高的一步。
3.2 HOG 特征提取:让分类器看到物体的形状纹理
HOG 的核心思想可以概括成一句话:图像的局部形状,能被“梯度方向的统计分布”描述。一个圆形杯子,边缘在每个方向都有像素;一支笔,边缘几乎全集中在两个方向。HOG 把图片切成小的 cell(细胞单元),每个 cell 里的像素算梯度方向,统计成方向直方图,再把相邻几个 cell 组成 block 做归一化,最后把所有 block 的特征串联成一个长向量。
OpenCV 里 HOGDescriptor 的参数比较玄学,不同组合效果差很多。调下来比较稳的一组是:winSize 64×64,blockSize 16×16,blockStride 8×8,cellSize 8×8,nbins 9。这样一张 64×64 的图被分成 8×8 个 cell,每 2×2 个 cell 成一个 block,block 数量是 7×7=49 个,每个 block 特征维数是 2×2×9=36,总特征是 49×36=1764 维。这个维度对 SVM 来说很小,训练速度飞快。
import cv2 def get_hog_descriptor(): win_size = (64, 64) block_size = (16, 16) block_stride = (8, 8) cell_size = (8, 8) n_bins = 9 hog = cv2.HOGDescriptor( _winSize=win_size, _blockSize=block_size, _blockStride=block_stride, _cellSize=cell_size, _nbins=n_bins, ) return hog def compute_features(hog, image_path): img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) img = cv2.resize(img, (64, 64)) # normalize:对光照变化做一次直方图均衡化 img = cv2.equalizeHist(img) # HOGDescriptor 的 compute 方法返回一维 float32 数组 return hog.compute(img)这段代码有两个细节值得说。第一,读图用 IMREAD_GRAYSCALE,HOG 不依赖颜色信息,转灰度省掉三分之二计算量。第二,compute_features 里加了 equalizeHist,这会在检测阶段造成一个常见陷阱——检测图片时也要做同样的均衡化,否则特征分布不一致,SVM 打分就会偏离训练时分布。踩过这个坑,一开始训练和检测差了这一步,检测率直线下降,所以把 normalization 写进特征提取函数里,保证全链路一致。
参数表里 winSize 必须和训练时完全一致。检测时想用更大窗口(比如 96×96),训练样本也必须统一缩放到 96×96,重新提取特征训练。HOG 特征维数是 winSize、bins、block 参数的函数,改任何一个,特征维度都会变,模型文件不能复用。
3.3 SVM 训练与模型持久化:把学习结果存成文件
SVM(支持向量机)在这个项目里扮演二分类器角色:给一个特征向量,判断“是目标”还是“不是目标”。样本量几百个时,用线性核就够了,非线性核容易过拟合,推理速度也慢。通常用 scikit-learn 的 LinearSVC,接口比 OpenCV 自带的 cv2.ml.SVM 更容易配合 numpy 处理数据。
import numpy as np import cv2 import pickle from sklearn.svm import LinearSVC from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score hog = get_hog_descriptor() X, y = [], [] # 正样本标签为 1,负样本标签为 0 for path in glob.glob("data/positive/*.jpg"): X.append(hog.compute(cv2.equalizeHist(cv2.resize(cv2.imread(path, 0), (64, 64)))).flatten()) y.append(1) for path in glob.glob("data/negative/*.jpg"): X.append(hog.compute(cv2.equalizeHist(cv2.resize(cv2.imread(path, 0), (64, 64)))).flatten()) y.append(0) X = np.array(X, dtype=np.float32) y = np.array(y, dtype=np.int32) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) model = LinearSVC(C=1.0, max_iter=5000) model.fit(X_train, y_train) y_pred = model.predict(X_test) print("accuracy:", accuracy_score(y_test, y_pred)) # 保存模型与 HOG 参数一起序列化 with open("models/svm_model.pkl", "wb") as f: pickle.dump({"hog_params": { "win_size": (64, 64), "block_size": (16, 16), "block_stride": (8, 8), "cell_size": (8, 8), "n_bins": 9}, "clf": model}, f)训练完成后,模型序列化成 PICKLE 文件,里面有分类器和 HOG 参数。保存 HOG 参数是后来加的习惯,检测时要用参数重新构造 HOGDescriptor,如果只存 SVM 权重不存参数,换台电脑就猜不出当时配置。LinearSVC 的 C 是正则化强度,默认 1.0 够用;max_iter 设 5000,因为 HOG 特征和分类器收敛通常用不了这么多轮,设大避免迭代次数不足的警告。训练完先看测试集 accuracy,低于 95% 说明样本质量或数量有问题,别急着部署,回去补数据比调参更有效。
4. 把模型跑起来:滑动窗口检测与摄像头实时识别
4.1 多尺度滑窗扫描:检测任意位置不同大小的目标
训练好的 SVM 只能对“一张 64×64 的图”判断是不是目标,现实里目标大小和位置都是未知的。检测阶段经典做法是“多尺度滑动窗口扫描”:在图像金字塔的每一层,用 64×64 窗口逐行逐列滑动,每滑到一个位置,窗口内图像经过缩放、灰度化、均衡化、提取 HOG、SVM 打分,分数超阈值就记为候选框。
def detect(image, hog, model, stride=8, scale=1.1, threshold=0.3): gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) boxes, scores = [], [] h, w = gray.shape[:2] # 图像金字塔:逐层缩小,直到小于窗口尺寸 scale_factor = 1.0 while h / scale_factor >= 64 and w / scale_factor >= 64: resized = cv2.resize(gray, (int(w / scale_factor), int(h / scale_factor))) resized = cv2.equalizeHist(resized) r_h, r_w = resized.shape[:2] for y in range(0, r_h - 64, stride): for x in range(0, r_w - 64, stride): patch = resized[y:y + 64, x:x + 64] feat = hog.compute(patch).reshape(1, -1) score = model.decision_function(feat)[0] if score > threshold: # 把缩放层上的坐标映射回原图 boxes.append([int(x * scale_factor), int(y * scale_factor), int(64 * scale_factor), int(64 * scale_factor)]) scores.append(float(score)) scale_factor *= scale return boxes, scores这段代码是整个检测器核心,可调参数三个:stride、scale、threshold。stride 是窗口滑动步长,越小扫描越密,越不容易漏掉目标,但耗时成倍增加,8 像素是常用平衡点。scale 是金字塔每层缩小比例,1.1 表示每层缩小 10%,值越接近 1.0 层数越多、检测越精细;目标大小跨度大用 1.05,否则 1.1 到 1.2 都行。threshold 是 SVM 决策阈值,LinearSVC 的 decision_function 输出到超平面的距离,正数代表预测为正样本,阈值抬到 0.3 过滤模棱两可的窗口。
实际跑这段代码会发现,目标稍微大一点窗口数非常多,一张 640×480 的图,缩放 4 层,每层几十个窗口,总数轻松超过两千。每个窗口算一次 HOG 特征,单帧耗时可能几百毫秒到一两秒。优化思路直接:既然目标尺寸范围大致可知,把金字塔层数限制在目标最小和最大尺寸之间,别一直缩放到 64×64;另一个思路是先用便宜的预处理(颜色直方图)快速滤掉大片背景窗口,再对剩下的做 HOG + SVM,对纯色桌面这种场景特别有效。
4.2 非极大值抑制:合并重叠框,输出干净的检测结果
滑动窗口天然产生大量彼此重叠的候选框:同一个目标往往被相邻几个窗口同时命中,如果不处理,画面里会框出好几层矩形。非极大值抑制(Non-Maximum Suppression,NMS)就是干这个的——在候选框集合里保留分数最高的框,把和它重叠度超过阈值的框删掉,迭代处理剩下的框。
def nms(boxes, scores, iou_threshold=0.4): if len(boxes) == 0: return [], [] boxes = np.array(boxes, dtype=np.float32) scores = np.array(scores, dtype=np.float32) idxs = np.argsort(scores)[::-1] # 按分数从高到低 keep = [] while idxs.size > 0: i = idxs[0] keep.append(i) if idxs.size == 1: break rest = idxs[1:] # 计算当前框与其他框的 IoU x1 = np.maximum(boxes[i, 0], boxes[rest, 0]) y1 = np.maximum(boxes[i, 1], boxes[rest, 1]) x2 = np.minimum(boxes[i, 0] + boxes[i, 2], boxes[rest, 0] + boxes[rest, 2]) y2 = np.minimum(boxes[i, 1] + boxes[i, 3], boxes[rest, 1] + boxes[rest, 3]) inter = np.maximum(0, x2 - x1) * np.maximum(0, y2 - y1) area_i = boxes[i, 2] * boxes[i, 3] area_r = boxes[rest, 2] * boxes[rest, 3] iou = inter / (area_i + area_r - inter + 1e-6) idxs = rest[iou <= iou_threshold] # 只留下重叠度低的 return boxes[keep].astype(int).tolist(), scores[keep].tolist()NMS 里最容易翻车的是 iou_threshold。设太大(0.6),重叠框删不干净,同一目标输出两个框;设太小(0.2),相邻的两个真实目标被误删。个人习惯先设 0.4,再根据实际效果微调。计算 IoU 时加 1e-6 防止除零,属于防御性编程。NMS 在目标检测里几乎是标配,后续切换深度学习模型也会发现 YOLO、SSD 输出后面都要接 NMS,只是底层实现可能换成 OpenCV 的 cv2.dnn.NMSBoxes。
过滤完还有个细节:输出的 boxes 坐标是原图坐标,画框直接用 cv2.rectangle 就行。但如果输入是摄像头帧而窗口是从金字塔某层映射回来的,一定要保留 scale_factor 的换算关系,否则框会偏到目标左上角。习惯把金字塔的 scale 和框坐标放在同一个数据结构里传下去,避免模块间坐标空间混乱。
4.3 摄像头实时识别:从 VideoCapture 到逐帧推理
把上面的检测和 NMS 串起来,就是完整的实时检测程序。摄像头读取用 VideoCapture,参数 0 代表默认摄像头。为了让画面不太卡,可以在每帧之间做跳帧:每 3 帧检测一次,中间帧直接复用上一帧的检测结果,这样丢帧少、体感流畅。
import cv2, pickle import numpy as np from hog_utils import get_hog_descriptor from nms import nms # 从模型文件恢复参数并构造 HOG with open("models/svm_model.pkl", "rb") as f: data = pickle.load(f) p = data["hog_params"] hog = cv2.HOGDescriptor(_winSize=p["win_size"], _blockSize=p["block_size"], _blockStride=p["block_stride"], _cellSize=p["cell_size"], _nbins=p["n_bins"]) model = data["clf"] cap = cv2.VideoCapture(0) frame_count = 0 while True: ret, frame = cap.read() if not ret: break frame_count += 1 if frame_count % 3 == 1: boxes, scores = detect(frame, hog, model, stride=6, scale=1.15, threshold=0.2) boxes, scores = nms(boxes, scores, iou_threshold=0.4) cache = (boxes, scores) else: boxes, scores = cache for box, score in zip(boxes, scores): x, y, w, h = box cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) cv2.putText(frame, f"{score:.2f}", (x, y - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow("detect", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()实时识别阶段性能瓶颈基本在 detect 函数。普通办公本建议把检测帧间隔从 3 调到 5,或者把输入图像缩小一半再检测,检测框坐标缩放回原图即可。另一个常见做法是设置“最小检测尺寸”,比如只检测面积大于 40×40 的目标,更小的窗口直接跳过,因为小窗口误检率本来就高。这些参数放在程序顶部用常量定义,方便不同场景快速实验。
值得一提的还有阈值。detect 里 threshold 设 0.2,比训练评估时低一些,目的是让误检多一点而不是漏检多一点,交给 NMS 和帧间平滑过滤。实际调优要记住:漏检(False Negative)和误检(False Positive)是一对矛盾。阈值下调,检测框变多,误检变多;阈值上调,检测框变少,漏检变多。没有一个万能默认值,得在真实画面里来回试,这也是检测项目躲不开的“玄学”环节。
5. 避坑指南:训练和部署中的常见问题排查
5.1 模型加载时报错“特征维度不匹配”
现象:训练完保存的模型,在 detect_image 或 detect_camera 里一跑就报错,错误信息通常是 dimension mismatch 或 assertion failed。
原因:几乎都是因为检测时用的 HOGDescriptor 参数和训练时不一致。比如训练时 winSize 是 64×64,检测时临时改到 80×80,特征维数变了,SVM 拿到的向量长度对不上。另一种情况是训练时样本做过灰度化和等比例缩放,检测时忘了做,输入分布不一致。
解决:把 HOG 参数和模型一起序列化保存,像 3.3 节代码那样,检测端从 pickle 里读出参数再构造 HOGDescriptor。以后换机器、换环境都从模型文件取配置,不再手写参数。如果要改窗口尺寸,必须回到训练阶段把样本统一成新尺寸,重新提取特征训练,模型文件和特征参数是强绑定的。
5.2 误检率居高不下,桌面、阴影都被框进去
现象:模型训练完 accuracy 很高,但真实场景里摄像头画面到处是检测框,桌面纹理、墙上影子、纯色区域都被识别成目标。
原因:负样本不够多样。训练时只用了几张固定背景图,SVM 把“这些背景的纹理特征”当成负样本的边界,新场景一出现,模型没见过的背景特征都被判断成目标。这是 HOG + SVM 路线最常见的翻车点。
解决:加大负样本规模,引入难负样本挖掘。第一次训练完,用检测器在真实画面里跑一遍,把误检框所在的区域截图存进 negative 文件夹,重新训练。这个流程迭代两三轮,误检率会肉眼可见下降。另一个容易忽略的点:正负样本不是单纯比数量,正样本要保证“目标在画面中占的比例和检测时一致”,如果训练时目标占画面 80%,检测时只占 10%,SVM 很难泛化。
5.3 检测速度只有一两帧每秒,卡得没法用
现象:摄像头画面延迟明显,帧率经常掉到 5 以下,CPU 占用接近 80%。
原因:滑动窗口的窗口数量太大。一张 640×480 的图,金字塔 4 层,每层滑动步长 6 像素,候选窗口轻松上万。每个窗口都要 compute 一次 HOG,而 HOG 特征计算是个相对重的操作,上万次算下来单帧耗时超过一两秒很正常。
解决:三层依次做。第一,控制金字塔层数,只扫描目标实际可能出现的尺寸范围,比如目标普遍大于 48×48、小于 128×128,就在这个区间内缩放,避免扫描无意义的极小窗口。第二,增大滑动步长,从 4 改成 8,检测框少一半,速度提升接近一倍,配合 NMS 效果差距不大。第三,降低输入图像分辨率,把帧先缩到 320×240 再检测,性价比最高的优化。这些还不够,考虑传统方案的天花板,把检测频率降下来,每 5 帧只检测一次,中间帧用上一次结果,视觉上几乎无感。
5.4 换了个光照场景就漏检,白天好用晚上就废
现象:白天在窗边表现良好,晚上开灯或换个房间,同一个物体检测率骤降,大量目标漏检。
原因:HOG 特征本身包含梯度信息,对光照变化有一定抵抗力,但对剧烈变化(明到暗)依然敏感。更关键的是,训练样本如果在同一个光源下拍,SVM 的决策边界容易依赖特定光照下的梯度分布。我的实际经验是,漏检的主要原因往往不是模型不好,而是训练样本和部署场景的光照差异过大。
解决:采集正样本时强制增加光照变化。不同时间段、房间不同灯光状态、把物体放到明暗交界处各拍一批。预处理里可加一步高斯模糊或对比度调整,让特征对噪声不那么敏感。还有一个屡试不爽的技巧:检测前对整帧做 CLAHE(Contrast Limited Adaptive Histogram Equalization,限制对比度的自适应直方图均衡化),它比全局 equalizeHist 对局部光照更鲁棒,阴影边缘不会放大噪声,漏检率会明显下降。凡是预处理在训练和检测两头都要保持一致,改了预处理必须重新提取特征训练。
5.5 训练准确率很高但检测框位置偏
现象:accuracy 95% 以上,但检测出的框总是不贴边,要么偏左上、要么把目标腰斩,看起来“框住了但没完全框住”。
原因:训练时正样本是“目标居中裁剪”,而滑动窗口在检测时是“从左上角扫到右下角”,窗口命中位置不总是正好套住目标整体。SVM 打分最高的窗口可能只是命中了目标某个局部特征,比如瓶子的高光区域。NMS 选出的分数最高的框如果本身位置偏,重叠度高的正确框也会被它“压住”。
解决:最直接的办法是训练时做一次数据增强:把正样本的物体稍微左右上下平移几个像素再保存,让模型见过“物体不在正中心”的情况。另外可以在 NMS 之前加后处理:把显著偏小的候选框过滤掉,只保留面积占比大约等于目标真实大小的框。最后一种做法是多个相邻窗口坐标做加权平均,权重用 SVM 分数,利用周围窗口信息把框位置“拉正”到目标中央。这个手段工程里很好使,代价只是几行代码。
6. 二次开发:从单类别检测到多类别扩展的思路
这个项目改造成多类别,核心不是代码,是数据结构。最简单的做法是把标签从 0/1 改成 0/1/2/3,把 LinearSVC 换成 sklearn 的 one-vs-rest 方案,直接上sklearn.multiclass.OneVsRestClassifier(LinearSVC())。要注意的是多类别下每个类要单独一批正样本,负样本可以共享。检测阶段扫完所有窗口后,每个窗口得到多个类别分数,取最高分且超过该类阈值的才输出。这个改动对现有架构是透明的,NMS 要在类别内做,不然不同类别的重叠框相互抑制会误删。
不想局限在传统特征上,这个项目预留的升级路径是替换特征提取器。OpenCV 的 dnn 模块可以直接加载 MobileNet-SSD 或 YOLOv4-tiny 的 ONNX 模型,改动点只在特征提取那一层:把 hog.compute 换成cv2.dnn.blobFromImage+net.forward(),后面的 SVM、NMS 逻辑大部分保留,滑窗换成网络本身的多尺度输出。代价是准备带标注框的数据集,训练成本上一个台阶,但对形变物体的适应能力是质的提升。建议按需升级:目标刚性、背景稳定、样本量小,HOG + SVM 足够;规模上去了、目标形态复杂了,再迁深度学习。
再有一个反复强调的细节:所有预处理逻辑必须集中在同一个工具函数里。我的做法是把尺寸缩放、灰度化、均衡化、特征提取写进 hog_utils.py,训练脚本和检测脚本都 import 同一个函数,避免两边各写一份导致特征分布漂移。多类别版本里这尤其重要——六个类别的样本如果经过不同预处理通道,整个模型就是黑匣子,出问题根本没法排查。从那以后每次做检测类项目,都强制走一遍“训练端和推理端共用同一个特征提取函数”的验证流程,确认两端中间特征值一致了才进入调参,这个习惯帮我省掉太多返工时间。希望帮到你。
本文还有配套的精品资源,点击获取