HOG+SVM行人检测

前言

在前面的博客:HOG特征检测学习笔记中,介绍了HOG特征,也附有代码实现。这篇博客中将会使用HOG+SVM这一经典的目标检测算法来进行行人检测,但是不会讨论HOG或者SVM的理论部分,如果有不懂的请自行查阅以前的博客。我分别写了python版本和C++版本的demo,数据集是直接下载了别人的,这些都会附在文章的最后。
网上也有很多介绍HOG的不错的文章:

HOG+SVM行人检测的两种方法
目标检测的图像特征提取之(一)HOG特征
python+opencv3.4.0 实现HOG+SVM行人检测

程序实现

基于python和c++写的demo思想上都是一样的,无非就是从数据集读入正负样本,提取HOG特征,送入SVM训练。而检测时,则使用训练好的SVM来识别滑动窗口中的ROI,也可以设置多尺寸,即使用滑动窗口中的ROI的图像金字塔,对多尺寸图像进行检测。这些在OpenCV中都实现好了,我们就不重复造轮子了。

python版本demo

直接上代码:

# *_*coding:utf-8 *_*

import os
import sys
import cv2
import logging
import numpy as np

def logger_init():
    '''
    自定义python的日志信息打印配置
    :return logger: 日志信息打印模块
    '''

    # 获取logger实例,如果参数为空则返回root logger
    logger = logging.getLogger("PedestranDetect")

    # 指定logger输出格式
    formatter = logging.Formatter('%(asctime)s %(levelname)-8s: %(message)s')

    # 文件日志
    # file_handler = logging.FileHandler("test.log")
    # file_handler.setFormatter(formatter)  # 可以通过setFormatter指定输出格式

    # 控制台日志
    console_handler = logging.StreamHandler(sys.stdout)
    console_handler.formatter = formatter  # 也可以直接给formatter赋值

    # 为logger添加的日志处理器
    # logger.addHandler(file_handler)
    logger.addHandler(console_handler)

    # 指定日志的最低输出级别,默认为WARN级别
    logger.setLevel(logging.INFO)

    return logger

def load_data_set(logger):
    '''
    导入数据集
    :param logger: 日志信息打印模块
    :return pos: 正样本文件名的列表
    :return neg: 负样本文件名的列表
    :return test: 测试数据集文件名的列表。
    '''
    logger.info('Checking data path!')
    pwd = os.getcwd()
    logger.info('Current path is:{}'.format(pwd))

    # 提取正样本
    pos_dir = os.path.join(pwd, 'Positive')
    if os.path.exists(pos_dir):
        logger.info('Positive data path is:{}'.format(pos_dir))
        pos = os.listdir(pos_dir)
        logger.info('Positive samples number:{}'.format(len(pos)))

    # 提取负样本
    neg_dir = os.path.join(pwd, 'Negative')
    if os.path.exists(neg_dir):
        logger.info('Negative data path is:{}'.format(neg_dir))
        neg = os.listdir(neg_dir)
        logger.info('Negative samples number:{}'.format(len(neg)))

    # 提取测试集
    test_dir = os.path.join(pwd, 'TestData')
    if os.path.exists(test_dir):
        logger.info('Test data path is:{}'.format(test_dir))
        test = os.listdir(test_dir)
        logger.info('Test samples number:{}'.format(len(test)))

    return pos, neg, test

def load_train_samples(pos, neg):
    '''
    合并正样本pos和负样本pos,创建训练数据集和对应的标签集
    :param pos: 正样本文件名列表
    :param neg: 负样本文件名列表
    :return samples: 合并后的训练样本文件名列表
    :return labels: 对应训练样本的标签列表
    '''
    pwd = os.getcwd()
    pos_dir = os.path.join(pwd, 'Positive')
    neg_dir = os.path.join(pwd, 'Negative')

    samples = []
    labels = []
    for f in pos:
        file_path = os.path.join(pos_dir, f)
        if os.path.exists(file_path):
            samples.append(file_path)
            labels.append(1.)

    for f in neg:
        file_path = os.path.join(neg_dir, f)
        if os.path.exists(file_path):
            samples.append(file_path)
            labels.append(-1.)

    # labels 要转换成numpy数组,类型为np.int32
    labels = np.int32(labels)
    labels_len = len(pos) + len(neg)
    labels = np.resize(labels, (labels_len, 1))

    return samples, labels

def extract_hog(samples, logger):
    '''
    从训练数据集中提取HOG特征,并返回
    :param samples: 训练数据集
    :param logger: 日志信息打印模块
    :return train: 从训练数据集中提取的HOG特征
    '''
    train = []
    logger.info('Extracting HOG Descriptors...')
    num = 0.
    total = len(samples)
    for f in samples:
        num += 1.
        logger.info('Processing {} {:2.1f}%'.format(f, num/total*100))
        hog = cv2.HOGDescriptor((64,128), (16,16), (8,8), (8,8), 9)
        # hog = cv2.HOGDescriptor()
        img = cv2.imread(f, -1)
        img = cv2.resize(img, (64,128))
        descriptors = hog.compute(img)
        logger.info('hog feature descriptor size: {}'.format(descriptors.shape))    # (3780, 1)
        train.append(descriptors)

    train = np.float32(train)
    train = np.resize(train, (total, 3780))

    return train

def get_svm_detector(svm):
    '''
    导出可以用于cv2.HOGDescriptor()的SVM检测器,实质上是训练好的SVM的支持向量和rho参数组成的列表
    :param svm: 训练好的SVM分类器
    :return: SVM的支持向量和rho参数组成的列表,可用作cv2.HOGDescriptor()的SVM检测器
    '''
    sv = svm.getSupportVectors()
    rho, _, _ = svm.getDecisionFunction(0)
    sv = np.transpose(sv)
    return np.append(sv, [[-rho]], 0)

def train_svm(train, labels, logger):
    '''
    训练SVM分类器
    :param train: 训练数据集
    :param labels: 对应训练集的标签
    :param logger: 日志信息打印模块
    :return: SVM检测器(注意:opencv的hogdescriptor中的svm不能直接用opencv的svm模型,而是要导出对应格式的数组)
    '''
    logger.info('Configuring SVM classifier.')
    svm = cv2.ml.SVM_create()
    svm.setCoef0(0.0)
    svm.setDegree(3)
    criteria = (cv2.TERM_CRITERIA_MAX_ITER + cv2.TERM_CRITERIA_EPS, 1000, 1e-3)
    svm.setTermCriteria(criteria)
    svm.setGamma(0)
    svm.setKernel(cv2.ml.SVM_LINEAR)
    svm.setNu(0.5)
    svm.setP(0.1)  # for EPSILON_SVR, epsilon in loss function?
    svm.setC(0.01)  # From paper, soft classifier
    svm.setType(cv2.ml.SVM_EPS_SVR)

    logger.info('Starting training svm.')
    svm.train(train, cv2.ml.ROW_SAMPLE, labels)
    logger.info('Training done.')

    pwd = os.getcwd()
    model_path = os.path.join(pwd, 'svm.xml')
    svm.save(model_path)
    logger.info('Trained SVM classifier is saved as: {}'.format(model_path))

    return get_svm_detector(svm)

def test_hog_detect(test, svm_detector, logger):
    '''
    导入测试集,测试结果
    :param test: 测试数据集
    :param svm_detector: 用于HOGDescriptor的SVM检测器
    :param logger: 日志信息打印模块
    :return: 无
    '''
    hog = cv2.HOGDescriptor()
    hog.setSVMDetector(svm_detector)
    # opencv自带的训练好了的分类器
    # hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector())
    pwd = os.getcwd()
    test_dir = os.path.join(pwd, 'TestData')
    cv2.namedWindow('Detect')
    for f in test:
        file_path = os.path.join(test_dir, f)
        logger.info('Processing {}'.format(file_path))
        img = cv2.imread(file_path)
        rects, _ = hog.detectMultiScale(img, winStride=(4,4), padding=(8,8), scale=1.05)
        for (x,y,w,h) in rects:
            cv2.rectangle(img, (x,y), (x+w,y+h), (0,0,255), 2)
        cv2.imshow('Detect', img)
        c = cv2.waitKey(0) & 0xff
        if c == 27:
            break
    cv2.destroyAllWindows()


if __name__ == '__main__':
    logger = logger_init()
    pos, neg, test = load_data_set(logger=logger)
    samples, labels = load_train_samples(pos, neg)
    train = extract_hog(samples, logger=logger)
    logger.info('Size of feature vectors of samples: {}'.format(train.shape))
    logger.info('Size of labels of samples: {}'.format(labels.shape))
    svm_detector = train_svm(train, labels, logger=logger)
    test_hog_detect(test, svm_detector, logger)

补充说明

代码中将每部分功能都分成了各个函数,并附有注释。这里不对全部代码进行说明了,而是补充几个重要的地方。

1、数据集

Positive文件夹中存放正样本图片。这里要做行人检测,所以正样本理应是行人。一般大小为64*128,如果尺寸不一致,可以在程序中调整大小为64*128.
png
Negative文件夹中存放负样本图片。负样本可以采用一些无关背景图片。
png
TestData文件夹中存放测试图片。
png
再来看看运行程序时的log信息。
png
可以看到程序自动检查上述几个文件夹,统计的结果为:Positive目录中有924个正样本,Negative目录中有924个负样本,TestData目录中有179个样本。
这里用到的数据集是我从网上下载的。数据量较小,所以一定程度上会限制训练出的模型的准确率,如果要求更高的精度需要增加样本数量。另外一点,我们应该尽量增加负样本的多样性,相比行人的特征,负样本的多样性高得多,所以我们可以预见,最后训练的结果很可能会有一些没能识别为负样本的情况,而这个问题可以通过增加负样本数量和多样性来缓解。

2、HOG特征维度

运行过程中提取出的每张图片对应的HOG特征维度为:(3780,1)。
png
将所有图片的HOG特征向量组合成待训练的特征向量和其对应的标签。
png
可以看出特征向量维度为:(1848,3780),标签维度为:(1848,1)。
总共有924个正样本和924个负样本,每个样本的HOG特征向量维度为(3780,1)。正好对应。
将训练样本送入SVM训练即可,数据集较小,很快就能迭代出结果。
训练好的SVM也会保存为svm.xml。
png

3、检测结果

使用自己训练的检测器:
png
png
虽然人都检测出来了,但是发现有些多出来的框了吧,这个正好就是前面说到的负样本不足的问题。我使用的负样本数据集很小并且多样性不好,所以会有一些样本不能正确分类为负样本。正如图中所示。

使用opencv自带的检测器:
修改代码:

hog.setSVMDetector(svm_detector)

为:

hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector())

其他不变,运行代码:
png
跟前面自己训练的检测器的结果差不多,除了右边几个人重合的部分。
png
看得出效果比用自己的数据集训练的好一些。

C++版本demo

套路上来说跟python版本的基本一致,除了C++读取数据集的图片时的操作是借助制作好的txt文件之外。代码也不难理解,不做赘述了。文件路径请自行更改。

#include "opencv2/opencv.hpp"
#include "opencv2/ml.hpp"
#include <iostream>
#include <string>
#include <fstream>
#include <stdio.h>
#include <cctype>
#include <windows.h>

using namespace std;

#define FILEPATH "E:/opencv/My OpenCV projects/HOG特征/HOG_Pedestran_Detect/Pedestran_Detect/Pedestran_Detect/Pedestrians64x128/"

void Train()
{
    ////////////////////////////////读入训练样本图片路径和类别///////////////////////////////////////////////////
    //图像路径和类别
    vector<string> imagePath;
    vector<int> imageClass;
    string buffer;
    ifstream trainingData(string(FILEPATH) + "TrainData.txt");
    int numOfLine = 0;
    while (!trainingData.eof())
    {
        getline(trainingData, buffer);
        //cout << buffer << endl;
        if (!buffer.empty())
        {
            numOfLine++;
            if (numOfLine % 2 == 0)
            {
                //读取样本类别
                imageClass.push_back(atoi(buffer.c_str()));
            }
            else
            {
                //读取图像路径
                imagePath.push_back(buffer);
            }
        }
    }
    trainingData.close();

    ////////////////////////////////获取样本的HOG特征///////////////////////////////////////////////////
    //样本特征向量矩阵
    int numOfSample = numOfLine / 2;
    cv::Mat featureVectorOfSample(numOfSample, 3780, CV_32FC1);

    //样本的类别
    cv::Mat classOfSample(numOfSample, 1, CV_32SC1);

    cv::Mat convertedImg;
    cv::Mat trainImg;

    for (vector<string>::size_type i = 0;i < imagePath.size();i++)
    {
        cout << "Processing: " << imagePath[i] << endl;
        cv::Mat src = cv::imread(imagePath[i], -1);
        if (src.empty())
        {
            cout << "can not load the image:" << imagePath[i] << endl;
            continue;
        }

        cv::resize(src, trainImg, cv::Size(64, 128));

        //提取HOG特征
        cv::HOGDescriptor hog(cv::Size(64, 128), cv::Size(16, 16), cv::Size(8, 8), cv::Size(8, 8), 9);
        vector<float> descriptors;
        hog.compute(trainImg, descriptors);

        cout << "hog feature vector: " << descriptors.size() << endl;

        for (vector<float>::size_type j = 0;j < descriptors.size();j++)
        {
            featureVectorOfSample.at<float>(i, j) = descriptors[j];
        }
        classOfSample.at<int>(i, 0) = imageClass[i];
    }

    cout << "size of featureVectorOfSample: " << featureVectorOfSample.size() << endl;
    cout << "size of classOfSample: " << classOfSample.size() << endl;

    ///////////////////////////////////使用SVM分类器训练///////////////////////////////////////////////////
    //设置参数,注意Ptr的使用
    cv::Ptr<cv::ml::SVM> svm = cv::ml::SVM::create();
    svm->setType(cv::ml::SVM::C_SVC);
    svm->setKernel(cv::ml::SVM::LINEAR);
    svm->setTermCriteria(cv::TermCriteria(CV_TERMCRIT_ITER, 1000, FLT_EPSILON));

    //训练SVM
    svm->train(featureVectorOfSample, cv::ml::ROW_SAMPLE, classOfSample);

    //保存训练好的分类器(其中包含了SVM的参数,支持向量,α和rho)
    svm->save(string(FILEPATH) + "classifier.xml");

    /*
    SVM训练完成后得到的XML文件里面,有一个数组,叫做support vector,还有一个数组,叫做alpha,有一个浮点数,叫做rho;
    将alpha矩阵同support vector相乘,注意,alpha*supportVector,将得到一个行向量,将该向量前面乘以-1。之后,再该行向量的最后添加一个元素rho。
    如此,变得到了一个分类器,利用该分类器,直接替换opencv中行人检测默认的那个分类器(cv::HOGDescriptor::setSVMDetector()),
    */
    //获取支持向量
    cv::Mat supportVector = svm->getSupportVectors();

    //获取alpha和rho
    cv::Mat alpha;
    cv::Mat svIndex;
    float rho = svm->getDecisionFunction(0, alpha, svIndex);

    //转换类型:这里一定要注意,需要转换为32的
    cv::Mat alpha2;
    alpha.convertTo(alpha2, CV_32FC1);

    //结果矩阵,两个矩阵相乘
    cv::Mat result(1, 3780, CV_32FC1);
    result = alpha2 * supportVector;

    //乘以-1,这里为什么会乘以-1?
    //注意因为svm.predict使用的是alpha*sv*another-rho,如果为负的话则认为是正样本,在HOG的检测函数中,使用rho+alpha*sv*another(another为-1)
    //for (int i = 0;i < 3780;i++)
        //result.at<float>(0, i) *= -1;

    //将分类器保存到文件,便于HOG识别
    //这个才是真正的判别函数的参数(ω),HOG可以直接使用该参数进行识别
    FILE *fp = fopen((string(FILEPATH) + "HOG_SVM.txt").c_str(), "wb");
    for (int i = 0; i<3780; i++)
    {
        fprintf(fp, "%f \n", result.at<float>(0, i));
    }
    fprintf(fp, "%f", rho);

    fclose(fp);
}

void Detect()
{
    cv::Mat img;
    FILE* f = 0;
    char _filename[1024];

    // 获取测试图片文件路径
    f = fopen((string(FILEPATH) + "TestData.txt").c_str(), "rt");
    if (!f)
    {
        fprintf(stderr, "ERROR: the specified file could not be loaded\n");
        return;
    }

    //加载训练好的判别函数的参数(注意,与svm->save保存的分类器不同)
    vector<float> detector;
    ifstream fileIn(string(FILEPATH) + "HOG_SVM.txt", ios::in);
    float val = 0.0f;
    while (!fileIn.eof())
    {
        fileIn >> val;
        detector.push_back(val);
    }
    fileIn.close();

    //设置HOG
    cv::HOGDescriptor hog;
    //hog.setSVMDetector(detector);
    hog.setSVMDetector(cv::HOGDescriptor::getDefaultPeopleDetector());
    cv::namedWindow("people detector", 1);

    // 检测图片
    for (;;)
    {
        // 读取文件名
        char* filename = _filename;
        if (f)
        {
            if (!fgets(filename, (int)sizeof(_filename) - 2, f))
                break;
            if (filename[0] == '#')
                continue;

            //去掉空格
            int l = (int)strlen(filename);
            while (l > 0 && isspace(filename[l - 1]))
                --l;
            filename[l] = '\0';
            img = cv::imread(filename);
        }
        printf("%s:\n", filename);
        if (!img.data)
            continue;

        fflush(stdout);
        vector<cv::Rect> found, found_filtered;
        // run the detector with default parameters. to get a higher hit-rate
        // (and more false alarms, respectively), decrease the hitThreshold and
        // groupThreshold (set groupThreshold to 0 to turn off the grouping completely).
        //多尺度检测
        hog.detectMultiScale(img, found, 0, cv::Size(8, 8), cv::Size(32, 32), 1.05, 2);

        size_t i, j;
        //去掉空间中具有内外包含关系的区域,保留大的
        for (i = 0; i < found.size(); i++)
        {
            cv::Rect r = found[i];
            for (j = 0; j < found.size(); j++)
                if (j != i && (r & found[j]) == r)
                    break;
            if (j == found.size())
                found_filtered.push_back(r);
        }

        // 适当缩小矩形
        for (i = 0; i < found_filtered.size(); i++)
        {
            cv::Rect r = found_filtered[i];
            // the HOG detector returns slightly larger rectangles than the real objects.
            // so we slightly shrink the rectangles to get a nicer output.
            r.x += cvRound(r.width*0.1);
            r.width = cvRound(r.width*0.8);
            r.y += cvRound(r.height*0.07);
            r.height = cvRound(r.height*0.8);
            rectangle(img, r.tl(), r.br(), cv::Scalar(0, 255, 0), 3);
        }

        imshow("people detector", img);
        int c = cv::waitKey(0) & 255;
        if (c == 'q' || c == 'Q' || !f)
            break;
    }
    if (f)
        fclose(f);
    return;
}

int main()
{
    Train();

    Detect();

    return 0;
}

完整工程下载

我把完整工程和数据集一起放到了github上:
Python版本的demo:https://github.com/ToughStoneX/hog_pedestran_detect_python
C++版本的demo:https://github.com/ToughStoneX/hog_pedestran_detect_c_plus_plus

猜你喜欢

转载自blog.csdn.net/hongbin_xu/article/details/79845290