OpenCV学习笔记(三)人脸检测的代码分析
一、预备知识:
1、动态内存存储及操作函数
CvMemStorage
typedef struct CvMemStorage
{
struct CvMemBlock* bottom;
struct CvMemBlock* top;
struct CvMemStorage* parent;
int block_size;
int free_space;
} CvMemStorage;
内存存储器是一个可用来存储诸如序列,轮廓,图形,子划分等动态增长数据结构的底层结构。它是由一系列以同等大小的内存块构成,呈列表型 ---bottom 域指的是列首,top 域指的是当前指向的块但未必是列尾.在bottom和top之间所有的块(包括bottom, 不包括top)被完全占据了空间;在 top和列尾之间所有的块(包括块尾,不包括top)则是空的;而top块本身则被占据了部分空间 -- free_space 指的是top块剩余的空字节数。新分配的内存缓冲区(或显示的通过 cvMemStorageAlloc 函数分配,或隐示的通过 cvSeqPush, cvGraphAddEdge等高级函数分配)总是起始于当前块(即top块)的剩余那部分,如果剩余那部分能满足要求(够分配的大小)。分配后,free_space 就减少了新分配的那部分内存大小,外加一些用来保存适当列型的附加大小。当top块的剩余空间无法满足被分配的块(缓冲区)大小时,top块的下一个存储块被置为当前块(新的top块) -- free_space 被置为先前分配的整个块的大小。如果已经不存在空的存储块(即:top块已是列尾),则必须再分配一个新的块(或从parent那继承,见 cvCreateChildMemStorage)并将该块加到列尾上去。于是,存储器(memory storage)就如同栈(Stack)那样, bottom指向栈底,(top, free_space)对指向栈顶。栈顶可通过 cvSaveMemStoragePos保存,通过 cvRestoreMemStoragePos 恢复指向, 通过 cvClearStorage 重置。
CvMemBlock
内存存储块结构
typedef struct CvMemBlock
{
struct CvMemBlock* prev;
struct CvMemBlock* next;
} CvMemBlock;
CvMemBlock 代表一个单独的内存存储块结构。 内存存储块中的实际数据存储在 header块 之后(即:存在一个头指针 head 指向的块 header ,该块不存储数据),于是,内存块的第 i 个字节可以通过表达式 ((char*)(mem_block_ptr+1))[i] 获得。然而,通常没必要直接去获得存储结构的域。
CvMemStoragePos
内存存储块地址
typedef struct CvMemStoragePos
{
CvMemBlock* top;
int free_space;
} CvMemStoragePos;
该结构(如以下所说)保存栈顶的地址,栈顶可以通过 cvSaveMemStoragePos 保存,也可以通过 cvRestoreMemStoragePos 恢复。
________________________________________
cvCreateMemStorage
创建内存块
CvMemStorage* cvCreateMemStorage( int block_size=0 );
block_size:存储块的大小以字节表示。如果大小是 0 byte, 则将该块设置成默认值 当前默认大小为64k.
函数 cvCreateMemStorage 创建一内存块并返回指向块首的指针。起初,存储块是空的。头部(即:header)的所有域值都为 0,除了 block_size 外.
________________________________________
cvCreateChildMemStorage
创建子内存块
CvMemStorage* cvCreateChildMemStorage( CvMemStorage* parent );
parent 父内存块
函数 cvCreateChildMemStorage 创建一类似于普通内存块的子内存块,除了内存分配/释放机制不同外。当一个子存储块需要一个新的块加入时,它就试图从parent 那得到这样一个块。如果 parent 中 还未被占据空间的那些块中的第一个块是可获得的,就获取第一个块(依此类推),再将该块从 parent 那里去除。如果不存在这样的块,则 parent 要么分配一个,要么从它自己 parent (即:parent 的 parent) 那借个过来。换句话说,完全有可能形成一个链或更为复杂的结构,其中的内存存储块互为 child/ parent 关系(父子关系)。当子存储结构被释放或清除,它就把所有的块还给各自的 parent. 在其他方面,子存储结构同普通存储结构一样。
子存储结构在下列情况中是非常有用的。想象一下,如果用户需要处理存储在某个块中的动态数据,再将处理的结果存放在该块中。在使用了最简单的方法处理后,临时数据作为输入和输出数据被存放在了同一个存储块中,于是该存储块看上去就类似下面处理后的样子: Dynamic data processing without using child storage. 结果,在存储块中,出现了垃圾(临时数据)。然而,如果在开始处理数据前就先建立一个子存储块,将临时数据写入子存储块中并在最后释放子存储块,那么最终在 源/目的存储块 (source / destination storage) 中就不会出现垃圾, 于是该存储块看上去应该是如下形式:Dynamic data processing using a child storage.
cvReleaseMemStorage
释放内存块
void cvReleaseMemStorage( CvMemStorage** storage );
storage: 指向被释放了的存储块的指针
函数 cvReleaseMemStorage 释放所有的存储(内存)块 或者 将它们返回给各自的 parent(如果需要的话)。 接下来再释放 header块(即:释放头指针 head 指向的块 = free(head))并清除指向该块的指针(即:head = NULL)。在释放作为 parent 的块之前,先清除各自的 child 块。
cvClearMemStorage
清空内存存储块
void cvClearMemStorage( CvMemStorage* storage );
storage:存储存储块
函数 cvClearMemStorage 将存储块的 top 置到存储块的头部(注:清空存储块中的存储内容)。该函数并不释放内存(仅清空内存)。假使该内存块有一个父内存块(即:存在一内存块与其有父子关系),则函数就将所有的块返回给其 parent.
cvMemStorageAlloc
在存储块中分配以内存缓冲区
void* cvMemStorageAlloc( CvMemStorage* storage, size_t size );
storage:内存块.
size:缓冲区的大小.
函数 cvMemStorageAlloc 在存储块中分配一内存缓冲区。该缓冲区的大小不能超过内存块的大小,否则就会导致运行时错误。缓冲区的地址被调整为CV_STRUCT_ALIGN 字节 (当前为 sizeof(double)).
cvMemStorageAllocString
在存储块中分配一文本字符串
typedef struct CvString
{
int len;
char* ptr;
}
CvString;
CvString cvMemStorageAllocString( CvMemStorage* storage, const char* ptr, int len=-1 );
storage:存储块
ptr:字符串
len:字符串的长度(不计算'\0')。如果参数为负数,函数就计算该字符串的长度。
函数 cvMemStorageAlloString 在存储块中创建了一字符串的拷贝。它返回一结构,该结构包含字符串的长度(该长度或通过用户传递,或通过计算得到)和指向被拷贝了的字符串的指针。
cvSaveMemStoragePos
保存内存块的位置(地址)
void cvSaveMemStoragePos( const CvMemStorage* storage, CvMemStoragePos* pos );
storage:内存块.
pos:内存块顶部位置。
函数 cvSaveMemStoragePos 将存储块的当前位置保存到参数 pos 中。 函数 cvRestoreMemStoragePos 可进一步获取该位置(地址)。
cvRestoreMemStoragePos
恢复内存存储块的位置
void cvRestoreMemStoragePos( CvMemStorage* storage, CvMemStoragePos* pos );
storage:内存块.
pos:新的存储块的位置
函数 cvRestoreMemStoragePos 通过参数 pos 恢复内存块的位置。该函数和函数 cvClearMemStorage 是释放被占用内存块的唯一方法。注意:没有什么方法可去释放存储块中被占用的部分内存。
2、分类器结构及操作函数:
CvHaarFeature
#define CV_HAAR_FEATURE_MAX 3
typedef struct CvHaarFeature
{
int tilted;
struct
{
CvRect r;
float weight;
} rect[CV_HAAR_FEATURE_MAX];
}
CvHaarFeature;
一个 harr 特征由 2-3 个具有相应权重的矩形组成
titled :
rect[CV_HAAR_FEATURE_MAX];
CvHaarClassifier
typedef struct CvHaarClassifier
{
int count;
CvHaarFeature* haar_feature;
float* threshold;
int* left;
int* right;
float* alpha;
}
CvHaarClassifier;
int count;
CvHaarStageClassifier
typedef struct CvHaarStageClassifier
{
int count;
float threshold;
CvHaarClassifier* classifier;
int next;
int child;
int parent;
}
CvHaarStageClassifier;
int count;
float threshold;
CvHaarClassifier* classifier;
CvHaarClassifierCascade
typedef struct CvHidHaarClassifierCasca de CvHidHaarClassifierCasca de;
typedef struct CvHaarClassifierCascade
{
int flags;
int count;
CvSize orig_window_size;
CvSize real_window_size;
double scale;
CvHaarStageClassifier* stage_classifier;
CvHidHaarClassifierCasca de* hid_cascade;
}
CvHaarClassifierCascade;
int flags;
int count;
CvSize orig_window_size;
CvSize real_window_size;
double scale;
CvHaarStageClassifier* stage_classifier;
CvHidHaarClassifierCasca de* hid_cascade;
所有的结构都代表一个级联boosted Haar分类器。级联有下面的等级结构:
Cascade:
Stage1:
Classifier11:
Feature11
Classifier12:
Feature12
...
Stage2:
Classifier21:
Feature21
...
...
整个等级可以手工构建,也可以利用函数cvLoadHaarClassifierCasc ade从已有的磁盘文件或嵌入式基中导入。
特征检测用到的函数:
cvLoadHaarClassifierCasc ade
从文件中装载训练好的级联分类器或者从OpenCV中嵌入的分类器数据库中导入
CvHaarClassifierCascade* cvLoadHaarClassifierCasc ade(
const char* directory,
CvSize orig_window_size );
directory :训练好的级联分类器的路径
orig_window_size:级联分类器训练中采用的检测目标的尺寸。因为这个信息没有在级联分类器中存储,所有要单独指出。
函数 cvLoadHaarClassifierCasc ade 用于从文件中装载训练好的利用海尔特征的级联分类器,或者从OpenCV中嵌入的分类器数据库中导入。分类器的训练可以应用函数haartraining(详细察看opencv/apps/haartraining)
函数 已经过时了。现在的目标检测分类器通常存储在 XML 或 YAML 文件中,而不是通过路径导入。从文件中导入分类器,可以使用函数 cvLoad 。
cvReleaseHaarClassifierC ascade
释放haar classifier cascade。
void cvReleaseHaarClassifierC ascade( CvHaarClassifierCascade** cascade );
cascade :双指针类型指针指向要释放的cascade. 指针由函数声明。
函数 cvReleaseHaarClassifierC ascade 释放cascade的动态内存,其中cascade的动态内存或者是手工创建,或者通过函数 cvLoadHaarClassifierCasc ade 或 cvLoad分配。
cvHaarDetectObjects
检测图像中的目标
typedef struct CvAvgComp
{
CvRect rect;
int neighbors;
}
CvAvgComp;
CvSeq* cvHaarDetectObjects( const CvArr* image,
CvHaarClassifierCascade* cascade,
CvMemStorage* storage,
double scale_factor=1.1,
int min_neighbors=3, int flags=0,
CvSize min_size=cvSize(0,0) );
image 被检图像
cascade harr 分类器级联的内部标识形式
storage 用来存储检测到的一序列候选目标矩形框的内存区域。
scale_factor 在前后两次相继的扫描中,搜索窗口的比例系数。例如1.1指将搜索窗口依次扩大10%。
min_neighbors 构成检测目标的相邻矩形的最小个数(缺省-1)。如果组成检测目标的小矩形的个数和小于min_neighbors-1 都会被排除。如果min_neighbors 为 0, 则函数不做任何操作就返回所有的被检候选矩形框,这种设定值一般用在用户自定义对检测结果的组合程序上。
flags 操作方式。当前唯一可以定义的操作方式是 CV_HAAR_DO_CANNY_PRUNING。如果被设定,函数利用Canny边缘检测器来排除一些边缘很少或者很多的图像区域,因为这样的区域一般不含被检目标。人脸检测中通过设定阈值使用了这种方法,并因此提高了检测速度。
min_size 检测窗口的最小尺寸。缺省的情况下被设为分类器训练时采用的样本尺寸(人脸检测中缺省大小是~20×20)。
函数 cvHaarDetectObjects 使用针对某目标物体训练的级联分类器在图像中找到包含目标物体的矩形区域,并且将这些区域作为一序列的矩形框返回。函数以不同比例大小的扫描窗口对图像进行几次搜索(察看cvSetImagesForHaarClassi fierCascade)。 每次都要对图像中的这些重叠区域利用cvRunHaarClassifierCasca de进行检测。 有时候也会利用某些继承(heuristics)技术以减少分析的候选区域,例如利用 Canny 裁减 (prunning)方法。 函数在处理和收集到候选的方框(全部通过级联分类器各层的区域)之后,接着对这些区域进行组合并且返回一系列各个足够大的组合中的平均矩形。调节程序中的缺省参数(scale_factor=1.1, min_neighbors=3, flags=0)用于对目标进行更精确同时也是耗时较长的进一步检测。为了能对视频图像进行更快的实时检测,参数设置通常是:scale_factor=1.2, min_neighbors=2, flags=CV_HAAR_DO_CANNY_PRUNING, min_size=<minimum possible face size> (例如, 对于视频会议的图像区域).
cvSetImagesForHaarClassi fierCascade
为隐藏的cascade(hidden cascade)指定图像
void cvSetImagesForHaarClassi fierCascade( CvHaarClassifierCascade* cascade,
const CvArr* sum, const CvArr* sqsum,
const CvArr* tilted_sum, double scale );
cascade 隐藏 Harr 分类器级联 (Hidden Haar classifier cascade), 由函数 cvCreateHidHaarClassifie rCascade生成
sum 32-比特,单通道图像的积分图像(Integral (sum) 单通道 image of 32-比特 integer format). 这幅图像以及随后的两幅用于对快速特征的评价和亮度/对比度的归一化。 它们都可以利用函数 cvIntegral从8-比特或浮点数 单通道的输入图像中得到。
sqsum 单通道64比特图像的平方和图像
tilted_sum 单通道32比特整数格式的图像的倾斜和(Tilted sum)
scale cascade的窗口比例. 如果 scale=1, 就只用原始窗口尺寸检测 (只检测同样尺寸大小的目标物体) - 原始窗口尺寸在函数cvLoadHaarClassifierCasc ade中定义 (在 "<default_face_cascade>"中缺省为24x24), 如果scale=2, 使用的窗口是上面的两倍 (在face cascade中缺省值是48x48 )。 这样尽管可以将检测速度提高四倍,但同时尺寸小于48x48的人脸将不能被检测到。
函数 cvSetImagesForHaarClassi fierCascade 为hidden classifier cascade 指定图像 and/or 窗口比例系数。 如果图像指针为空,会继续使用原来的图像(i.e. NULLs 意味这"不改变图像")。比例系数没有 "protection" 值,但是原来的值可以通过函数 cvGetHaarClassifierCasca deScale 重新得到并使用。这个函数用于对特定图像中检测特定目标尺寸的cascade分类器的设定。函数通过cvHaarDetectObjects进行内部调用,但当需要在更低一层的函数cvRunHaarClassifierCasca de中使用的时候,用户也可以自行调用。
cvRunHaarClassifierCasca de
在给定位置的图像中运行 cascade of boosted classifier
int cvRunHaarClassifierCasca de( CvHaarClassifierCascade* cascade,
CvPoint pt, int start_stage=0 );
cascade Haar 级联分类器
pt 待检测区域的左上角坐标。待检测区域大小为原始窗口尺寸乘以当前设定的比例系数。当前窗口尺寸可以通过cvGetHaarClassifierCasca deWindowSize重新得到。
start_stage 级联层的初始下标值(从0开始计数)。函数假定前面所有每层的分类器都已通过。这个特征通过函数cvHaarDetectObjects内部调用,用于更好的处理器高速缓冲存储器。
函数 cvRunHaarHaarClassifierC ascade 用于对单幅图片的检测。在函数调用前首先利用 cvSetImagesForHaarClassi fierCascade设定积分图和合适的比例系数 (=> 窗口尺寸)。当分析的矩形框全部通过级联分类器每一层的时返回正值(这是一个候选目标),否则返回0或负值。
二、例程分析:
例子:利用级联的Haar classifiers寻找检测目标(e.g. faces).
#include "cv.h"
#include "highgui.h"
//读取训练好的分类器。
CvHaarClassifierCascade* load_object_detector( const char* cascade_path )
{
return (CvHaarClassifierCascade*)cvLoad( cascade_path );
}
void detect_and_draw_objects( IplImage* image,
CvHaarClassifierCascade* cascade,
int do_pyramids )
{
IplImage* small_image = image;
CvMemStorage* storage = cvCreateMemStorage(0); //创建动态内存
CvSeq* faces;
int i, scale = 1;
if( do_pyramids )
{
small_image = cvCreateImage( cvSize(image->width/2,image->height/2), IPL_DEPTH_8U, 3 );
cvPyrDown( image, small_image, CV_GAUSSIAN_5x5 );//函数 cvPyrDown 使用 Gaussian 金字塔分解对输入图像向下采样。首先它对输入图像用指定滤波器进行卷积,然后通过拒绝偶数的行与列来下采样图像。
scale = 2;
}
faces = cvHaarDetectObjects( small_image, cascade, storage, 1.2, 2, CV_HAAR_DO_CANNY_PRUNING );
for( i = 0; i < faces->total; i++ )
{
CvRect face_rect = *(CvRect*)cvGetSeqElem( faces, i, 0 );
cvRectangle( image, cvPoint(face_rect.x*scale,face_rect.y*scale),
cvPoint((face_rect.x+face_rect.width)*scale,
(face_rect.y+face_rect.height)*scale),
CV_RGB(255,0,0), 3 );
}
if( small_image != image )
cvReleaseImage( &small_image );
cvReleaseMemStorage( &storage ); //释放动态内存
}
int main( int argc, char** argv )
{
IplImage* image;
if( argc==3 && (image = cvLoadImage( argv[1], 1 )) != 0 )
{
CvHaarClassifierCascade* cascade = load_object_detector(argv[2]);
detect_and_draw_objects( image, cascade, 1 );
cvNamedWindow( "test", 0 );
cvShowImage( "test", image );
cvWaitKey(0);
cvReleaseHaarClassifierC ascade( &cascade );
cvReleaseImage( &image );
}
return 0;
}
关键代码很简单,装载分类器,对输入图像进行金字塔采样,然后用cv的函数进行检测目标,最后输出检测到的目标矩形。