65.9K
CodeProject 正在变化。 阅读更多。
Home

使用PCA和并行优化实现EMGU多重人脸识别

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (111投票s)

2011年10月3日

CPOL

26分钟阅读

viewsIcon

1099879

使用EMGU执行主成分分析(PCA)实现多重人脸识别。通过.NET并行工具箱,在一个用户友好的应用程序中引入了实时分析和优化。

引言

本文旨在成为解释EMGU图像处理包装器使用方法的系列文章中的第一篇。有关EMGU包装器的更多信息,请访问EMGU网站。如果您是此包装器的新手,请参阅创建您的第一个EMGU图像处理项目文章。您可能会遇到三个关于引用未找到的警告。展开解决方案资源管理器中的References文件夹,删除带有黄色警告图标的三个引用,然后将位于Lib文件夹中的新引用添加进去。如果您以前使用过此包装器,请随意浏览EMGU代码参考页面上的其他示例。

人脸识别一直是图像处理领域的热门课题,本文基于Sergio Andrés Gutiérrez Rojas及其原创文章此处[^]的工作。人脸识别如此受欢迎的原因不仅在于其现实世界的应用,还在于主成分分析(PCA)的普遍使用。PCA是识别数据中统计模式的理想方法。人脸识别的流行在于用户可以轻松应用一种方法,并在不了解太多其工作原理的情况下查看其是否有效。

本文将更详细地探讨PCA分析及其应用,同时讨论并行处理的使用及其在图像分析中的未来。源代码在可用性和训练方式方面对原始源代码进行了一些关键改进,并利用并行架构进行多重人脸识别。

更新

最新版本V2.4.9使用更新的CascadeClassifier类来获取帧中的人脸位置,以及一个新的FaceRecognizer,它允许应用特征脸(Eigen)、费舍尔脸(Fisher)和局部二值模式直方图(LBPH)识别器。FaceRecognizer类中存在一个更改未知个体识别的错误,此问题已得到解决。

源代码要求

该程序设计用于网络摄像头,因此这是必不可少的。虽然程序应该在单核机器上执行,但请注意,使用顺序帧处理方法可能会提高性能。有关更多详细信息,请参阅“提高检测性能”部分。x86源代码也可以在x64机器上运行,但x64源代码仅适用于x64架构。

Face_Recognition/Main1.jpg

EMGU FaceRecognizer的工作原理

新的FaceRecognizer是一个全局构造函数,允许同时使用Eigen、Fisher和LBPH分类器。该类结合了分类器之间通用的方法调用。每种分类器类型的构造函数如下:

  • FaceRecognizer recognizer = new EigenFaceRecognizer(num_components, threshold);
  • FaceRecognizer recognizer = new FisherFaceRecognizer(num_components, threshold);
  • FaceRecognizer recognizer = new LBPHFaceRecognizer(radius, neighbors, grid_x, grid_y, threshold);

每个构造函数将在下面描述。请注意,Eigen识别器的分类阈值与Fisher和LBPH分类器的阈值不同。

特征分类器 (The Eigen Classifier)

Eigen识别器接受两个变量。第一个是为主成分分析保留的组件数量。对于良好的重建能力,没有规定应该保留多少组件。它基于您的输入数据,因此请尝试不同的数量。OpenCV文档建议保留80个组件几乎总是足够的。第二个变量设计为预测阈值;此变量包含一个bug,因为任何高于此值的值都被视为未知。对于Fisher和LBHP,这是未知数分类的方式,但是对于Eigen识别器,我们必须使用返回距离来提供我们自己的未知数测试。在Eigen识别器中,返回的值越大,我们越接近匹配。

为了以后允许我们设置阈值规则,我们将阈值设置为正无穷大,允许所有面部都被识别。

FaceRecognizer recognizer = new EigenFaceRecognizer(80, double.PositiveInfinity);

然后,我们在识别后检查特征距离返回,如果它高于我们在表单中设置的阈值,则将其识别,否则为未知。

public string Recognise(Image<Gray, Byte> Input_image, int Eigen_Thresh = -1)
{
    if (_IsTrained)
    {
        FaceRecognizer.PredictionResult ER = recognizer.Predict(Input_image);

.....
        //Only use the post threshold rule if we are using an Eigen Recognizer 
        //since Fisher and LBHP threshold set during the constructor will work correctly 
        switch (Recognizer_Type)
        {
            case ("EMGU.CV.EigenFaceRecognizer"):
                    if (Eigen_Distance > Eigen_threshold) return Eigen_label;
                    else return "Unknown";
            case ("EMGU.CV.LBPHFaceRecognizer"):
            case ("EMGU.CV.FisherFaceRecognizer"):
            default:
                    return Eigen_label; //the threshold set in training controls unknowns
        }
    }
......
}

Fisher 分类器

Fisher 识别器与 Eigen 构造函数一样接受两个变量。第一个是使用 Fisherfaces 准则为线性判别分析保留的组件数量。保留所有组件很有用,这意味着您的训练输入数量。如果将其保留为默认值 (0),设置为小于 0 或大于您的训练输入数量的值,它将自动设置为正确数量(您的训练输入 - 1)。第二个变量是未知值的阈值,如果结果 Eigen 距离高于此值,Predict() 方法将返回 -1 值,表示未知。此方法有效,阈值默认为 3500,更改此值以限制您希望结果的准确性。如果您更改构造函数中的值,识别器将需要重新训练。

FaceRecognizer recognizer = new FisherFaceRecognizer(0, 3500);//4000

与Eigen一样,您可以引入自己的规则,如下所示,尽管此代码版本(2.4.9)中未实例化,但提供了示例,以防您希望添加额外的表单控件来调整阈值设置。在构造函数中,阈值必须设置为`double.PositiveInfinity`。

//NOTE: This is not within V2.4.9 of the code....
public string Recognise(Image<Gray, Byte> Input_image, int Eigen_Thresh = -1)
{
    if (_IsTrained)
    {
        FaceRecognizer.PredictionResult ER = recognizer.Predict(Input_image);

.....
            //Only use the post threshold rule if we are using an Eigen Recognizer 
            //since Fisher and LBHP threshold set during the constructor will work correctly 
            switch (Recognizer_Type)
            {
                case ("EMGU.CV.EigenFaceRecognizer"):
                        if (Eigen_Distance > Eigen_threshold) return Eigen_label;
                        else return "Unknown";
                case ("EMGU.CV.FisherFaceRecognizer"):
                        //Note how the Eigen Distance 
                        //must be below the threshold unlike as above
if (Eigen_Distance < Fisher_threshold) return Eigen_label;
                        else return "Unknown";
                case ("EMGU.CV.LBPHFaceRecognizer"):
                default:
                        return Eigen_label; //the threshold set in training controls unknowns
            }
        }
......
    }

局部二值模式直方图(LBPH)分类器

LBPH 识别器与另外两个不同,它需要五个变量

  • radius – 用于构建圆形局部二值模式的半径。
  • neighbors – 用于从圆形局部二值模式构建的采样点数量。OpenCV文档建议值为“8”个采样点。请记住:包含的采样点越多,计算成本越高。
  • grid_x – 水平方向的单元格数量,8 是出版物中常用的值。单元格越多,网格越细,结果特征向量的维度越高。
  • grid_y – 垂直方向的单元格数量,8 是出版物中常用的值。单元格越多,网格越细,结果特征向量的维度越高。
  • threshold – 预测中应用的阈值。如果到最近邻居的距离大于阈值,此方法返回 -1

最后一个变量,阈值,其工作方式与 Fisher 方法相同。如果计算出的特征距离高于此值,则`Predict()`方法将返回一个`-1`值,表示未知。此方法有效,阈值默认为`100`,请更改此值以限制您想要结果的准确性。如果您更改构造函数中的值,则识别器将需要重新训练。

FaceRecognizer recognizer = new LBPHFaceRecognizer(1, 8, 8, 8, 100);//50

与Eigen一样,您可以引入自己的规则,如下所示,尽管此代码版本(2.4.9)中未实例化,但提供了示例,以防您希望添加额外的表单控件来调整阈值设置。在构造函数中,阈值必须设置为`double.PositiveInfinity`。

//NOTE: This is not within V2.4.9 of the code....
public string Recognise(Image<Gray, Byte> Input_image, int Eigen_Thresh = -1)
{
    if (_IsTrained)
    {
        FaceRecognizer.PredictionResult ER = recognizer.Predict(Input_image);

.....
            //Only use the post threshold rule if we are using an Eigen Recognizer 
            //since Fisher and LBHP threshold set during the constructor will work correctly 
            switch (Recognizer_Type)
            {
                case ("EMGU.CV.EigenFaceRecognizer"):
                        if (Eigen_Distance > Eigen_threshold) return Eigen_label;
                        else return "Unknown";
                case ("EMGU.CV.LBPHFaceRecognizer"):
                        //Note how the Eigen Distance must be 
                        //below the threshold unlike as above
                        if (Eigen_Distance < LBPH_threshold) return Eigen_label;
                        else return "Unknown";
                case ("EMGU.CV.FisherFaceRecognizer"):
                default:
                        return Eigen_label; //the threshold set in training controls unknowns
            }
    }
......
}

主成分分析 (PCA)

由于本文最初是为应用程序中的`EigenObjectRecognizer`类在人脸检测中编写的,因此本文的重点将继续放在新的`EigenFaceRecognizer`上。`EigenFaceRecognizer`应用PCA。`EigenFaceRecognizer`允许更轻松地应用`FisherFaceRecognizer`和`LBPHFaceRecognizer`。

FisherFaceRecognizer 应用 R.A. Fisher 推导的线性判别分析。LDA 找到一组人脸图像的子空间表示,定义该空间的基向量被称为 Fisherface。这可以产生比基于 PCA 的分析更好的结果,偏向分类而非表示。有关更多信息,请参阅这篇ScholarPedia 文章

LBPHFaceRecognizer使用局部二值模式(LBP)创建特征向量,用于支持向量机或其他机器学习算法。LBP统一了传统上不同的纹理分析统计和结构模型。由于其处理由光照变化引起的单调灰度变化的方式,LBP在实际应用中非常鲁棒。有关更多信息,请参阅这篇ScholarPedia文章

EigenFaceRecognizer 类对每张图像应用 PCA,其结果将是可训练神经网络识别的特征值数组。PCA 是一种常用的物体识别方法,因为如果使用得当,其结果可以相当准确且对噪声具有鲁棒性。PCA 应用的方法在不同阶段可能有所不同,因此将展示一种清晰的 PCA 应用方法。个人可以通过实验来找到从 PCA 产生准确结果的最佳方法。

为了执行 PCA,需要执行几个步骤:

  • 第 1 阶段:从每个变量中减去数据的均值(我们的调整后的数据)。
  • 第 2 阶段:计算并形成协方差矩阵。
  • 第 3 阶段:从协方差矩阵计算特征向量和特征值。
  • 第 4 阶段:选择一个特征向量(一个向量矩阵的花哨名称)。
  • 第 5 阶段:将转置的特征向量乘以转置的调整后的数据。

第一阶段:均值减法

这些数据相当简单,现在使我们的协方差矩阵计算变得稍微简单一些。这不是从每个值中减去总均值,因为对于协方差,我们至少需要两个维度的数据。实际上,它是从该行中的每个元素中减去该行的均值。

(或者,从列中的每个元素中减去每列的均值,但这将调整我们计算协方差矩阵的方式。)

第二阶段:协方差矩阵

二维数据的基本协方差方程是

Face_Recognition/EQ1.jpg

这与方差的公式相似,但是 x 的变化是相对于 y 的变化,而不是仅仅 x 相对于 x 的变化。在这个方程中,x 代表像素值,x̄ 是所有 x 值的均值,n 是值的总数。

由图像数据形成的协方差矩阵表示各个维度相对于彼此与均值的偏差程度。协方差矩阵的定义是:

Face_Recognition/EQ2.jpg

现在最简单的解释方式是一个例子,其中最简单的是一个3x3矩阵。

Face_Recognition/EQ3.jpg

现在对于更大的矩阵,这会变得更加复杂,计算算法的使用至关重要。

第三阶段:特征向量和特征值

特征值是矩阵乘法的产物,但它们是特殊情况。特征值是通过将协方差矩阵乘以二维空间中的向量(即Eigenvector)的倍数找到的。这使得协方差矩阵等同于变换矩阵。通过一个例子更容易说明:

Face_Recognition/EQ4.jpg

特征向量可以缩放,因此向量的½或x2仍将产生相同类型的结果。向量是一个方向,您所做的只是改变比例而不是方向。

Face_Recognition/EQ5.jpg

特征向量通常被缩放以使其长度为 1

Face_Recognition/EQ6.jpg

值得庆幸的是,寻找这些特殊的特征向量已为您完成,不会在此解释,但网上有几个教程可以解释其计算方法。

Eigenvalue 与所使用的 Eigenvector 密切相关,是原始向量在示例中被缩放的值,Eigenvalue4

第四阶段:特征向量

现在,通常特征值特征向量的结果不像上面示例中那样清晰。在大多数情况下,提供结果被缩放到长度为1。所以这里有一些使用 Matlab 计算的示例值:

Face_Recognition/EQ7.jpg

从协方差矩阵中找到特征向量后,下一步是按照特征值从高到低对它们进行排序。这为您提供了按重要性排序的组件。在这里,数据可以被压缩,较弱的向量被移除,从而产生一种有损压缩方法,丢失的数据被认为是无关紧要的。

Face_Recognition/EQ8.jpg

第五阶段:转置

PCA 的最后一步是获取特征向量矩阵的转置,并将其乘以转置的调整后数据集的左侧(调整后数据集来自第一阶段,其中从数据中减去了均值)。

EigenFaceRecognizer 类执行所有这些操作,然后将转置数据作为训练集输入到神经网络中。当它被传递一张图像以进行识别时,它执行 PCA 并将生成的 EigenvalueEigenvector 与训练集中的进行比较,然后神经网络生成一个匹配(如果找到)或一个负匹配(如果没有找到匹配)。这比这稍微复杂一些,但是神经网络的使用是一个复杂的主题,不是本文的目标。

源代码

训练“人脸识别器”

FaceRecognizer的训练在识别器的所有三个类成员之间保持一致。在代码中,这在程序开始时使用EigenFaceRecognizer作为默认值完成。在v2.4.9版本中,FaceRecognizer选择可以通过主窗体的“识别器类型”菜单选择进行。在添加或创建训练数据后进行的训练由该选择控制。从v2.4开始,引入了保存Eigen识别器以导出到其他应用程序的功能。然而,需要注意的是,您将需要原始训练数据来添加更多人脸。

Face_Recognition/Training.jpg

训练表单允许单独识别和添加人脸,因为程序设计为从网络摄像头运行,因此人脸以相同的方法识别。已包含一项功能,可获取10个成功的人脸分类,并将它们全部或单独添加到训练数据中。这增加了训练数据的收集,并且获取的图像数量可以在*Training Form.cs*的Variables区域中进行调整。增加或减少num_faces_to_aquire到任何首选值。

#region Variables            
....
    //For acquiring 10 images in a row
    List<Image<Gray, byte>> resultImages = new List<Image<Gray, byte>>();
    int results_list_pos = 0;

    int num_faces_to_aquire = 10;

    bool RECORD = false;
....
#endregion

包含了一个 `Classifier_Train` 类,它有两个构造函数,默认构造函数使用标准文件夹路径 `Application.StartupPath + "\\TrainedFaces"`,这也是训练数据的默认保存位置。如果您希望拥有不同的训练数据集,则另一个构造函数带有一个包含训练文件夹的 `string`。该程序只使用默认构造函数;它的目的是为了方便开发。该类的唯一设计目标是使 `Form` 代码更具可读性。要更改默认路径,必须更正以下函数:

//Forms
private bool save_training_data(Image face_data) //Training_Form.cs*
private void Delete_Data_BTN_Click(object sender, EventArgs e) //Training_Form.cs*

//Classes
public Classifier_Train() //Classifier_Train.cs

训练数据的存储

训练数据的默认位置在应用程序路径的*TrainedFaces*文件夹中。它有一个XAML文件,其中包含人员的Name标签和训练图像的文件名。XAML文件的结构如下:

<Faces_For_Training>
    <FACE>
    <NAME>NAME</NAME> 
    <FILE>face_NAME_2057798247.jpg</FILE> 
</FACE>
</Faces_For_Training>

这种结构可以很容易地更改以处理额外的数据或使用不同的布局。以下函数必须进行调整以适应额外的信息和根据需要添加的额外变量。

//Forms
private bool save_training_data(Image face_data) //Training_Form.cs*
//Classes
private bool LoadTrainingData(string Folder_loacation) //Classifier_Train.cs

每张图像都使用随机数保存,以便生成唯一的文件标识符。这可以防止图像被覆盖,并轻松地允许为一个个体获取和存储多张图像而不会出现问题。

Random rand = new Random();
bool file_create = true;
string facename = "face_" + NAME_PERSON.Text + "_" +    rand.Next().ToString() + ".jpg";
while (file_create)
{
    if (!File.Exists(Application.StartupPath + "/TrainedFaces/" + facename))
    {
        file_create = false;
    }
    else
    {
    facename = "face_" + NAME_PERSON.Text + "_" + rand.Next().ToString() + ".jpg";
    }
}

训练表单允许将数据添加到训练集,已经注意到此过程可能很慢。虽然更快速的方法是在打开和关闭时分别加载和写入所有数据,但此方法尚未包含。如果采取此类操作,则必须仔细考虑内存管理,以使训练图像的数量不会导致内存问题。

使用 JPEG 编码器存储图像,但可以更改为位图编码器以防止任何数据丢失,请参阅 *Traing_Form.cs* 文件中的以下函数:

//Saving The Data
private bool save_training_data(Image face_data)

private ImageCodecInfo GetEncoder(ImageFormat format)

保存和加载经过训练的EigenObjectRecognizer

要保存和加载不同的`FaceRecognizer`,只需使用“文件>识别器>”保存/加载选项。好处是识别器不再需要在每次加载程序时进行训练,它可以简单地读取以前训练过的数据。v2.4与v2.3保持兼容性,在启动时尝试读取*TrainedFaces*文件夹并重新训练识别器。

由于`FaceRecognizer`类包含自己的保存/加载方法,因此保存方式已从以前的版本中更改。保存方法只保存识别器类型相关的必要数据,并且没有内置方法可以从保存的数据中确定识别器类型。为了实现这一点,使用了三种文件扩展名,在加载数据时会检查这些文件扩展名以确定识别器类型。

  • EFR - EigenFaceRecognizer
  • FFR - FisherFaceRecognizer
  • LBPH - LBPHFaceRecognizer

重要的是要注意,识别器不再包含用于存储名称的`string`数组,而是生成一个整数,然后可以使用该整数对个人进行分类。因此,在训练和预测中使用列表来存储每个个人的`string`名称。然后使用`FaceRecognizer`类返回的整数从数组中的特定位置读取名称。同样,保存识别器也不会保存名称列表。为了允许保存所有数据,还会与识别器一起保存一个额外的“*Labels.xml*”文件来存储这些数据。

recognizer.Save(filename);

//save label data as this isn't saved with the network
string direct = Path.GetDirectoryName(filename);
FileStream Label_Data = File.OpenWrite(direct + "/Labels.xml");
using (XmlWriter writer = XmlWriter.Create(Label_Data))
{
    writer.WriteStartDocument();
    writer.WriteStartElement("Labels_For_Recognizer_sequential");
    for (int i = 0; i < Names_List.Count; i++)
    {
        writer.WriteStartElement("LABEL");
        writer.WriteElementString("POS", i.ToString());
        writer.WriteElementString("NAME", Names_List[i]);
        writer.WriteEndElement();
    }

    writer.WriteEndElement();
    writer.WriteEndDocument();
}
Label_Data.Close();

加载FaceRecognizer是通过使用内置的load方法实现的。识别器的文件扩展名用于确定FaceRecognizer所需的构造函数。在您自己的代码中,这不需要,因为您可能只使用一种识别器类型。除了识别器之外,还会加载“*Labels.xml*”文件,并填充包含识别器整数输出的string表示的列表。不过不用太担心,因为默认情况下,当您重新启动程序时,*TrainedFaces*文件夹中的数据仍会加载。

public void Load_Eigen_Recogniser(string filename)
{
    //Lets get the recogniser type from the file extension
    string ext = Path.GetExtension(filename);
    switch (ext)
    {
        case (".LBPH"):
            Recognizer_Type = "EMGU.CV.LBPHFaceRecognizer";
            recognizer = new LBPHFaceRecognizer(1, 8, 8, 8, 100);//50
            break;
        case (".FFR"):
            Recognizer_Type = "EMGU.CV.FisherFaceRecognizer";
            recognizer = new FisherFaceRecognizer(0, 3500);//4000
            break;
        case (".EFR"):
            Recognizer_Type = "EMGU.CV.EigenFaceRecognizer";
            recognizer = new EigenFaceRecognizer(80, double.PositiveInfinity);
            break;
    }

    //introduce error checking
    recognizer.Load(filename);

    //Now load the labels
    string direct = Path.GetDirectoryName(filename);
    Names_List.Clear();
    if (File.Exists(direct + "/Labels.xml"))
    {
        FileStream filestream = File.OpenRead(direct + "/Labels.xml");
        long filelength = filestream.Length;
        byte[] xmlBytes = new byte[filelength];
        filestream.Read(xmlBytes, 0, (int)filelength);
        filestream.Close();

        MemoryStream xmlStream = new MemoryStream(xmlBytes);

        using (XmlReader xmlreader = XmlTextReader.Create(xmlStream))
        {
            while (xmlreader.Read())
            {
                if (xmlreader.IsStartElement())
                {
                    switch (xmlreader.Name)
                    {
                        case "NAME":
                            if (xmlreader.Read())
                            {
                                Names_List.Add(xmlreader.Value.Trim());
                            }
                            break;
                    }
                }
            }
        }
        ContTrain = NumLabels;
    }
    _IsTrained = true;
}

如果您调整文件扩展名,除非您更正代码的加载部分,否则不会影响数据的保存和加载。

提高人脸识别准确性

如果您在不做任何修改的情况下运行程序,用自己的脸进行训练,然后引入另一张未经训练的脸,您会发现它会被识别为您。为了提高FaceRecognizer的准确性,已采取更严格的方法。用于控制未知人脸的阈值,无论是在构造函数中还是在EigenFaceRecognizer中根据计算距离,都可以进行调整以获得更好的准确性。对于EigenFaceRecognizer,这可以通过在表单中更改“未知阈值”文本框中的值来完成。默认值为2000,但将其增加到例如5000将意味着它不太可能产生错误的匹配。然而,过高的话您可能永远无法获得匹配,eigenDistance与人脸一起显示在右侧面板中,以方便校准。

`FisherFaceRecognizer`和`LBPHFaceRecognizer`在构造函数中设置了阈值。可以在表单中添加额外的控件以允许训练后校准。这在“EMGU FaceRecognizer如何工作”一节中讨论。

直方图均衡化也用于提高准确性,这会产生更均匀的图像,对光照变化更具弹性。还可以采用其他方法来生成独特的训练集。您可以只提取眼睛和嘴巴特征,连接数据并使用它,但这需要进一步的实验。在版本2.4.9中,人脸数据居中到其检测到的位置,以消除可能影响先前版本结果的背景噪声。

result = currentFrame.Copy(face_found.rect).Convert<Gray, 
       byte>().Resize(100, 100, Emgu.CV.CvEnum.INTER.CV_INTER_CUBIC);
result._EqualizeHist();

您还可以尝试更大的训练图像,两种形式的以下代码将人脸大小调整为100x100的图像,增加此值可以提高准确性。但请注意,两个出现的地方都必须更改,并且图像越大,所需的训练和识别时间越长。

result = currentFrame.Copy(face_found.rect).Convert<Gray, 
  byte>().Resize(100, 100, Emgu.CV.CvEnum.INTER.CV_INTER_CUBIC); //Training Form.cs* 
                                                                 //& Main Form.cs*

当使用小型数据集(例如识别自己)时,您应该对训练数据进行偏移。为了帮助分类未知对象并减少错误识别,您还可以向识别器提供一些训练数据,其中包含按名称识别为“未知”的个体。代码中提供了一个包含五人图像的“*Group_Photo_Unknowns.pdf*”。将其打印在一张A4纸上,并使用名称“Unknown”对其中四个可检测到的人进行识别器训练。虽然这些人将被识别为名为“*Unknown*”的个体,但他们将通过提供4组PCA特征来偏移您自己的训练数据,这些特征应该代表未知个体。这是一种常见的做法,可以增强您的数据集,减少误报的可能性,因为未知个体与照片中的人匹配特征的几率更大。

检测未知人脸

正如评论中讨论的,这以前是通过编辑 EMGU 的源代码 2.3.0 来访问 `Eigen 距离` 变量来实现的。EMGU v2.4.2 通过引入 `RecognitionResult` 类来存储三个变量,从而使此功能可访问。可用的变量为人脸标签、Eigen 距离和索引。V2.4.2 允许您设置一个 `Eigen_threshold`,这样如果 Eigen 距离不大于此值,则将返回 `null` 结果。此版本代码中已解决了与此相同的 bug。`classifier_Train.cs` 类中有一个方法可以在只使用 `EigenFaceRecognizer` 时设置 Eigen 阈值。`FisherFaceRecognizer` 和 `LBPHFaceRecognizer` 允许在构造函数中正确设置阈值。

主窗体包含一个用于校准阈值值的阈值框。此值取决于您的训练数据的大小。为了便于校准,识别时 Eigen 距离会打印在名称旁边。`recognise` 方法检查 Eigen 距离是否大于设定的阈值。如果不是,则返回“未知”标签,而不是数据库中最后一个人的姓名。

public string Recognise(Image<Gray, Byte>  Input_image, int Eigen_Thresh = -1)
{
    if (_IsTrained)
    {
        FaceRecognizer.PredictionResult ER = recognizer.Predict(Input_image);

        if (ER.Label == -1)
        {
            Eigen_label = "Unknown";
            Eigen_Distance = 0;
            return Eigen_label;
        }
        else
        {
            Eigen_label = Names_List[ER.Label];
            Eigen_Distance = (float)ER.Distance;
            if (Eigen_Thresh > -1) Eigen_threshold = Eigen_Thresh;

            //Only use the post threshold rule if we are using an Eigen Recognizer 
            //since Fisher and LBHP threshold set during the constructor will work correctly 
            switch (Recognizer_Type)
            {
                case ("EMGU.CV.EigenFaceRecognizer"):
                        if (Eigen_Distance > Eigen_threshold) return Eigen_label;
                        else return "Unknown";
                case ("EMGU.CV.LBPHFaceRecognizer"):
                case ("EMGU.CV.FisherFaceRecognizer"):
                default:
                        return Eigen_label; //the threshold set in training controls unknowns
            }
        }
    }
    else return "";
}

提高检测性能

该程序旨在提高现代机器的性能,而不是专注于提高较慢处理器的性能。虽然实时图像处理是人们所期望的,但通常不切实际。实时处理与图像处理算法的准确性密切相关。更快的算法是处理更少数据的结果,它们区分真假数据的能力本质上存在缺陷。在视频采集中,每秒30帧被认为是标准,它比我们眼睛能够处理的速度更快,因此提供了平滑的运动。在实际应用中,这对于计算机来说太慢而无法准确。

现代高速摄像机能够以 300 帧/秒的速度采集 640 x 480 图像,令标准网络摄像头相形见绌。这些高端摄像机使用专门的帧捕获器来处理图像采集。普通用户不太可能遇到如此高的速度,最多可能达到 60 帧/秒,但重要的是这些摄像机实时进行图像预处理的方式。帧捕获器将 FPGA(现场可编程门阵列)芯片集成到板卡上。这些芯片负责生成图像,同时还可以执行直方图均衡化和边缘检测等处理器功能。要了解如何实现这一点,重要的是要指出 FPGA 芯片是什么及其架构。

简单来说,FPGA 是一种高端处理器,可以设计用于执行特定操作,如果您有智能手机,那么您认为是什么在运行它(ARM 处理器是一种高级 FPGA)。在计算机上,您可以设计一个运行 Word,另一个运行浏览器,另一个运行游戏。显然,复杂性和实用性阻止了这一点。FPGA 处理器可以设计成具有极其并行的架构。因此,在执行边缘检测的同时,您也可以执行对象识别。

虽然FPGA的使用超出了本文的范围,但可以构建用于图像处理的并行架构。许多Visual Studio用户以前都遇到过线程应用程序。这是数据处理在计算机核心之间分配的地方。这过去很复杂,需要大量的经验,但微软已经投入了大量时间进行并行计算。这是一个指向主页的链接:http://msdn.microsoft.com/en-us/concurrency/default。Visual Studio的程序员们开发了一套类,可以并行化您使用的几乎任何循环。性能提升取决于您的机器和物理核心的数量。一个8核i7只有4个物理核心,平均性能提升x3.7。

不要急于将所有内容都并行化。它的使用时好时坏,必须对其性能进行检查。它的使用可能会增加执行时间,并且很容易耗尽计算机上的所有内存。它还取决于您正在运行的其他应用程序。

记住两件事

  1. 想想计算机是如何工作的,如果您只进行少量处理,计算机必须共享所有资源,然后告诉每个处理器要处理什么。然后它必须收集结果,处理它们,并重复该过程,直到您的循环耗尽。有时,让一个处理器处理信息可能会更快。秒表是您在这里的朋友,计时两个实例,看看会发生什么。还要记住您的机器并非所有人的机器,您可能有8个核心,但您的最终用户可能仍然只有一个核心。
  2. 几个简单的规则是,每个处理器在运行时都不会查看其他处理器正在做什么。不要在并行循环中使用并行循环,因为这会阻碍性能。不要设置一个任务,其中第二个循环的输出依赖于第一个循环,否则它将不起作用。类似地,如果记录的结果依赖于每个循环,也会发生错误。像下面这样不依赖的操作是您的朋友。任何并行循环有时都可能出现错误,因此保留`try catch`语句很有用。
//variables
Int Count = 0
Image<Gray,Byte> Image_Blank_Copy = My_Image.CopyBlank();
...

Parallel.For....

    Count += 10;
    Count -= 10;
    Count++;
    
    //or for an image
    Image_Blank_Copy.Data[y,x,0] += 10;

此外,请注意,在图像中设置 ROI 然后在循环中使用它比简单地将该区域复制到新图像并进行处理要慢得多,例如:

//Bad and Slow
My_Image.ROI = new Rectangle(0,0,100,100);
Parallel.For(0, My_Image.Width, (int i) =>
{
    for (int j = 0; j < My_Image.Height; j++)
    {
        //Do something
    }
});

//Good and Fast
My_Image.ROI = new Rectangle(0,0,100,100);

Using(Image<Bgr,Byte> tempory_image = My_Image.Copy())
{
    Parallel.For(0, tempory_image.Width, (int i) =>
    {
        for (int j = 0; j < tempory_image.Height; j++)
        {
            //Do something
        }
    });
}

要访问Parallel.ForParallel.ForEachTaskThreadPool,您需要添加以下using语句:

using System.Threading;
using System.Threading.Tasks;

在源代码中,提供了一个并行 `foreach` 循环。这意味着每个面部都使用单独的线程进行识别。因此,对于检测到的每个面部,信息都独立传递给识别器进行分类。虽然在一个面部上的性能并不明显,但如果房间里有多个人,每个人都可以独立识别。如果您使用大量训练数据,这非常有用,因为 `EigenObjectRecognizer` 输出的可能性越多,准确分类所需的时间就越长。使用 `try catch` 语句来过滤可能零星发生的错误,但这不影响性能或准确性。

{
    try
    {
        facesDetected[i].X += (int)(facesDetected[i].Height * 0.15);
        facesDetected[i].Y += (int)(facesDetected[i].Width * 0.22);
        facesDetected[i].Height -= (int)(facesDetected[i].Height * 0.3);
        facesDetected[i].Width -= (int)(facesDetected[i].Width * 0.35);
        result = currentFrame.Copy(facesDetected[i]).Convert<Gray, 
          Byte> ().Resize(100, 100, Emgu.CV.CvEnum.INTER.CV_INTER_CUBIC);
        result._EqualizeHist(); //draw the face detected in the 0th (gray) 
                                //channel with blue color
        currentFrame.Draw(facesDetected[i], new Bgr(Color.Red), 2);
        if (Eigen_Recog.IsTrained)
        {
            string name = Eigen_Recog.Recognise(result);
            int match_value = (int)Eigen_Recog.Get_Eigen_Distance;
            //Draw the label for each face detected and recognized

            currentFrame.Draw(name + " ", ref font, 
              new Point(facesDetected[i].X - 2, facesDetected[i].Y - 2), 
              new Bgr(Color.LightGreen)); ADD_Face_Found(result, name, match_value);
        }
    }
    catch
    {
        //do nothing as parallel loop buggy
        //No action as the error is useless, it is simply an error in
        //no data being there to process and this occurs sporadically
    }
});

性能提升不止于此。在主程序中,右侧面板显示了最近检测到的5张人脸。这些控件是以编程方式并行创建和显示的。由于这是在并行循环中完成的,因此每个变量必须独立于循环中的操作。重要的函数如下所示,您会注意到每个组件的位置由变量`faces_panel_X`,`faces_panel_Y`控制,对这些变量的任何操作都是独立的,并根据其当前值进行。

void Clear_Faces_Found()
void ADD_Face_Found(Image<Gray, Byte> img_found, string name_person)
{
    ...
    PI.Location = new Point(faces_panel_X, faces_panel_Y);
    ...
    LB.Location = new Point(faces_panel_X, faces_panel_Y + 80);
    ...
      
    faces_count++;
    if (faces_count == 2)
    {
        faces_panel_X = 0;
        faces_panel_Y += 100;
        faces_count = 0;
    }
    else faces_panel_X += 85;
    ...
}

您可以通过调整控制面板何时清除来控制显示的人脸数量。由于每个面都有一个`picturebox`和一个标签,因此您必须将人脸数量乘以二。在这种情况下,10/2 = 5张人脸和名称。

if (Faces_Found_Panel.Controls.Count > 10)

并行和顺序执行之间的切换 v2.4

为了便于比较,现在提供了一个菜单选项,可以在并行和顺序处理之间切换,而无需退出程序。有关更多详细信息,请参见下文“并行和顺序执行之间的切换 v2.3”。

并行和顺序执行之间的切换 v2.3

由于用户可能希望调查性能提升,因此并行处理人脸识别和顺序处理功能都已包含在内。默认方法是并行方法。在*Main Form.cs*代码中,您将看到两个函数:

//Process Frame
void FrameGrabber_Standard(object sender, EventArgs e)  //This is the Sequential
void FrameGrabber_Parrellel(object sender, EventArgs e) //and this the Parallel 

其中一个函数的使用由*Main Form.cs*中的摄像头StartStop函数控制。

//Camera Start Stop
public void initialise_capture()
{
    grabber = new Capture();
    grabber.QueryFrame();
    //Initialize the FrameGraber event
    Application.Idle += new EventHandler(FrameGrabber_Parrellel);
}
private void stop_capture()
{
    Application.Idle -= new EventHandler(FrameGrabber_Parrellel);
    if(grabber!= null)
    {
        grabber.Dispose();
    }
}

您必须更改以下两行代码,并将它们重定向到顺序函数。

Application.Idle += new EventHandler(FrameGrabber_Parrellel); //initialise Capture
Application.Idle -= new EventHandler(FrameGrabber_Parrellel); //Stop Capture

//becomes

Application.Idle += new EventHandler(FrameGrabber_Standard);  //initialise Capture
Application.Idle -= new EventHandler(FrameGrabber_Standard);  //Stop Capture

图像处理代码的并行化很重要,而不仅仅是线程化代码。它很容易实现,但通过实践,可以很好地实现。EMGU中还有一个新来者,它利用CUDA图形处理。这个主题更高级,不会涉及,因为不是每个人都有启用CUDA的显卡,但基本原理是它允许更高程度的并行化,因为您可以处理数百个核心,而不是4个或8个核心,很容易推测执行时间可以做出的改进。EMGU附带的`PedestrianDetection`示例展示了如何实现这一点。

结论

本文在解释 PCA 原理的同时,介绍了通过并行化缩短执行时间这一重要课题。尽管该软件和文章展示了其实现的微小示例,但其重要性必须引起注意。随着多核处理器和基于 CUDA 的显卡日益普及,实时图像处理变得更易于实现。不再需要先进的微电子技术来加速更简单的图像处理系统,并且开发时间的缩短使得更多人可以使用图像处理。

如果您觉得本文有需要改进或更正的地方,请发表评论,我们将予以处理。

旧版本

SourceForge

致谢

非常感谢亚利桑那大学矿物学和晶体学系2006年研究小组的所有成员,他们的合影被用于演示目的。http://www.geo.arizona.edu/xtal/group/group2006.htm

如果您不希望您的图像被使用,请发表评论或联系我,我将删除所有相关引用。

感谢Sergio Andrés Gutiérrez Rojas的代码启发了本文。希望您在图像处理方面继续取得优异成绩。

历史

  1. 直接链接应用于通过kiwi6.com的外部文件托管网站。如果这些链接失效,请见谅,code project不允许托管如此大的文件。为防止停机,还提供了Sourceforge链接。所有文件现已托管在 Sourceforge 上。
  2. 直接链接设置为在新窗口中打开,因为现在热链接会重定向流量。将寻找新的主机。x64位版本现已托管在私人网站上,仍在为更大的x86版本寻找主机。新版本同时运行x86/x64代码
  3. 上传了版本2.4,允许检测未识别的面部。包括识别器变量的保存/加载。代码更新已包含在文章中。
  4. 版本 2.9.4 已更新为新的 `FaceRecognizer` 类,允许应用特征脸(Eigen)、Fisher 和 LBHP 识别器。使用 Haar 进行的人脸检测已使用新的方法调用进行更新。保存/加载方法已调整以适应新的 `FaceRecognizer` 类,因此文章也进行了显著更新。
© . All rights reserved.