[CV]人脸识别检测数据集

做了一段时间的人脸识别和检测,在这里列一下用过的数据集。基本上,大家近期也都是在这几个数据集上检测自己的算法。网上这方面的总结虽然不少,但是一则有些是多年前的数据,或是规模太小或是链接已经失效,再则是数据集的测试协议定义不明,不尽适合用来和其它方法做比较。

1. Labeled Faces in the Wild:
做人脸识别,准确的说是人脸验证(Face Verification),UMass的LFW估计是最近被用的最多的了,LFW采用的测试协议也已经被几个新的数据集沿用了。人脸验证是指,给定两张人脸的照片,算法需要判断它们是不是来自同一个人。最新的结果(ICCV2013),在限制条件最少的协议下,识别的准确率现在已经高达96%了。[广告^_^] 在限制条件最严的协议下,我们的CVPR2013的结果曾经是最好的。最近被Fisher Vector超过了.. 我们还会回来的…

2. YouTube Faces DB:
YouTube Video Faces也是用来做人脸验证的,和LFW不同的是,在这个数据集下,算法需要判断两段视频里面是不是同一个人。有不少在照片上有效的方法,在视频上未必有效/高效。[广告^_^] 在这个数据集上,我们的最新的结果超过81%,目前还没有看到更高的准确率。

3. FDDB:
FDDB也是UMass的数据集,被用来做人脸检测(Face Detection)。这个数据集比较大,比较有挑战性。而且作者提供了程序用来评估检测结果,所以在这个数据上面比较算法也相对公平。FDDB的一个问题是它的标注是椭圆而不是一般用的长方形,这一点可以会导致检测结果评估不准确。不过因为标准统一,问题倒也不大。[广告^_^] 我们ICCV2013的文章在这个数据上面有不错的结果。

4. The Gallagher Collection Person Dataset:
这也是一个做人脸检测的数据集,是Andrew Gallagher的家庭相册。虽然不是给人脸识别设计的,但是很接近实际应用的场景。很适合用来测试自己的方法。

5. The Annotated Faces in the Wild (AFW) testset:
这还是一个做人脸检测的数据集,随UCI的Xiangxin Zhu在CVPR2012的文章发布。值得注意的是在他们的主页有公开的源代码。虽然人脸检测做了很久,但是效果比较好的,可以在网上方便的得到的检测库除了OpenCV以外并不多见。

6. CMU Dataset:
做人脸检测的数据集,这是一个很有些年头的数据集了,虽然大家最近不常用这个数据,但是这不代表这个老数据集很容易对付。最新的检测算法往往需要比较稠密的取比较复杂的特征,这在这个黑白而且分辨率不高的数据集上未必可行。

7. POS Labeled Faces in the Wild:
这个数据我还没有用过,是最近才发布的一个更大型的LFW。可以用来做人脸识别。看起来很不错的样子。

自己感觉比较好用的数据集就是这些了。
感觉不知道应该写点什么,想来还是写写自己专业相关的内容会比较有趣。

[OpenCV] detectMultiScale: output detection score

OpenCV provides quite decent implementation of the Viola-Jones Face detector.

A quick example looks like this (OpenCV 2.4.5 tested):

// File: main.cc
#include 

using namespace cv;

int main(int argc, char **argv) {

    CascadeClassifier cascade;
    const float scale_factor(1.2f);
    const int min_neighbors(3);

    if (cascade.load("./lbpcascade_frontalface.xml")) {

        for (int i = 1; i < argc; i++) {

            Mat img = imread(argv[i], CV_LOAD_IMAGE_GRAYSCALE);
            equalizeHist(img, img);
            vector objs;
            cascade.detectMultiScale(img, objs, scale_factor, min_neighbors);

            Mat img_color = imread(argv[i], CV_LOAD_IMAGE_COLOR);
            for (int n = 0; n < objs.size(); n++) {
                rectangle(img_color, objs[n], Scalar(255,0,0), 8);
            }
            imshow("VJ Face Detector", img_color);
            waitKey(0);
        }
    }

    return 0;
}
g++ -std=c++0x -I/usr/local/include `pkg-config --libs opencv` main.cc -o main

The detection results are as shown below:
result

For more serious user, it would be nice to have a detection result for each detected face.
The OpenCV provides a overloaded function designed for this usage which is lack of detailed documentation:

vector reject_levels;
vector level_weights;
cascade.detectMultiScale(img, objs, reject_levels, level_weights, scale_factor, min_neighbors);

The reject_levels and level_weights will keep being empty until you write it like this (The whole file):

// File: main.cc
#include 

using namespace cv;

int main(int argc, char **argv) {

    CascadeClassifier cascade;
    const float scale_factor(1.2f);
    const int min_neighbors(3);

    if (cascade.load("./lbpcascade_frontalface.xml")) {

        for (int i = 1; i < argc; i++) {

            Mat img = imread(argv[i], CV_LOAD_IMAGE_GRAYSCALE);
            equalizeHist(img, img);
            vector objs;
            vector reject_levels;
            vector level_weights;
            cascade.detectMultiScale(img, objs, reject_levels, level_weights, scale_factor, min_neighbors, 0, Size(), Size(), true);

            Mat img_color = imread(argv[i], CV_LOAD_IMAGE_COLOR);
            for (int n = 0; n < objs.size(); n++) {
                rectangle(img_color, objs[n], Scalar(255,0,0), 8);
                putText(img_color, std::to_string(level_weights[n]),
                        Point(objs[n].x, objs[n].y), 1, 1, Scalar(0,0,255));
            }
            imshow("VJ Face Detector", img_color);
            waitKey(0);
        }
    }

    return 0;
}

However, this will give you a large number of detected rectangles:
result-org

This is because OpenCV skips the step of filtering out the overlapped small rectangles. I have no idea whether this is by design. But output likes this would not be helpful at least in my own case.

So we would need to make our own changes in the OpenCV's source code.
There are different ways to design detection score, such as
"In the OpenCV implementation, stage_sum is computed and compared against the i stage_threshold for each stage to accept/reject a candidate window. We define the detection score for a candidate window as K*stage_when_rejected + stage_sum_for_stage_when_rejected. If a window is accepted by the cascade, we just K*last_stage + stage_sum_for_last_stage. Choosing K as a large value e.g., 1000, we ensure that windows rejected at stage i have higher score than those rejected at stage i-1." from http://vis-www.cs.umass.edu/fddb/faq.html

Actually, I found a straightforward design of detection score works well in my own work. In the last stage of the face detector in OpenCV, detection rectangles are grouped into clustered to eliminated small overlapped rectangles while keeping the most potential rectangles. The number of final detected faces is at most same as the number of clusters. So we can simply use the number of rectangles grouped into the cluster as the detection score of the associated final rectangle, which may not be accurate but could work.

To make this change, in OpenCV-2.4.5, find the file modules/objdetect/src/cascadedetect.cpp (line 200)

// modules/objdetect/src/cascadedetect.cpp (line 200)
// int n1 = levelWeights ? rejectLevels[i] : rweights[i]; //< comment out this line
int n1 = rweights[i]; //< the change

We then modify the file main.cc accordingly:

// File: main.cc
#include 

using namespace cv;

int main(int argc, char **argv) {

    CascadeClassifier cascade;
    const float scale_factor(1.2f);
    const int min_neighbors(3);

    if (cascade.load("./lbpcascade_frontalface.xml")) {

        for (int i = 1; i < argc; i++) {

            Mat img = imread(argv[i], CV_LOAD_IMAGE_GRAYSCALE);
            equalizeHist(img, img);
            vector objs;
            vector reject_levels;
            vector level_weights;
            cascade.detectMultiScale(img, objs, reject_levels, level_weights, scale_factor, min_neighbors, 0, Size(), Size(), true);

            Mat img_color = imread(argv[i], CV_LOAD_IMAGE_COLOR);
            for (int n = 0; n < objs.size(); n++) {
                rectangle(img_color, objs[n], Scalar(255,0,0), 8);
                putText(img_color, std::to_string(reject_levels[n]),
                        Point(objs[n].x, objs[n].y), 1, 1, Scalar(0,0,255));
            }
            imshow("VJ Face Detector", img_color);
            waitKey(0);
        }
    }

    return 0;
}

And we can have the detection scores like this:
result-final

[CV]检测灰色块

遇到一个看起来非常简单的问题:一张图片里面有一些某种颜色的色块,怎么样找到它们?
sample-mask
比较囧的是这个问题的起因。因为图片的标注文件丢了,不得不这么反向做检测来找回这些标注…想想人脸那么麻烦的结构都可以被检测出来,CV对付这种几乎完美的单颜色色块应该是小菜一碟吧。所以,大家虽然感觉反向检测自己处理的图片比较囧,但是完全不觉得这是个问题。同屋的哥们当场表示,他可以在10分钟之内搞定。

他的做法是我们一开始的想法,先按照色块的颜色(RGB: 128,128,128)把图片二值化,由于JPEG压缩,色块可能会有噪声。
sample-mask-binarized

然后我们准备对每行求和,对每列求和,会得到两个“直方图”,然后根据“峰”的位置和宽度就可以知道色块的位置和大小了。这个方法的确可以处理一张图里只有一个色块的情况,但是当图里有多个色块的时候,会出现“峰”的叠加,比如这张图,按行求和之后,由于有并排的色块。直方图就会变成这样:
sample-mask-histogram
这种情况之前这种方法就不好处理了。

结果这个看起来非常简单的问题,我们也折腾了好半天。最后还是得人指点,用连通分量来做,才得以解决。

做法是在二值化的图像上,找到不同的连通块 (Connected component),然后留下比较大的,就是灰色块了。为了处理噪声,当然需要用Gaussian做一做模糊之类的。效果还不错。(彩色色块表示检测出来的灰色色块)

large-sample-mask

large-sample-mask-result

问题总是没有看起来的那么简单。
matlab 代码放到 Github 上了: detect-gray-square

[OpenCV]detectMultiScale

I met a problem when using the interface ‘detectMultiScale’ of OpenCV. The rectangles it gives out may not be fully inside the frame of the original image. As a result, if these rectangles are applied directly on the original image to crop out the detected objects, your programs crash.

These are the interfaces

virtual void detectMultiScale( const Mat& image,
                               CV_OUT vector& objects,
                               double scaleFactor=1.1,
                               int minNeighbors=3, int flags=0,
                               Size minSize=Size(),
                               Size maxSize=Size() );

and

virtual void detectMultiScale( const Mat& image,
                               CV_OUT vector& objects,
                               vector& rejectLevels,
                               vector& levelWeights,
                               double scaleFactor=1.1,
                               int minNeighbors=3, int flags=0,
                               Size minSize=Size(),
                               Size maxSize=Size(),
                               bool outputRejectLevels=false );

I am copying what I wrote for a pull request on Github, this ‘may-be issue’ can be fixed easily by modifying one line in the source file

modules/objdetect/src/cascadedetect.cpp

Replace this one

Size processingRectSize( scaledImageSize.width - originalWindowSize.width + 1, scaledImageSize.height - originalWindowSize.height + 1 );

with this line

Size processingRectSize( scaledImageSize.width - originalWindowSize.width , scaledImageSize.height - originalWindowSize.height);

My explanation goes here, ignore the line numbers if they look wrong to you.
“Actually, in the code, the workflow is more complicated. In the file cascadedetect.cpp

This is the line building the final detected rectangle

995 rectangles->push_back(Rect(cvRound(x*scalingFactor), cvRound(y*scalingFactor), winSize.width, winSize.height));

the winSize is assigned here

969 Size winSize(cvRound(classifier->data.origWinSize.width * scalingFactor), cvRound(classifier->data.origWinSize.height * scalingFactor));

while the maximum value of x and y can be find here, they are related to the processingRectSize.

971         int y1 = range.start * stripSize;
972         int y2 = min(range.end * stripSize, processingRectSize.height);
973         for( int y = y1; y < y2; y += yStep )
974         {
975             for( int x = 0; x < processingRectSize.width; x += yStep )

Say the original image size is O, the original window size is W, scaling factor is F. O and W is integer and F is a decimal usually larger than 1. The width and height are assumed to be the same for example.

If we calculate the right-most point of the detected rectangle, it should be:

the maximum x is: (cvRound(O/F) - W), current winSize is W*F, following the line 995 we get:

cvRound( (cvRound(O/F) - W) * F ) + W*F

This can be larger than O, say O is 600, F is 4.177250, W is 24, the number we can get above is 601.254 which is larger than 600."

Hope these help.

比较变量地址并不可靠

最近云风大牛又在黑我C++,可是在我学会之前,我还是要坚定不移地待在这贼船上。
嘿嘿 :]

“用比较地址的方法来判断两个变量(的引用)是不是同一个变量是不可靠的”,这个问题很简单,却也容易忽视。

现实的情况是这样的。在写Computer Vision实验的时候,因为程序要面临的计算量往往很大,对程序的性能的优化是十分重要的。
所以我就写出了这样的代码:

void foo_func(Foo& foo)
{
    //< 如果foo和上次传入的是同一个,就省掉一部分重复计算
    static Foo *p_foo = NULL;
    if (p_foo != &foo)
    {
        p_foo = &foo;
        //< 大量计算
    }

    //< 后续计算
}

在实验里,Foo这个类本身往往比较复杂,不可能做内容的逐一比较,同时这个类的实例在初始化之后内容就保持不变。写这个函数的时候没有觉得有什么问题,结果也是正确的。

但是,这么做显然是不可靠的。
这段代码就说明问题了:

#include 

int main(int argc, char **argv)
{
    using namespace std;
    for (int i = 0; i < 100; i++)
    {   
        int j;
        cout << "i: " << i << " &j: " << &j << endl;
    }   
    return 0;
}

在Mac下输出是这样的:

i: 0 &j: 0x7fff52decad0
i: 1 &j: 0x7fff52decad0
i: 2 &j: 0x7fff52decad0
i: 3 &j: 0x7fff52decad0
i: 4 &j: 0x7fff52decad0
i: 5 &j: 0x7fff52decad0
i: 6 &j: 0x7fff52decad0
i: 7 &j: 0x7fff52decad0
i: 8 &j: 0x7fff52decad0
i: 9 &j: 0x7fff52decad0
....

编译器在分配内存的时候,会去复用之前刚刚释放掉的空间,这个是很显然的道理。在上面这种循环里面,局部变量所在的那块空间正好一直被同一个变量复用,如果我每次给j初始化不同的值,它们的地址一致但却不是同一个变量,所以像这样比较变量地址并不可靠。

现在看来,一个可能的解决方法就是给Foo的每一个实例增加一个唯一的ID了。
有没有不用修改Foo就能达到快速比较两个Foo实例的方法呢?

利用摄像头判断手机移动方向[MPMotionPattern]

这部分代码是做原型用的,现在已经不在项目里。
也没有什么特别的算法,所以就放在这里吧。
https://github.com/pppoe/MPMotionPattern

需要用到之前的“iOS逐帧处理录像-MPVideoProcessor”
主要的算法是从TinyMotion这个项目来的,用到的也是他们的算法。
功能很简单,就是通过iPhone摄像头拍摄的图像,判断手机当前的移动方向。

Screenshot

现在也只是支持上下左右。因为iPhone本身就有传感器可以取到这种动作,所以用图像似乎也没有什么特别的优势…
只是感觉挺好玩的。XD

CVPR 2012 酱油记

没有文章,老板还是很nice的给了一次去参加CVPR的机会。虽然只是去酱油一把,但是可以到CV三会之一的CVPR看看,还是很高兴的。

提前一天到了罗德岛,这里貌似是美国最小的一个州。风景不错,但是也不觉得有什么很特别的地方。按照会议进程,头两天是Workshop和Tutorial。Tutorial是集中时间讲一个话题,比如这次有Deep Learning, Gaussian Process这种,这里列出了所有的Tutorial,有很多很吸引人的题目。Workshop是做一个小领域的人们讨论这个方向的文章。一趟下来感觉,除了很偏工程的Tutorial,没有事先看看相关的东西就去听还是比较吃力的。相对来说,Workshop比较容易懂。主要是因为Workshop里的文章多,总有一篇适合你… 两天的Workshop和Tutorial覆盖了各个方向,虽然还没有开始主会,已经感觉是码农进了大观园。感叹各种听不懂…

这两天比较有印象的是Perceptual Organization和Biometric Workshop的几篇文章和Vision Industry & Entrepreneur Workshop的两场演讲,Boaz Super列了很多最近的利用CV技术创业的公司,然后用一大堆数据告诉大家,CV是很有前途地。虽然也听了两次Deep Learning相关的Tutorial,唯一有印象的就是Kai Yu的Sparse Coding那场,他的SC相关的文章回头还得找出来看看。

三天主会还是比较紧的。主会时间是早上8点半开始第一场Posters,一天两场Oral,三场Posters,如果Poster很相关的话,基本上一整天都在那些海报前面走来走去。

我感兴趣的Image Classification相关的文章主要是在第二天和第三天。收获确实不小,看到了很多不熟悉的名词,还有些之前没想到的东西。不过也可能是因为自己比较土鳖,看啥都新鲜…可以和作者讨论是比较理想的状态,而且也很有意思,只是文章读得还是少了,能去讨论的情况不多。今年有好几篇讨论Fine-Grained Classification的文章,做这个的可能会越来越多吧。还有就是知道了Transfer Learning这个方向,和Classification也很相关的。训练样本和测试样本不是同一个环境下得到的,这个本身就是比较实际的问题。Object Recognition这块,还是很多文章在讨论BoW之上的改进,有几篇都是在讨论如何加入空间信息,不知道什么时候会有完全不一样的一套模型出来。然后比较有印象的就是各种Simultaneously,比如把分割,分类,标注等等在一个框架下一块儿做完。Oral里面对于Transfer Learning的两篇印象较深,有一篇是Boqing师兄的,讲得很清楚。

这几天,除了看各种文章,就是看到了各位大牛小牛,认识了一些朋友,不过因为没有文章,也不是很容易讨论起来…还有就是遇到了很多科大的师兄,还有同一级的同学,我科的各位一如既往的给力而且Nice,相当高兴。

有一天吃晚饭的时候因为没有找到地方,和三个外国朋友一桌。有一位是开公司的,不住的抱怨现在CVPR里能用的东西太少,算法太慢。想来也是,现在在做的分类算法就根本不能放在机器人上去用。不过技术总是厚积薄发的,总是很多人做了很久,然后忽然一个突破。虽然取得突破的方法可能看起来全新,之前做的那么多东西都没用上,但是前面的积累想来也是必要的吧。XD

2012.6.20 在回程的大巴上

PCA的实现

PCA,全称是Principal component analysis,中文叫做主成分分析,是一种常用的数据处理手段。

直观的说,PCA是一种降维的手法。比如现在我们有1000个数据点,每个数据点是一个128维的向量,存储上可以是一个1000×128维的数组。经过PCA处理,我们仍然得到1000个数据点,但是每个数据点是一个小于128维的向量,比如我们用PCA将128维的数据降到64维。
PCA可以保证,在降维之后,数据表示的信息损失最小。

“损失最小”具体怎么定义?
还是以1000个128维的点为例,这1000个点,也就是1000个向量在一个128维的空间中。从在任何一维,也就是一个方向上来看,如果在这个方向上,各个向量大小差异很大,那么这个方向是很重要的。
也就是,反过来看,如果在某个方向上,每一个向量大小都很接近,那么如果不考虑这个方向,也就是去掉这一维的数据,对我们分析这1000个点并没有多大的影响。所以,“损失最小”对应着“差异最小”。

那么具体怎么做呢?

这里是两种常用的方法: SVD分解和EIG分解(特征值分解)。
共同点在于先从数据得到一个矩阵M,M的特征值个数对应着数据的维度,特征值越大那么对应的这一维越重要,也就是“差异越大”。

SVD分解, matlab

    sub_input_data = (input_data - repmat(mean(input_data),count,1))/sqrt(count-1);
    [U,S,V] = svd(sub_input_data);
    % First out_dim columns as PCA bases
    pcaV = V(:,1:out_dim);
    output_data = input_data * pcaV;

EIG分解, matlab

    mean_input_data = mean(input_data);
    sub_input_data = input_data - repmat(mean_input_data, count,1);
    mean_mat = sub_input_data' * sub_input_data ./ (count - 1);
    cov_mat = mean_mat;
    [V D] = eig(cov_mat);
    % Last out_dim columns as PCA bases
    pcaV = V(:,in_dim - out_dim + 1: in_dim);
    output_data = input_data * pcaV;

如果用C++的话,OpenCV本身就提供PCA,当然也可以自己实现。
OpenCV下可以用这个方法做EIG分解。

cv::eigen(covMat, eigenValues, eigenVectors);

具体Matlab代码也比较简单,放在Github上了。
MatlabPCA

原始数据:

PCA之后: