跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
PythonAI算法

基于 HOG 特征与 SVM 的目标检测实现及 Python 代码

详细阐述了基于 HOG 特征与 SVM 分类器的目标检测原理及实现流程。内容包括图像预处理、梯度计算、细胞单元直方图构建及块归一化等 HOG 特征提取步骤,以及 SVM 二分类训练与滑动窗口检测机制。文中提供了基于 Pascal VOC 数据集的数据解析、模型训练及测试的完整 Python 代码示例,并包含非极大值抑制(NMS)处理重复框的方法。

蜜桃汽水发布于 2026/3/26更新于 2026/9/93.3K 浏览
基于 HOG 特征与 SVM 的目标检测实现及 Python 代码

HOG 特征提取+SVM 分类器

1、HOG 特征提取过程

步骤 1:预处理图像(标准化,提升鲁棒性) ① 转灰度图:丢弃颜色信息(颜色对目标检测无核心帮助,还会增加计算量)。 ② Gamma 校正(可选):对图像做灰度值非线性变换,抑制光照变化的影响(比如过亮、过暗区域的细节保留)。 ③ 裁剪或缩放:将图像块调整为 winSize(64x64)尺寸(对应训练/检测的窗口大小),保证特征提取的一致性。

步骤 2:计算图像梯度(提取边缘/纹理信息,梯度能反映图像的边缘、纹理变化,目标的轮廓就是强梯度区域,这是 HOG 特征的核心来源) ① 对每个像素点,分别计算水平梯度(x 方向)和垂直梯度(y 方向)(常用 Sobel 算子或简单差分法)。 ② 由每个像素的 x、y 梯度,计算两个关键值:

  • 梯度幅值:反映梯度的强度(边缘的清晰程度)。
  • 梯度方向:反映边缘的朝向(范围 0-180° 或 0-360°) 最终得到两张和原图尺寸一致的「梯度幅值图」和「梯度方向图」。

步骤 3:细胞单元(cellSize)构建梯度直方图(量化特征) ① 将 winSize 梯度幅度图和梯度方向图按 cellSize 分别划分为多个独立细胞单元; ② 对每个细胞单元内的所有像素,构建直方图(区间个数 nbins);其中梯度方向决定哪个区间计数,梯度幅值决定计数的权重。 ③ 每个细胞单元最终得到一个长度为 nbins 的一维数组,这是最基础的局部特征,反映该区域的纹理方向分布。

步骤 4:将相邻的多个细胞单元组合成一个块(Block),Block 内特征归一化。 块的滑动步长为 blockStride,在 winSize 窗口内滑动,对每个块内的所有细胞直方图进行归一化处理(常用 L2 归一化),消除光照、对比度变化带来的特征偏差(比如同一目标在亮处和暗处的梯度幅值差异)。

步骤 5:拼接所有块特征,得到一位数组即最终 HOG 特征向量。

如果不打算细究具体的实现算法,我理解 Hog 特征就是基于图像的梯度与局部梯度的直方图经过一系列计算的新向量化表示。

2、SVM 分类器判断 下文详解

3、非极大值抑制(NMS) 非极大值抑制(NMS)的唯一核心作用是去除目标检测过程中产生的大量重复、重叠的候选检测框,只保留置信度最高、最具代表性的那个框;属于目标检测的推理/检测阶段。

4、绘制边框,得到检测结果

使用二分类模型实现目标检测原理:

SVM 本身确实只是二分类模型,它完全没有能力直接预测目标的坐标值,最终能得到检测框坐标,是「HOG + 滑动窗口 + 多尺度缩放」这套组合流程的功劳,SVM 只在其中承担「分类判断」的单一角色。

第一步:SVM 的训练阶段(只做二分类,和坐标无关) 训练时,只给 SVM 喂两种数据,让它学习「是目标」和「不是目标」的区分:

  • 正样本:裁剪好的「仅包含目标」的图像块(尺寸和 HOG 的 winSize 一致,比如 64x64),提取 HOG 特征后,标注为「1」(是目标)。
  • 负样本:不包含目标的图像块(尺寸同样 64x64),提取 HOG 特征后,标注为「-1」(不是目标)。 训练完成后,这个 SVM 只有一个能力:输入一个 64x64 图像块的 HOG 特征,返回一个分类结果(是目标/不是目标),附带一个置信度得分,它完全不知道这个图像块来自整张图片的哪个位置。

第二步:检测阶段(滑动窗口 + 多尺度,让 SVM'间接'产出坐标) 检测时,坐标不是 SVM 预测的,而是「滑动窗口」遍历图像时的「窗口位置」决定的,流程如下:

固定尺寸滑动窗口(遍历单张图) 以 HOG 的 winSize(64x64)为窗口大小,以一定步长(比如 16 像素),在整张输入图片上「从左到右、从上到下」滑动。每滑动到一个位置,就裁剪出这个窗口对应的 64x64 图像块,提取 HOG 特征。把这个 HOG 特征输入给训练好的 SVM,让 SVM 做二分类判断:「这个窗口里是目标吗?」。如果 SVM 判断「是目标」(置信度得分较高),就记录下「这个窗口在整张图上的左上角坐标 + 窗口尺寸(64x64)」,这就是最初的检测框坐标(x, y, w, h)。

多尺度缩放(解决不同大小的目标) 因为目标在图片中可能有大有小(比如近处的人更大,远处的人更小),固定 64x64 窗口无法检测到所有尺寸的目标。所以会先把输入图片按不同比例缩放(比如缩小为 0.8 倍、0.6 倍…),形成一组「多尺度图像金字塔」。对每一张缩放后的图像,都执行上面的「滑动窗口 + SVM 分类」流程,收集所有 SVM 判断为「是目标」的窗口位置。最终坐标整合所有尺度下的窗口,都会映射回原始图片的尺寸(还原缩放带来的坐标偏差),形成最终的 locations 检测框列表。hog.detectMultiScale() 其实已经封装了「滑动窗口 + 多尺度缩放 + SVM 分类 + 坐标还原」这一系列操作,所以直接返回了可用的检测框坐标。

注意:

  1. HOG 参数:HOG 有多个参数,包括窗口大小、块(block)大小、单元格(cell)大小等。这些参数同时影响边界框的尺寸和宽高比。如果候选框的尺寸不同,可能需要进行尺寸调整。
  2. 旋转敏感性:HOG 对图像旋转非常敏感,因此如果图像发生倾斜,提取的特征向量可能不适用于目标检测。
  3. 同一对象的差异性:即便是同一个目标,不同的边界框也可能生成不同的 HOG 向量。因此,需要借助机器学习模型判断目标是否被正确检测到。

数据集-VOC

<!-- 典型 Pascal VOC 标注文件结构 -->
<annotation>
    <folder>VOC2012</folder>
    <filename>2007_000027.jpg</filename><!-- ← 从这里提取 -->
    <size>
        <width>486</width>
        <height>500</height>
        <depth>3</depth>
    </size>
    <object><!-- ← 每个 object 标签对应一个目标 -->
        <name>person</name><!-- ← 目标类别 -->
        <bndbox><!-- ← 边界框坐标(关键!) -->
            <xmin>174</xmin><!-- ← 提取 -->
            <ymin>101</ymin><!-- ← 提取 -->
            <xmax>349</xmax><!-- ← 提取 -->
            <ymax>351</ymax><!-- ← 提取 -->
        </bndbox>
    </object>
    <object><!-- 可以有多个 object --></object>
</annotation>

提取关键信息

import xml.etree.ElementTree as ET

def read_voc_xml(xmlfile):
    root = ET.parse(xmlfile).getroot()
    boxes = {"filename": root.find("filename").text, "objects": []}
    for box in root.iter('object'):
        bb = box.find('bndbox')
        obj = {
            "name": box.find('name').text,
            "xmin": int(bb.find("xmin").text),
            "ymin": int(bb.find("ymin").text),
            "xmax": int(bb.find("xmax").text),
            "ymax": int(bb.find("ymax").text),
        }
        boxes["objects"].append(obj)
    return boxes

Hog 特征提取与 SVM 训练

import xml.etree.ElementTree as ET
import glob
import cv2
import random
import sklearn.svm as svm
import pickle
import numpy as np
import time

def make_square(xmin, xmax, ymin, ymax):
    # 辅助函数示例,实际需根据需求实现确保正方形
    size = max(xmax - xmin, ymax - ymin)
    cx, cy = (xmin + xmax) // 2, (ymin + ymax) // 2
    return cx - size // 2, cx + size // 2, cy - size // 2, cy + size // 2

if __name__ == "__main__":
    data_path = 'F:\\ObjectDetection\\oxford-iiit-pet\\annotations\\xmls\\'
    data_xmls = glob.glob(data_path + '*.xml')
    img_path = 'F:\\ObjectDetection\\oxford-iiit-pet\\images\\'
    
    winSize = (64, 64)
    blockSize = (32, 32)
    blockStride = (16, 16)
    cellSize = (16, 16)
    nbins = 9
    num_samples = 10000
    positive = []  # 猫
    negative = []  # 其他动物

    for xml in data_xmls:
        result = read_voc_xml(xml)
        if not result['objects'] or result['objects'][0]['name'] != 'cat':
            continue
        object_data = result['objects'][0]
        img = cv2.imread(img_path + result['filename'])
        xmin, xmax, ymin, ymax = object_data["xmin"], object_data["xmax"], object_data["ymin"], object_data["ymax"]
        xmin, xmax, ymin, ymax = make_square(xmin, xmax, ymin, ymax)
        sample = img[ymin:ymax, xmin:xmax]
        sample = cv2.resize(sample, winSize)
        positive.append(sample)
        if len(positive) >= num_samples:
            print('--------- positive success ---------')
            break

    for xmlfile in data_xmls:
        result = read_voc_xml(xmlfile)
        if not result['objects'] or result["objects"][0]["name"] == "cat":
            continue
        object_data = result['objects'][0]
        img = cv2.imread(img_path + result['filename'])
        height, width = img.shape[:2]
        boxsize = random.randint(winSize[0], min(height, width))
        x = random.randint(0, width - boxsize)
        y = random.randint(0, height - boxsize)
        sample = img[y:y + boxsize, x:x + boxsize]
        sample = cv2.resize(sample, winSize)
        negative.append(sample)
        if len(negative) >= num_samples:
            print('--------- negative success ---------')
            break

    images = positive + negative
    labels = ([1] * len(positive) + [0] * len(negative))
    
    hog = cv2.HOGDescriptor(winSize, blockSize, blockStride, cellSize, nbins)
    data = []
    for img in images:
        features = hog.compute(img)
        data.append(features.flatten())
    data = np.array(data, dtype=np.float32)
    labels = np.array(labels, dtype=np.int32)
    
    print('--------- svm start ---------')
    ts = time.time()
    svm_model = cv2.ml.SVM_create()
    svm_model.setType(cv2.ml.SVM_C_SVC)  # 用于设置 SVM 的模型类型,决定 SVM 适用的任务场景,引入惩罚参数 C(后续可额外配置),允许少量样本违反间隔约束,用于解决线性不可分问题,提升模型的泛化能力。
    svm_model.setKernel(cv2.ml.SVM_RBF)  # 核函数的作用是将低维线性不可分的数据映射到高维空间,使其在高维空间中线性可分;径向基函数(Radial Basis Function),也称为高斯核,是最常用的非线性核函数
    svm_model.setTermCriteria((cv2.TERM_CRITERIA_MAX_ITER + cv2.TERM_CRITERIA_EPS, 100000, 1e-8))  # 设置 SVM 训练过程的终止条件;「最大迭代次数」或「精度收敛」任一条件即终止
    svm_model.train(data, cv2.ml.ROW_SAMPLE,  # 指定样本数据的组织格式,表示每一行对应一个训练样本,每一列对应一个特征(这是最常用的数据组织方式,符合大多数人的数据预处理习惯
                    labels)
    te = time.time() - ts
    
    # 保存模型
    svm_model.save('F:\\ObjectDetection\\save_model\\svm_model.yml')
    print(svm_model.getSupportVectors().shape)
    print('--------- svm end ---------')
    print('fit time: {}s'.format(te))

测试代码

import glob
import cv2
import xml.etree.ElementTree as ET

def read_voc_xml(xmlfile):
    root = ET.parse(xmlfile).getroot()
    boxes = {"filename": root.find("filename").text, "objects": []}
    for box in root.iter('object'):
        bb = box.find('bndbox')
        obj = {
            "name": box.find('name').text,
            "xmin": int(bb.find("xmin").text),
            "ymin": int(bb.find("ymin").text),
            "xmax": int(bb.find("xmax").text),
            "ymax": int(bb.find("ymax").text),
        }
        boxes["objects"].append(obj)
    return boxes

if __name__ == "__main__":
    data_path = 'F:\\ObjectDetection\\oxford-iiit-pet\\annotations\\xmls\\'
    data_xmls = glob.glob(data_path + '*.xml')
    img_path = 'F:\\ObjectDetection\\oxford-iiit-pet\\images\\'
    model_path = 'F:\\ObjectDetection\\save_model\\svm_model.yml'
    
    winSize = (64, 64)  # 检测从窗口大小,即每次提取 HOG 特征的图像块大小,训练的时候是将图像按照标注提取出目标区域 resize 到 winSize
    blockSize = (32, 32)  # 块大小,HOG 特征以块为单位进行归一化,减少光照等因素影响
    blockStride = (16, 16)  # 快步长,相邻块之间的偏移量,决定了块的覆盖密度
    cellSize = (16, 16)  # 细胞单元大小,每个细胞单元内计算梯度方向直方图
    nbins = 9  # 梯度方向直方图的分箱树,即把 0~180°(或 0~360°)梯度方向划分为 9 个区间,统计每个区间的梯度幅值
    
    svm_model = cv2.ml.SVM_load(model_path)
    hog = cv2.HOGDescriptor(winSize, blockSize, blockStride, cellSize, nbins)
    
    # 创建 HOG 特征描述符实例
    hog.setSVMDetector(svm_model.getSupportVectors()[0])  # SVM 模型的支持向量设置为 HOG 检测器的分类器,让 HOG 提取特征后可直接用该 SVM 进行目标分类/检测
    
    for data_xml in data_xmls[:5]:
        result = read_voc_xml(data_xml)
        img = cv2.imread(img_path + result['filename'])
        bbox = result['objects'][0]
        start_point = (bbox['xmin'], bbox['ymin'])  # 真实框左上角坐标
        end_point = (bbox['xmax'], bbox['ymax'])  # 真实框右下角坐标
        annotated_img = cv2.rectangle(img, start_point, end_point, (0, 0, 255))  # 绘制矩形框,参数依次是「图片」「左上角坐标」「右下角坐标」「颜色(BGR 格式,(0,0,255) 对应红色)」,默认线宽为 1
        
        locations, scores = hog.detectMultiScale(img)  # 用 HOG+SVM 对图片进行多尺度目标检测,自动缩放图片;locations:所有检测到的目标边界框坐标 (x, y, w, h) 左上坐标 + 宽高,scores:对应每个检测框的置信度得分,得分越高,检测结果越可靠。
        
        # ------------ 选择置信度最高的------------
        # x, y, w, h = locations[np.argmax(scores.flatten())]  # 选取检测得分最高的边界框(取 scores 最大值对应的位置)
        # cv2.rectangle(img, (x, y), (x+w, y+h), (0, 255, 0), 5)
        # cv2.imshow(f"{result['filename']}: {result['objects'][0]['name']}", annotated_img)
        # key = cv2.waitKey(0)
        # cv2.destroyAllWindows()
        # if key == ord('q'):
        #     break
        
        # ------------ 使用 NMS ------------
        boxes = locations.tolist()
        confidences = scores.flatten().tolist()
        indices = cv2.dnn.NMSBoxes(boxes, confidences, score_threshold=0.5,  # 信度阈值:过滤掉得分低于 0.5 的低质量框
                                   nms_threshold=0.3)  # IOU 阈值:重叠度超过 0.3 的框,保留得分最高的,去除其他
        
        if len(indices) > 0:
            # 兼容 OpenCV 返回格式,提取有效索引
            indices = indices.flatten()
            # 取第一个最优框(也可遍历所有筛选后的框)
            best_idx = indices[0]
            x, y, w, h = boxes[best_idx]
            # 绘制筛选后的检测框
            cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 5)
            cv2.imshow(f"{result['filename']}: {result['objects'][0]['name']}", annotated_img)
            key = cv2.waitKey(0)
            cv2.destroyAllWindows()
            if key == ord('q'):
                break

目录

  1. HOG 特征提取+SVM 分类器
  2. 使用二分类模型实现目标检测原理:
  3. 数据集-VOC
  4. 提取关键信息
  5. Hog 特征提取与 SVM 训练
  6. 测试代码

更多推荐文章

查看全部
  • MCP 插件配置实战:browser-tools-mcp 集成指南
  • 生产环境 Nginx 双机热备部署 - Keepalived 多模式配置
  • Claude Code 的模型配置与默认值说明
  • Flutter EWS 组件适配鸿蒙实战:Exchange 邮件日历同步方案
  • 《Agent Runtime 工程化》第十章 Eval Harness 与 CI 回归:导读
  • Python 从零构建 AI 多智能体系统:三方协作实现复杂任务
  • AI 体操视频暴露物理缺陷,LeCun:视频生成模型并不懂物理
  • DFS 算法详解:求解数组子集问题
  • 深度学习卷积神经网络(CNN)实战应用
  • RAG 技术如何精准应对大模型敏感问题的知识幻觉难题
  • MacBook 手动安装多版本 JDK(8 和 17)并配置灵活切换
  • AI Infra 基础架构:定义、核心组件与发展趋势
  • 基于 Docker 部署 Web-Check 结合内网穿透实现远程网站检测
  • Qwen3.5-4B 微调实战:LLaMA-Factory 打造医疗 AI 助手
  • 前端实现视频画中画功能及主小窗同步控制
  • GitHub 学生开发者包认证全流程指南
  • 智能在线考试系统设计与实现:AI 辅助开发实践
  • 豆包 AI 视频去水印方法及工具推荐
  • GitHub Copilot 学生认证申请教程及配置步骤
  • Llama-3.2V-11B-COT 模型视觉推理质量评估指南

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • RSA密钥对生成器

    生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online

  • Mermaid 预览与可视化编辑

    基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online

  • 随机西班牙地址生成器

    随机生成西班牙地址(支持马德里、加泰罗尼亚、安达卢西亚、瓦伦西亚筛选),支持数量快捷选择、显示全部与下载。 在线工具,随机西班牙地址生成器在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • curl 转代码

    解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online