3D人脸查看器和匹配器

610 篇文章 16 订阅
405 篇文章 53 订阅

目录

背景

技术

Mesh 文件

人脸图像

面部拟合

相机,灯光,动作

用户界面

代码高亮

演示

注意

更新

4.0版

Face Matcher的代码高亮

创建FaceServiceClient对象

将图像文件异步加载到后端服务器进行人脸检测

异步人脸匹配


3.0版的最新源代码也可以从GitHub - kwyangyh/ThreeDFace: 3D Face Viewer下载。

 

背景

人脸识别是目前最流行的生物特征识别方法之一。人脸捕捉就像拍摄人脸照片一样简单。然而,为了使面部照片对面部识别有用,必须满足某些面部图像规范。

  • 脸部必须在正面视图中。
  • 图像不得不均匀地拉伸。
  • 脸部应该被均匀照亮。
  • 受试者必须处于中性表情状态,睁眼闭嘴。

符合这些规格的照片将有资格作为合适的可注册照片。为了与自动面部识别系统一起使用,这些照片将成为面部特征文件生成的来源。特征文件大多是源照片独有的,并存储可用于比较的摘要面部特征

当两个特征文件匹配时,原始照片和拍摄照片的主体很可能是同一个人。

面部识别系统面临的主要挑战之一是难以获得好的正面图像进行匹配。相机应放置在适当的位置,以获得拍摄对象的完整正面视图。然而,人们的身高各不相同,他们在镜头前看起来略微偏离正面的倾向也有所不同。相机可能会略微侧向、自上而下、自下而上或以某个角度看,这样拍摄的照片就不会是理想的全正面。

根据人脸识别系统,系统可以在生成特征文件之前进行内部自动校正,但匹配分数会受到影响。

另一个问题是照明。通常,大多数注册照片的质量都相当好,因为这些照片是在大多数受控环境中拍摄的,例如在专门的照相亭中。在大多数情况下,为匹配而拍摄的照片是在照明条件可能发生变化的环境中拍摄的,例如窗户附近的面部门禁单元。

为了测试面部识别系统的准确性和可靠性,需要在不同角度和光照条件下拍摄对象的测试用例。这些图像将用于针对同一主题的受控图像进行测试。这是一个耗时且乏味的过程,需要专门的测试对象的积极参与,或者,我们需要从实时系统中捕获图像。

本文的想法背后的动机是提出可以从单个面部图像重新创建的测试用例,改变照明条件和相机角度。

技术

即使使用复杂的商业工具,创建逼真的3D面部模型也并非易事。

随着Kinect X-Box One的发布,我们找到了正确的技术,可以轻松创建逼真的3D面部模型Kinect 2.0传感器是一种高度精密的设备。它有一个1920X1080全高清分辨率的摄像头,可以捕捉到质量相当好的图像。还有一个红外深度传感器,可以输出512X424分辨率的深度图像。这个深度传感器的深度信息可能是目前市面上最好的。基本上,这些是来自每个相机/传感器的唯一原始帧(每帧每秒30帧)。但是,还有其他可用的计算帧(也是每秒30帧)。这些是Body IndexBodyFace BasicHDFace框架。

特别有趣的是HDFace框架。这些是跟踪面部的3D世界坐标。每个面有1347个顶点。连接起来的3个点将构成一个3D三角形表面。Kinect 2.0使用一组包含2630个三角形的标准三角形索引参考。使用2630曲面,人脸模型确实很逼真。

HDFace框架创建的3D模型可以由.NET System.Windows.Media.Media3D类渲染,MeshGeometry3D用于建模,Viewport3DWPF窗口中查看,PerspectiveCameraAmbientLight以及DirectionalLight在各种灯光和视角下渲染模型。

AForge .NET类提供过滤器来处理源图像,改变亮度和对比度。

OpenCV提供HaarClassifier从输入图像中查找面部和眼睛的功能。

System.Drawing .NET类用于GDI+图像操作,例如拉伸和旋转。

System.Windows.Media类用于在WPF窗口中进行演示。

Mesh 文件

 

3D图像处理类GeometryModel3D2个基本组件组成:

  • GeometryModel3D.Geometry
  • GeometryModel3D.Material

GeometryModel3D.Geometry类需要定义以下信息:

  • Position
  • TriangleIndices
  • TextureCoordinates

Position由在3D世界坐标中定义的顶点组成。每个顶点都按它们在Position vertices集合中的顺序来引用。例如,要定义一个立方体,我们需要8个顶点,一个在立方体的每个角。我们将坐标输入到Position集合的顺序很重要。

Positions="-0.05,-0.1,0 0.05,-0.1,0 -0.05,0,0 0.05,0,0 -0.05,-0.1,
           -0.1 0.05,-0.1,-0.1 -0.05,0,-0.1 0.05,0,-0.1"

在上面的示例中,-0.05,-0.1,0是第一个顶点,并且将具有索引0

TriangleIndices指以3个为一组的索引列表,它们定义了3D model

TriangleIndices="0,1,2 1,3,2 0,2,4 2,6,4 2,3,6 3,7,6 3,1,5 3,5,7 0,5,1 0,4,5 6,5,4 6,7,5"/>

在上面的例子中,我们定义了16个三角形,每个三角形有3个顶点,共同定义了立方体的所有表面。前3个索引0,1,2指的是索引为012Position顶点。这将在3D model中形成一个表面。

对于要渲染的表面,我们可以使用该GeometryModel3D.Material类定义绘画brush。但是brush需要知道将什么texture/color应用于表面。这就是TextureCoordinate的作用。

TextureCoordinates="1,1  0,0  0,0 1,1 1,1  0,0  0,0  1,1"

上面有8个坐标,每个坐标都应用于立方体的一个顶点。第一个坐标是(1,1)。这如何定义一个 color texture?这些数字只有当我们参考brush来绘制表面时才有意义。

对于这个例子,我们将参考一个LinearGradientBrush。这个brush允许定义从StartPointEndPoint的梯度。

<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
    <GradientStop Color="AliceBlue" Offset="0" />
    <GradientStop Color="DarkBlue" Offset="1" />
</LinearGradientBrush>

在上面的示例中,想象一个1单位乘1单位的区域。两个对角将具有坐标(0,0)(1,1)0表示颜色AliceBlue1 表示颜色DarkBlue。中间的任何数字都是这两种颜色之间的阴影。以这种方式定义颜色可以让我们得到一个连续的渐变图1X1区域中的每个点都将映射到定义的颜色。

回到立方体顶点索引0TextureCoordinate(1,1),基于LinearGradientBrush刷,颜色将是DarkBlue 

同样的,我们可以有一个ImageBrushImageBrush会有一个image source图像源的图像将用于定义纹理/颜色。同样,坐标的值范围是(0,0)(1,1)。例如,如果图像大小为1920X1080(0,0)则将引用图像上的点(0,0)并且(1,1)引用图像的右下角的(1920-1, 1080-1)点。TextureCoordinate (0.5,0.5)将映射到(0.5X1920-1, 0.5X1080-1)。通过这种方式,我们将能够获得TextureCoordinate范围内的任何图像点(0,0) -(1,1)

面部模型的网格文件由1347 Position顶点组成,后面是1347 TextureCoordinates点。纹理文件是一个1920X1080的图像,将用作渲染3D modelImageBrush图像源。

定义面部模型中所有表面的三角形索引对于Kinect 2.0生成的所有面部模型都是相同的。我已将这些索引包含在文件tri_index.txt中。7890个索引定义了2630个表面。

还有另一个文件列出了这些面部点的图像坐标:

  • RightEye
  • LeftEye
  • Nose
  • Mouth
  • Chin

这些文件中的信息均来自使用Kinect 2.0 sensorKinect 2.0 SDK API生成的数据。在本文中,我不会涉及Kinect特定领域。如果您有兴趣,请参考Kinect 2.0 SDK HDFace中的示例。

这些HDFace点没有很好的记录,可能是因为它们太多了。1347个点中的每一个点命名并不容易。但是,根据我的调查,我们感兴趣的面部点是:

  • 328-1105(右眼在这2点之间)
  • 883-1092(左眼在这两点之间)
  • 10(上唇根部中心)
  • 14(鼻托)
  • 0(下巴)

人脸图像

 

本文背后的想法基于Kinect 2.0 SDK HDFace示例。在该示例中,HDFace帧和Color帧被同步处理。使用Kinect 2.0 Coordinate Mapper,每个HDFace点都可以映射到Color帧中对应的颜色坐标,并且使用Color帧作为ImageBrushimage source, TextureCordinate可以准确地在运行中生成。

我拍摄了一组同步HDFaceColor帧的快照,记录了生成的TextureCoordinate和标准的TriangleIndices。这将提供我在没有Kinect 2.0 sensor的情况下复制3D face model所需的所有信息。

但是,该模型只能用于保存的特定纹理(Color帧)。Color帧是1920X1080,但是人脸图像只占据了大约500X500的面积。如果我们可以用另一个人脸替换那个区域,我们可能会在3D人脸模型上渲染那个人脸!

这就像戴上口罩。但必须准确安装。

为了精确替换面部区域,我们需要知道该面部的方向。它是向侧面看,向上还是向下看,眼睛是否水平且张开,嘴巴是否闭合?要获得相同方向的替换面将很困难。

我们需要一个standard人脸图像。为每个3D 模型记录的原始Color帧具有标准规格中的面部。本质上,它与身份证照片采用的标准相同:全脸正面,中性表情,睁眼,闭嘴。图像分辨率应保持在400X400800X800左右。

对于替换面,我们需要遵守相同的标准。

尽管如此,在版本3.0中,我们可以使用自定义网格处理非正面输入(请参阅本文末尾的更新部分)。

面部拟合

 

不合适

 

合适

原始纹理为1920X1080。面部图像可能位于中心的某处。为了替换那个人脸,我们需要将新人脸锚定在一些不变的点上。选择的点应位于面部的主要特征区域。大多数面部识别系统将眼睛、鼻子和嘴巴定义为重要特征。有些还包括脸颊和眉毛。

对于这篇文章,我确定了5个点:右眼左眼鼻根上唇根下巴

新面孔不会同样适合每个面孔模型。我们需要一种计算拟合优度的方法。最佳拟合是水平和垂直均匀拉伸后5个点全部对齐(即放大变换)。如果我们有足够的人脸模型,我们也许能够找到一个或多个适合的理想人脸模型。只有6面部模型,我们可能无法获得理想的拟合。

人脸拟合算法:

  1. 在参考(原始人脸)图像中找到两只眼睛之间的距离。
  2. 在新的人脸图像中找到两只眼睛之间的距离。
  3. 找到参考图像的鼻根到眼睛中点的距离。
  4. 找到新图像的鼻根到眼睛中点的距离。
  5. 通过将1)除以2)获得的因子水平拉伸新图像。
  6. 通过将3)除以4)获得的因子垂直拉伸新图像。
  7. 对于新的拉伸图像,找到从鼻子基部到上唇基部的垂直距离。
  8. 对于参考图像,找到从鼻基到上唇基的垂直距离。
  9. 对于新图像,从鼻子底部垂直拉伸(或压缩)通过将8)除以7)获得的因子。
  10. 现在嘴巴会对齐。对于新的重新拉伸图像,找到从上唇基部到下巴的垂直距离。
  11. 对于参考图像,找到从上唇基部到下巴的垂直距离。
  12. 最后一步:从上唇底部垂直向下拉伸(或压缩),乘以11)除以12)获得的系数。

现在所有面部的点都将对齐

但是对于某些人脸模型,生成的新图像可能会明显变形,请参见上面标有Bad Fit的图。

对于拟合优度的测量,我设计了一种基于拉伸因子的方法。涉及4拉伸因素

  1. factor1=眼对眼
  2. factor2=鼻根到眼睛中点
  3. factor3=鼻根到上唇根
  4. factor4=上唇根到下巴

对于1)2),我们希望这些值尽可能相似,我们计算绝对比率(factor1-factor2)/(factor1)。我们称之为眼鼻误差

对于3),我们希望保持因子尽可能接近1.00,我们使用绝对比率(factor3-1)。姑且称之为鼻口误差吧。

对于4),我们还希望使因子尽可能接近 1.00,我们使用绝对比率(factor4-1)。我们称之为嘴唇误差

由于眼鼻拉伸是在整个面部进行的,因此它被分配了更高的weightage。类似地,-拉伸涉及从鼻子向下拉伸,而-下巴拉伸仅涉及从嘴向下拉伸,分配的误差weightage小于-误差,但大于-下巴误差。

目前的权重是4用于eye-nose2用于nose-mouth1用于mouth-chin

相机,灯光,动作

 

1:设置

 

2:点亮一个立方体

 

3:使用Mesh Translation查看更改

 

4:使用相机旋转查看更改

设置Viewport3DXaml标记代码:

<Viewport3D  HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
 Width="Auto" Height="Auto" x:Name="viewport3d" RenderTransformOrigin="0.5,0.5"
 MouseDown="viewport3d_MouseDown" MouseRightButtonDown="viewport3d_MouseRightButtonDown" >
    <Viewport3D.RenderTransform>
        <ScaleTransform ScaleX="1" ScaleY="1"/>
    </Viewport3D.RenderTransform>
    <!-- Defines the camera used to view the 3D object. -->
    <Viewport3D.Camera>
        <!--<PerspectiveCamera Position="0.0, 0.0, 0.45" LookDirection="0,0, -1"
             UpDirection="0,1,0" FieldOfView="70" />-->
        <PerspectiveCamera
            Position = "0, -0.08, 0.5"
            LookDirection = "0, 0, -1"
            UpDirection = "0, 1, 0"
            FieldOfView = "70">
            <PerspectiveCamera.Transform>
                <Transform3DGroup>
                    <RotateTransform3D>
                        <RotateTransform3D.Rotation>
                            <AxisAngleRotation3D
                                Axis="0 1 0"
                                Angle="{Binding Value, ElementName=hscroll}" />
                        </RotateTransform3D.Rotation>
                    </RotateTransform3D>
                    <RotateTransform3D>
                        <RotateTransform3D.Rotation>
                            <AxisAngleRotation3D
                                Axis="1 0 0"
                                Angle="{Binding Value, ElementName=vscroll}" />
                        </RotateTransform3D.Rotation>
                    </RotateTransform3D>
                    <RotateTransform3D>
                        <RotateTransform3D.Rotation>
                            <AxisAngleRotation3D
                                Axis="0 0 1"
                                Angle="{Binding Value, ElementName=vscrollz}" />
                        </RotateTransform3D.Rotation>
                    </RotateTransform3D>

                </Transform3DGroup>
            </PerspectiveCamera.Transform>

        </PerspectiveCamera>
    </Viewport3D.Camera>

    <!-- The ModelVisual3D children contain the 3D models -->
    <!-- This ModelVisual3D defines the light cast in the scene. Without light, the 3D
       object cannot be seen. Also, the direction of the lights affect shadowing.
       If desired, you can create multiple lights with different colors
       that shine from different directions. -->
    <ModelVisual3D>
        <ModelVisual3D.Content>
            <Model3DGroup>
                <AmbientLight x:Name="amlight" Color="White"/>
                <!--<DirectionalLight x:Name="dirlight" Color ="Black"
                     Direction="1,-2,-3" />-->
                <DirectionalLight x:Name="dirlight" Color="White" Direction="0,0,-0.5" >
                    <DirectionalLight.Transform>
                        <Transform3DGroup>
                            <TranslateTransform3D OffsetZ="0" OffsetX="0" OffsetY="0"/>
                            <ScaleTransform3D ScaleZ="1" ScaleY="1" ScaleX="1"/>
                            <TranslateTransform3D OffsetZ="0" OffsetX="0" OffsetY="0"/>
                            <TranslateTransform3D OffsetY="-0.042"
                             OffsetX="0.469" OffsetZ="-0.103"/>
                        </Transform3DGroup>
                    </DirectionalLight.Transform>
                </DirectionalLight>
            </Model3DGroup>
        </ModelVisual3D.Content>
    </ModelVisual3D>
    <ModelVisual3D>
        <ModelVisual3D.Content>
            <GeometryModel3D>

                <!-- The geometry specifies the shape of the 3D plane.
                     In this sample, a flat sheet is created. -->
                <GeometryModel3D.Geometry>
                    <MeshGeometry3D x:Name="theGeometry"
                        Positions="-0.05,-0.1,0 0.05,-0.1,0 -0.05,0,0 0.05,
                        0,0 -0.05,-0.1,-0.1 0.05,-0.1,-0.1 -0.05,0,-0.1 0.05,0,-0.1"
                        TextureCoordinates="0,1 1,1 0,0 1,0 0,0 1,0 0,1 1,1"
                        TriangleIndices="0,1,2 1,3,2 0,2,4 2,6,4 2,3,6 3,7,6 3,1,
                                         5 3,5,7 0,5,1 0,4,5 6,5,4 6,7,5"/>
                </GeometryModel3D.Geometry>

                <!-- The material specifies the material applied to the 3D object.
                     In this sample a linear gradient
                     covers the surface of the 3D object.-->
                <GeometryModel3D.Material>
                    <MaterialGroup>
                        <DiffuseMaterial x:Name="theMaterial">
                            <DiffuseMaterial.Brush>
                                <LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
                                    <GradientStop Color="AliceBlue" Offset="0" />
                                    <GradientStop Color="DarkBlue" Offset="1" />
                                </LinearGradientBrush>
                            </DiffuseMaterial.Brush>
                        </DiffuseMaterial>
                    </MaterialGroup>
                </GeometryModel3D.Material>

            </GeometryModel3D>
        </ModelVisual3D.Content>
    </ModelVisual3D>
</Viewport3D>

Viewport3D包含以下元素:

  • Viewport3D.Camera
  • ModelVisual3D

Viewport3D.Camera包含PerspectiveCamera。相机规格如下:

<PerspectiveCamera
    Position = "0, -0.08, 0.5"
    LookDirection = "0, 0, -1"
    UpDirection = "0, 1, 0"
    FieldOfView = "70">

相机位于World coordinates(0,-0.08,0.5)。请参阅1:设置LookDirection(0,0,-1)表示相机朝着负Z方向。

此相机设置有关于XYZ轴的Rotational Transformation功能:

<PerspectiveCamera.Transform>
    <Transform3DGroup>
        <RotateTransform3D>
            <RotateTransform3D.Rotation>
                <AxisAngleRotation3D
                    Axis="0 1 0"
                    Angle="{Binding Value, ElementName=hscroll}" />
            </RotateTransform3D.Rotation>
        </RotateTransform3D>
        <RotateTransform3D>
            <RotateTransform3D.Rotation>
                <AxisAngleRotation3D
                    Axis="1 0 0"
                    Angle="{Binding Value, ElementName=vscroll}" />
            </RotateTransform3D.Rotation>
        </RotateTransform3D>
        <RotateTransform3D>
            <RotateTransform3D.Rotation>
                <AxisAngleRotation3D
                    Axis="0 0 1"
                    Angle="{Binding Value, ElementName=vscrollz}" />
            </RotateTransform3D.Rotation>
        </RotateTransform3D>

    </Transform3DGroup>
</PerspectiveCamera.Transform>

旋转角度都绑定到来自Sliders的值:旋转X Axis="1 0 0"绑定vscroll slider, Y Axis "0 1 0"绑定hscroll slider, Z Axis "0 0 1"绑定vscrollz slider。有效角度为-180180度。滑动这些滑块将导致相机位置发生变化。结果视图看起来就像被查看的对象已被旋转。请参见图4:使用相机旋转查看更改

ModelVisual3D有两个ModelVisual3D.Content

一个包括包含光源的Model3DGroup。有两种光源:

<AmbientLight x:Name="amlight" Color="White"/>
<DirectionalLight x:Name="dirlight" Color="White" Direction="0,0,-0.5" >

灯光的默认color设置都设置为White。所有物体都将被白光照亮。

在下面的源代码中,我们根据sliders中的值更改这些灯的color

private void sliderColor_ValueChanged
  (object sender, RoutedPropertyChangedEventArgs<double> e)
{
//    if (!bIsXLoaded) return;
    if (sliderRed!= null && sliderGreen!= null && sliderBlue!= null && sliderAmb!=null)
    {
        Color color = Color.FromArgb(255, (byte)sliderRed.Value,
                      (byte)sliderGreen.Value, (byte)sliderBlue.Value);
        if (labelColor != null)
        {
            labelColor.Content = color.ToString();
            labelColor.Background = new SolidColorBrush(color);
        }

        if (dirlight != null)
            dirlight.Color = color;

        Color amcolor = Color.FromArgb(255, (byte)sliderAmb.Value,
                        (byte)sliderAmb.Value, (byte)sliderAmb.Value);
        if (amlight != null)
            amlight.Color = amcolor;
    }
}

我们还可以改变Directional灯光的方向:

void dispatcherTimer2_Tick(object sender, EventArgs e)
{
    var dir = dirlight.Direction;

    if (dir.Y > 5 || dir.Y < -5) deltaYdir = -1 * deltaYdir;
    dir.Y += deltaYdir;
    dirlight.Direction = new Vector3D(dir.X, dir.Y, dir.Z);
}

void dispatcherTimer_Tick(object sender, EventArgs e)
{
    var dir = dirlight.Direction;

    if (dir.X > 5 || dir.X<-5) deltaXdir = -1 * deltaXdir;
    dir.X += deltaXdir;
    dirlight.Direction = new Vector3D(dir.X, dir.Y, dir.Z);
}

更改灯光colordirection会导致以不同颜色和阴影查看对象。参见图2:点亮一个立方体

另一个ModelVisual3D.ContentModel3DGroup,其包包含GeometryModel3D.GeometryGeometryModel3D.Material.

GeometryModel3D.Geometry指定Mesh详细信息:PositionTextureCoordinatesTriangleIndicesGeometryModel3D.Material指定Brush用于渲染对象。原始对象是一个立方体,而画笔是一个简单的渐变图。

<GeometryModel3D.Geometry>
    <MeshGeometry3D x:Name="theGeometry"
        Positions="-0.05,-0.1,0 0.05,-0.1,0 -0.05,0,0 0.05,0,0 -0.05,-0.1,
                   -0.1 0.05,-0.1,-0.1 -0.05,0,-0.1 0.05,0,-0.1"
        TextureCoordinates="0,1 1,1 0,0 1,0 0,0 1,0 0,1 1,1"
        TriangleIndices="0,1,2 1,3,2 0,2,4 2,6,4 2,3,6 3,7,6 3,1,5 3,5,
                         7 0,5,1 0,4,5 6,5,4 6,7,5"/>
</GeometryModel3D.Geometry>

<!-- The material specifies the material applied to the 3D object.
     In this sample a linear gradient covers the surface of the 3D object.-->
<GeometryModel3D.Material>
    <MaterialGroup>
        <DiffuseMaterial x:Name="theMaterial">
            <DiffuseMaterial.Brush>
                <LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
                    <GradientStop Color="AliceBlue" Offset="0" />
                    <GradientStop Color="DarkBlue" Offset="1" />
                </LinearGradientBrush>
            </DiffuseMaterial.Brush>
        </DiffuseMaterial>
    </MaterialGroup>
</GeometryModel3D.Material>

为了改变网格的位置,我们在它的顶点上执行平移:

private void UpdateMesh(float offsetx,float offsety,float offsetz)
{
    var vertices = orgmeshpos;

    for (int i = 0; i < vertices.Count; i++)
    {
        var vert = vertices[i];
        vert.Z += offsetz;
        vert.Y += offsety;
        vert.X += offsetx;
        this.theGeometry.Positions[i] = new Point3D(vert.X, vert.Y, vert.Z);
    }
}

我们加载启动位置(存储在orgmeshpos中),然后应用我们通过XYZ滑块更改的平移。请参见图 3:使用Mesh Translation查看更改

用户界面

启动时,UI如下图5所示:

 

5:启动

这是启动立方体的前视图。您可以在位于左侧、右侧和底部的相机旋转滑块上滑动,以获得立方体的不同视图。

右键单击立方体以在面立方体渐变颜色立方体之间切换。

 

6:面立方体

6 显示了由ImageBrush渲染的面立方体 您可以使用平移设置滑块来更改立方体的位置。更改相机旋转滑块的值将导致以不同角度查看立方体。ambient光影和directional颜色设置会影响灯光的颜色和强度。这将导致对象看起来以不同的方式点亮。方向灯的方向可以通过点击标题<->^--v按钮来改变。定向光的方向会不断变化,在物体上产生不同的明暗和阴影。再次单击这些按钮可锁定当时的灯光方向。右键单击这些按钮可重置灯光的方向。

 

7:人脸模型

6face models可供选择。单击右侧6个面部图像中的任何一个。所选的face model将在default position中加载,但与当前camera rotation settings。要获取所有default positionscamera rotation settings,请单击Reset按钮,或双击渲染的face model

要获取模型当前视图的快照,请单击Snap按钮。快照将被记录,存储在内存中,并显示在左侧的垂直列中。该列显示最近拍摄的6张图像。要滚动查看其他图像,请将鼠标移到任何图像上并滚动鼠标滚轮。

单击图像以查看/保存到文件。

face model加载后,模型的纹理文件将显示在左上角。单击texture image以选择并加载新的面部文件。

 

8:面部拟合

当选择并加载一张人脸图像时,程序将尝试定位眼睛。眼睛检测是使用OpenCV HaarClassifier完成的。注意在活动face feature locator(红色圆圈)的中心,有一个小框。这是用于特征点的精确定位。同样在左上角,magnifier将显示活动face feature locator中的内容。

要定位的5个点是2 eyes, nose base, upper lip base and chin要精细移动locator,点击选择它,然后使用方向键,同时,查看放大镜中心的内容,以精确定位面点。

如果输入的人脸图像的眼睛不是很水平,请选中该Aligned Eye复选框。请注意,大多数人无法在正面照片中保持眼睛绝对水平。

单击Best Fit button以获取最适合新面孔的face model。单击Update以使用当前选定的face model

 

9:人脸拟合评估

选择并使用新面孔更新face model后,请注意以下事项:

  • 右上图:用作纹理的拉伸面
  • 右下:拉伸面与人脸模型的对齐拟合
  • 拟合误差:显示4个数字 <eye-nose>:<nose-mouth>:<mouse-chin>:<over-all>

为了良好的拟合,拉伸的脸不应该太变形。请参阅本文前面标有不合适的图,以获取不合适的人脸图像的示例。

Fitting Error将帮助您重新调整面部点。以值-10:15:-10:80为例。在这种情况下,eye-nose error=-10nose-mouth error=15mouth-chin error=-10overall error的计算与分配权重4eye-nose, 2nose-mouth, 1mouth-chin,将是80

为了弥补负面 eye-nose error,将2只眼睛locators靠得更近和/或降低nose locator。同样,为了补偿积极 eye-nose error的因素,将eyes locators距离拉得更远和/nose locator拉得更高。

为了补偿负面 nose-mouth error,请移动mouth and nose locators更远。对于积极误差,将这些locators彼此靠近。

为了弥补负面 mouth-chin error,将嘴巴和下巴locators进一步分开。对于积极误差,将这些locators彼此靠近。

在我们的值-10:15:-10:80示例中,我们移动eye locators得更近,nose locator向下以补偿-10 eye-nose error。对于nose-mouth error更正为15,把mouth locator增加。而对于mouth-chin error=-10,则chin locator进一步降低。

需要注意的是,为了补偿拟合误差而进行的face points locators重定位会导致拉伸后的人脸图像,其比例更接近原图,但如果校正过多,可能会导致部分人脸点与人脸模型上对应的人脸点不对齐。单击更新按钮,然后检查右下角的对齐拟合图像。

这是一个反复的过程,可能需要一些练习才能精通。但是,如果与我们的6个面部模型中的任何一个相比,它们的面部点配置明显不成比例,则某些面部图像可能根本无法令人满意地拟合。我尝试过的大多数人脸都可以以某种方式拟合到最大overall error100

请注意,如果您无法将nose basemesh nose base对齐,您可以将两个眼睛定位器移向同一方向。如果网格节点基础位于图像鼻子根的左侧,则将两个眼睛定位器向右移动。同样,如果网格鼻子根位于图像鼻子根的右侧,则将眼睛定位器向左移动。为了不拉伸图像,您必须将定位器移动相同的幅度。使用箭头键控制移动的幅度。每按一次箭头键,定位器都会移动相同的幅度。因此,为了不拉伸图像,如果将左眼定位器移动5次,则需要将右眼定位器移动5次。

面部拟合后,您可以执行平移和相机旋转以获得所需的视图。然后单击快照按钮拍摄快照。

要删除面上的网格线,请取消选中右上角的ShowGrid复选框。

有时,人脸图像不能完全覆盖人脸模型,尤其是在人脸边缘附近。

 

10:面部纹理不足

10显示我们无法有效地渲染脸部的侧面,因为脸部纹理不足。由于使用耳朵和头发的一部分进行渲染,脸部的边缘被扭曲了。为了处理这种情况,我设计了一种方法,用更靠近脸颊和眼睛一侧的面部纹理来修补新脸的一侧。取消选中左上角的No-Stretching复选框以启用此功能。

 

11:修补的面

11 显示脸部的侧面已经被从脸部内部延伸的纹理修补。

代码高亮

OpenCV:在没有EmgucvC#中寻找人脸和眼睛。最初,我想使用Emgucv,但是占用空间太大,不适合在本文中分发。此处的代码使用Opencv 2.2的包装器。包装器的代码在DetectFace.cs中。下面的代码使用此包装器中的方法进行面部和眼睛检测。包装器代码是从https://gist.github.com/zrxq/1115520/fc3bbdb8589eba5fc243fb42a1964e8697c70319的detectface.cs修改的。

public static void FindFaceAndEyes(BitmapSource srcimage, 
out System.Drawing.Rectangle facerect, out System.Drawing.Rectangle[] eyesrect)
{
    String faceFileName = AppDomain.CurrentDomain.BaseDirectory + 
                          "haarcascade_frontalface_alt2.xml";
    String eyeFileName = AppDomain.CurrentDomain.BaseDirectory + "haarcascade_eye.xml";

    IntelImage _img = CDetectFace.CreateIntelImageFromBitmapSource(srcimage);

    using (HaarClassifier haarface = new HaarClassifier(faceFileName))
    using (HaarClassifier haareye = new HaarClassifier(eyeFileName))
    {
        var faces = haarface.DetectObjects(_img.IplImage());
        if(faces.Count>0)
        {
                var face = faces.ElementAt(0);
                facerect = new System.Drawing.Rectangle
                           (face.x, face.y, face.width, face.height);
                
                int x=face.x,y=face.y,h0=face.height ,w0=face.width;
                System.Drawing.Rectangle temprect = 
                                         new System.Drawing.Rectangle(x,y,w0,5*h0/8);
                System.Drawing.Bitmap bm_current=
                                      CDetectFace.ToBitmap(_img.IplImageStruc(),false)  ;  
                System.Drawing.Bitmap bm_eyes =   bm_current.cropAtRect(temprect);
                bm_eyes.Save(AppDomain.CurrentDomain.BaseDirectory + "temp\\~eye.bmp", 
                                       System.Drawing.Imaging.ImageFormat.Bmp);
                IntelImage image_eyes = CDetectFace.CreateIntelImageFromBitmap(bm_eyes);
            
                 IntPtr p_eq_img_eyes= CDetectFace.HistEqualize(image_eyes);
        
                 var eyes = haareye.DetectObjects(p_eq_img_eyes);

              //clean up
                 NativeMethods.cvReleaseImage(ref  p_eq_img_eyes);  
                image_eyes.Dispose();
                image_eyes = null;                      
                bm_eyes.Dispose();
              
                if (eyes.Count > 0)
                {
                    eyesrect = new System.Drawing.Rectangle[eyes.Count];

                    for (int i = 0; i < eyesrect.Length; i++)
                    {
                        var eye = eyes.ElementAt(i);
                        eyesrect[i] = new System.Drawing.Rectangle
                                      (eye.x, eye.y, eye.width, eye.height);
                    }
                }
                else
                    eyesrect = null;   
           }
            else
            {
                facerect = System.Drawing.Rectangle.Empty;
                eyesrect = null;
            }
    }

    _img.Dispose();
}

WPFGDI+转换WPF System.Windows.Media类非常适合演示,但在图像处理方面它们不是那么灵活。在System.Winows.Drawing.Bitmap上绘图比在System.Windows.Media.ImageSource上绘图更容易。因此,对于位图操作,我将WPF BitmapSource转换为WPF System.Windows.Drawing.Bitmap并且为了在WPF上进行演示,我将System.Windows.Drawing.Bitmap转换为BitmapSource

public static System.Windows.Media.Imaging.BitmapImage Bitmap2BitmapImage
       (System.Drawing.Bitmap bitmap)
{
        System.Drawing.Image img = new System.Drawing.Bitmap(bitmap);
        ((System.Drawing.Bitmap)img).SetResolution(96, 96);
        MemoryStream ms = new MemoryStream();

        img.Save(ms, System.Drawing.Imaging.ImageFormat.Png );
        img.Dispose();
        img=null;
        ms.Seek(0, SeekOrigin.Begin);

        BitmapImage bi = new BitmapImage();

        bi.BeginInit();
        bi.StreamSource = ms;
        bi.EndInit();
        bi.Freeze();
        return bi;
}

public static System.Drawing.Bitmap BitmapImage2Bitmap(BitmapSource bitmapImage)
{
    using (MemoryStream outStream = new MemoryStream())
    {
        //BitmapEncoder enc = new BmpBitmapEncoder();
        BitmapEncoder enc = new PngBitmapEncoder();
        enc.Frames.Add(BitmapFrame.Create(bitmapImage));
        enc.Save(outStream);
        System.Drawing.Bitmap bitmap = new System.Drawing.Bitmap(outStream);
        bitmap.SetResolution(96, 96);
        System.Drawing.Bitmap bm=new System.Drawing.Bitmap(bitmap);
        tempbm.Dispose();
        tempbm=null;
        return bm;
    }
}

从视口拍照。类RenderTargetBitmap对于从Viewport3D中抓取图像非常有用。 然而,整个viewport被抓取了。尽管如此,我们还是可以得到对象,因为大多数viewport捕捉到的像素都是透明的。可以找到非透明像素的边界矩形,调整边界以获得一些边距,然后我们从抓取的RenderTargetBitmap使用CropBitmap类中进行裁剪。然后,我们使用FormatConvertedBitmap类将最终图像转换为大多数图像处理软件(包括我们的Opencv包装器)使用的标准RGB24格式。

var viewport = this.viewport3d;
var renderTargetBitmap = new RenderTargetBitmap((int)
                                               (((int)viewport.ActualWidth+3)/4 *4) ,
                                               (int)viewport.ActualHeight  ,
                                               96, 96, PixelFormats.Pbgra32);
renderTargetBitmap.Render(viewport);

byte[] b=new byte[(int)renderTargetBitmap.Height*(int)renderTargetBitmap.Width*4];
int stride=((int)renderTargetBitmap.Width )*4;
renderTargetBitmap.CopyPixels(b, stride, 0);

//get bounding box;
int x = 0, y = 0,minx=99999,maxx=0,miny=99999,maxy=0;
//reset all the alpha bits
for(int i=0;i<b.Length;i=i+4)
{
    y = i /stride;
    x = (i % stride) / 4;

    if (b[i + 3] == 0) //if transparent we set to white
    {
        b[i] = 255;
        b[i + 1] = 255;
        b[i + 2] = 255;
    }
    else
    {
        if (x > maxx) maxx = x;
        if (x < minx) minx = x;
        if (y > maxy) maxy = y;
        if (y < miny) miny = y;
    }
}

BitmapSource image = BitmapSource.Create(
    (int)renderTargetBitmap.Width ,
    (int)renderTargetBitmap.Height,
    96,
    96,
    PixelFormats.Bgra32,
    null,
    b,
    stride);

int cropx = minx - 20;
if (cropx < 0) cropx = 0;
int cropy = miny - 20;

if (cropy < 0) cropy = 0;

int cropwidth = (((maxx - cropx + 20 + 1) + 3) / 4) * 4;
int cropheight = maxy - cropy + 20 + 1;

//check oversized cropping
int excessx = cropwidth + cropx - image.PixelWidth;
int excessy = cropheight + cropy - image.PixelHeight;
if (excessx < 0) excessx = 0;
if (excessy < 0) excessy = 0;
excessx = ((excessx + 3) / 4) * 4;

CroppedBitmap crop;
try
{
    crop = new CroppedBitmap(image, new Int32Rect
           (cropx, cropy, cropwidth - excessx, cropheight - excessy));
}
catch
{
    return;
}

Convert to rgb24
var destbmp = new FormatConvertedBitmap();
destbmp.BeginInit();
destbmp.DestinationFormat = PixelFormats.Rgb24;
destbmp.Source = crop;
destbmp.EndInit();

保存图像和背景Window2实现一个通用窗口来显示和保存图像。它由一个包含一个Image (Image1)Grid (TopGrid) 组成。 代码检索TopGrid背景的image source并将image sourceImage1绘制到其上面。对于这样的叠加,background图像和foreground图像都必须支持transparency

int imagewidth = (int)Image1.Source.Width;
int imageheight = (int)Image1.Source.Height ;
System.Drawing.Bitmap bm=null;

if (SourceBrushImage == null)
    bm = new System.Drawing.Bitmap
    (imagewidth, imageheight, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
else
{
  //TopGrid Background store the ImageBrush
  ImageBrush ib=(ImageBrush)(TopGrid.Background) ;
  BitmapSource ibimgsrc = ib.ImageSource as BitmapSource;
  bm = CCommon.BitmapImage2Bitmap(ibimgsrc);
}
System.Drawing.Graphics gbm = System.Drawing.Graphics.FromImage(bm);

if (SourceBrushImage == null)
   gbm.Clear(System.Drawing.Color.AliceBlue);

//Image1 store the image
System.Drawing.Bitmap bm2 =
 CCommon.BitmapImage2Bitmap(Image1.Source as BitmapSource );// as BitmapImage);
gbm.DrawImage(bm2, 0, 0);
gbm.Dispose();

bm.Save(filename, System.Drawing.Imaging.ImageFormat.Jpeg);

AForge亮度和对比度。当我们调整亮度或/和对比度时,我们检索原始未过滤(即未使用任何图像过滤器)并在原始图像上应用亮度和对比度过滤器,依次是亮度,然后使用我们设置的结果图像对比度过滤器。

 if (((System.Windows.Controls.Slider)sender).Name == "sliderBrightness" ||
                ((System.Windows.Controls.Slider)sender).Name == "sliderContrast")
            {
                    if (colorbitmap == null) return;
                    System.Drawing.Bitmap bm = 
                           CCommon.BitmapImage2Bitmap((BitmapImage)colorbitmap);

                    AForge.Imaging.Filters.BrightnessCorrection filterB = 
                                   new AForge.Imaging.Filters.BrightnessCorrection();
                    AForge.Imaging.Filters.ContrastCorrection filterC = 
                                   new AForge.Imaging.Filters.ContrastCorrection();

                    filterB.AdjustValue = (int)sliderBrightness.Value;
                    filterC.Factor = (int)sliderContrast.Value;
                  
                    bm = filterB.Apply(bm);
                    bm = filterC.Apply(bm);               

                    BitmapImage bitmapimage = CCommon.Bitmap2BitmapImage(bm);
                    theMaterial.Brush = new ImageBrush(bitmapimage)
                    {
                        ViewportUnits = BrushMappingMode.Absolute
                    };                                
            }

获得最佳拟合人脸模型面部拟合的算法已经在前面介绍过。在这里,我们通过比率来工作,而不实际对新人脸进行图像处理,以找到拟合误差,然后选择误差最小的人脸模型

public string getBestFittingMesh(string filename)
       {
           FeaturePointType righteyeNew = new FeaturePointType();
           FeaturePointType lefteyeNew = new FeaturePointType();
           FeaturePointType noseNew = new FeaturePointType();
           FeaturePointType mouthNew = new FeaturePointType();
           FeaturePointType chinNew = new FeaturePointType();

           for (int i = 0; i < _imagefacepoints.Count; i++)
           {
               FeaturePointType fp = new FeaturePointType();
               fp.desp = _imagefacepoints[i].desp;
               fp.pt = _imagefacepoints[i].pt;
               switch (fp.desp)
               {
                   case "RightEye1":
                       righteyeNew = fp;
                       break;
                   case "LeftEye1":
                       lefteyeNew = fp;
                       break;
                   case "Nose1":
                       noseNew = fp;
                       break;
                   case "Mouth3":
                       mouthNew = fp;
                       break;
                   case "Chin1":
                       chinNew = fp;
                       break;
               }
           }

           //do prerotation
           if (_degPreRotate != 0)
           {
               //all point are to be altered
               righteyeNew = rotateFeaturePoint(righteyeNew, _degPreRotate);
               lefteyeNew = rotateFeaturePoint(lefteyeNew, _degPreRotate);
               noseNew = rotateFeaturePoint(noseNew, _degPreRotate);
               mouthNew = rotateFeaturePoint(mouthNew, _degPreRotate);
               chinNew = rotateFeaturePoint(chinNew, _degPreRotate);
           }

           int eyedistNew = (int)(lefteyeNew.pt.X - righteyeNew.pt.X);

           FeaturePointType righteyeRef = new FeaturePointType();
           FeaturePointType lefteyeRef = new FeaturePointType();
           FeaturePointType noseRef = new FeaturePointType();
           FeaturePointType mouthRef = new FeaturePointType();
           FeaturePointType chinRef = new FeaturePointType();

           string[] meshinfofiles = Directory.GetFiles
           (AppDomain.CurrentDomain.BaseDirectory + "mesh\\","*.info.txt");

           List<Tuple<string,string, double>> listerr =
                             new List<Tuple<string,string, double>>();

           foreach(var infofilename in meshinfofiles)
           {
               //string infofilename = AppDomain.CurrentDomain.BaseDirectory +
               // "\\mesh\\mesh" + this.Title + ".info.txt";
               using (var file = File.OpenText(infofilename))
               {
                   string s = file.ReadToEnd();
                   var lines = s.Split(new string[] { "\r\n", "\n" },
                               StringSplitOptions.RemoveEmptyEntries);
                   for (int i = 0; i < lines.Length; i++)
                   {
                       var parts = lines[i].Split('=');
                       FeaturePointType fp = new FeaturePointType();
                       fp.desp = parts[0];
                       fp.pt = ExtractPoint(parts[1]);
                       switch (fp.desp)
                       {
                           case "RightEye1":
                               righteyeRef = fp;
                               break;
                           case "LeftEye1":
                               lefteyeRef = fp;
                               break;
                           case "Nose1":
                               noseRef = fp;
                               break;
                           case "Mouth3":
                               mouthRef = fp;
                               break;
                           case "Chin1":
                               chinRef = fp;
                               break;
                       }
                   }
               }

               double x0Ref = (lefteyeRef.pt.X + righteyeRef.pt.X) / 2;
               double y0Ref = (lefteyeRef.pt.Y + righteyeRef.pt.Y) / 2;
               double x0New = (lefteyeNew.pt.X + righteyeNew.pt.X) / 2;
               double y0New = (lefteyeNew.pt.Y + righteyeNew.pt.Y) / 2;

              int eyedistRef = (int)(lefteyeRef.pt.X - righteyeRef.pt.X);
              double noselengthNew = Math.Sqrt((noseNew.pt.X - x0New) *
              (noseNew.pt.X - x0New) + (noseNew.pt.Y - y0New) * (noseNew.pt.Y - y0New));
              double noselengthRef = Math.Sqrt((noseRef.pt.X - x0Ref) *
              (noseRef.pt.X - x0Ref) + (noseRef.pt.Y - y0Ref) * (noseRef.pt.Y - y0Ref));

              double ratiox = (double)eyedistRef / (double)eyedistNew;
              double ratioy = noselengthRef / noselengthNew;
              double errFitting = /*Math.Abs*/(ratiox - ratioy) / ratiox;

              Alight the mouth//
              Point newptNose = new Point(noseNew.pt.X * ratiox, noseNew.pt.Y * ratioy);
              Point newptMouth = new Point(mouthNew.pt.X * ratiox, mouthNew.pt.Y * ratioy);

              double mouthDistRef = mouthRef.pt.Y - noseRef.pt.Y;

              double mouthDistNew = newptMouth.Y - newptNose.Y;//noseNew.pt.Y * ratioy;

              double ratioy2 = mouthDistRef / mouthDistNew;

              double errFitting1 = /*Math.Abs*/(1 - ratioy2);

              ///Align the chin
              Point newptChin = new Point(chinNew.pt.X * ratiox, chinNew.pt.Y * ratioy);
              double chinDistRef = chinRef.pt.Y - mouthRef.pt.Y;

              double chinDistNew = newptChin.Y - newptMouth.Y;//noseNew.pt.Y * ratioy;

              double ratioy3 = chinDistRef / chinDistNew;

              double errFitting2 = /*Math.Abs*/(1 - ratioy3);

              double score = Math.Abs(errFitting)*4+ Math.Abs(errFitting1)*2+
                             Math.Abs(errFitting2);
              string fittingerr = (int)(errFitting*100)+":"+
                                  (int)(errFitting1*100) +":"+ (int)(errFitting2*100);
              Tuple<string,string,double> tp=new Tuple<string,string,double>
                                             (infofilename,fittingerr,score);
              listerr.Add(tp);
           }
           var sortedlist = listerr.OrderBy(o => o.Item3).ToList();
           string selected=sortedlist[0].Item1;
           var v=selected.Split('\\');
           var v2 = v[v.Length - 1].Split('.');
           string meshname = v2[0].Replace("mesh","");
           return meshname ;
       }

使用WritableBitmap实现放大镜。这是一个简单但非常有用的Magnifier实现。这个想法是,当容器(Grid/Window)调整大小时,具有Auto宽度和高度的WPF图像将被拉伸。在xaml文件中,我们将窗口大小定义为50X50用于Window3

<Window x:Class="ThreeDFaces.Window3"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Height="50" 
                 Width="50" WindowStyle="None"  
                 PreviewMouseLeftButtonDown="Window_PreviewMouseLeftButtonDown" 
                 PreviewMouseMove="Window_PreviewMouseMove" Loaded="Window_Loaded" 
                 SizeChanged="Window_SizeChanged">
    <Grid Name="MainGrid">
        <Image Name="Image1" HorizontalAlignment="Left" Height="Auto" 
                             VerticalAlignment="Top"  Width="Auto"/>
    </Grid>
</Window>

winMagnifier是对Window3的引用。当我们第一次创建一个新的Window3时,我们将在winMagnifier中的Image1image source初始化为WritetableBitmap50X50大小)。

_wbitmap = new WriteableBitmap(50, 50, 96, 96, PixelFormats.Bgra32, null);
winMagnifier = new Window3();
winMagnifier.Image1.Source = _wbitmap;
UpdateMagnifier(0, 0);
winMagnifier.Owner = this;
winMagnifier.Show();

加载窗口时,我们将其大小调整为150X150,因此图像看起来像是被放大了3倍。我们还必须确保保持纵横比,以使窗口不会被不均匀地拉伸。我们实现了一个计时器,当窗口调整大小时,它会检查窗口宽度是否等于窗口高度。

private void Window_Loaded(object sender, RoutedEventArgs e)
{
    this.Width = 150;
    this.Height = 150;
    _resizeTimer.Tick += _resizeTimer_Tick;
}

void _resizeTimer_Tick(object sender, EventArgs e)
{
    _resizeTimer.IsEnabled = false;
    if (bHeightChanged)
        this.Width = this.Height;
    else
        this.Height = this.Width;
}

private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
{
    Size oldsize = e.PreviousSize;
    Size newsize = e.NewSize;

    bHeightChanged = ((int)oldsize.Height) == ((int)newsize.Height) ? false : true;

    _resizeTimer.IsEnabled = true;
    _resizeTimer.Stop();
    _resizeTimer.Start();
}

在调用过程中,我们有一个方法可以更新WritetableBitmap,它是winMagnifierImage1的源。这样,每次调用UpdateMagnifier时,winMagnifier的内容都会发生变化。

private void UpdateMagnifier(int x, int y)
     {
         try
         {
             BitmapImage bmi = Image1.Source as BitmapImage;
             int byteperpixel=(bmi.Format.BitsPerPixel + 7) / 8;
             int stride = bmi.PixelWidth * byteperpixel;
             byte[] _buffer = new byte[50 * stride];

             bmi.CopyPixels(new Int32Rect(x, y, 50, 50), _buffer, stride, 0);
             //Draw the cross bars
             for (int i = 0; i < 50;i++ )
                 for (int k = 0; k < 2;k++ )
                     _buffer[24 * stride + i * byteperpixel+k] = 255;

             for (int j = 0; j < 50;j++ )
                 for (int k = 0; k < 2; k++)
                 {
                     _buffer[j * stride + 24 * byteperpixel + k] = 255;
                 }

                 _wbitmap.WritePixels(new Int32Rect(0, 0, 50, 50), _buffer, stride, 0);
         }
         catch
         {

         }
     }

演示

对于犯罪调查:从以前的正面获取侧​​面轮廓,以匹配同一个人的另一个图像的侧面视图。

 

 

威廉莎士比亚面具适合我们所有6种面部模型:

 

只是为了好玩,有色调、颜色和方向:

 

注意

张开嘴露出牙齿的脸在侧视图中显示效果不佳。看起来牙齿从嘴里伸出来。

眼睛明显不水平的脸效果不好。

我在\Images目录中包含了一些图像以进行测试。这些图片的来源是:

更新

2.0版本开始,您可以处理具有多张面孔的图像。

 

当加载多张人脸图像时,会弹出一个人脸选择窗口,所有检测到的人脸都标记在红色矩形内。通过在矩形中双击来选择一个面。人脸选择窗口将被最小化,选择的人脸将出现在人脸拟合窗口中。如果您在不进行任何选择的情况下最小化人脸选择窗口,则会自动选择检测到的第一个人脸。如果关闭人脸选择窗口,则整个多张人脸图像将出现在人脸拟合窗口中。

如果要从多张人脸图像中选择另一张人脸,只需从任务栏恢复人脸选择窗口并进行其他选择,您不必从文件中重新加载多张人脸图像。

我还在\GroupImages目录中包含了一个包含许多面孔的测试文件。

2.2版本中,您可以为模型创建动画gif文件,其中相机围绕y轴左右旋转。

 

用于动画gif的编码器来自于文章:NGif,用于.net的动画gif编码器

2.3 版本中,您可以进行Face Left/Right Mapping。右键单击视口中的面部模型,将出现一个上下文菜单供您选择1) Map Left2) Map Right3) No Map

如果您选择Map Left,左脸(即出现在屏幕右侧的脸的一侧)将被右脸替换。同样,Map RightRight Face替换为Left Face。见下图:

 

为了正确完成面部映射,拉伸的面部应该对齐,使得眼睛是水平的,并且鼻子底部与面部网格叠加图像上的鼻子底部标记对齐。见下图:

 

对于微调,您可以在Face Fitting窗口中增量移动眼睛和鼻子标记,并观察人脸网格叠加图像和人脸模型的变化。请注意,如果您的一只眼睛定位器高于另一只眼睛定位器,您可以相对于脸部网格有效地旋转脸部图像。

我在/temp目录中包含了/Images目录中所有面部图像的面部点信息文件。您可以从/Images/ronald-reagan.jpg测试上面的图像。加载文件后,将标记面部点,然后您可以单击最佳拟合按钮更新面部模型,然后右键单击它以选择所需的面部映射

3.0中,您可以添加和编辑基于6个基本面模型中的任何一个的面网格。

 

要创建自己的面部模型:

  1. 从右侧的面模型网格列加载6个基本面模型中的任何一个。
  2. 加载面部模型后,右键单击右下角的面部网格。

要编辑或删除任何新创建的人脸模型,滚动到新人脸模型并右键单击它。将弹出一个上下文菜单以允许您编辑/删除面部模型。

要编辑面部模型,请使用位于面部模型编辑窗口四个边缘的四个滑块中的任何一个来旋转和拉伸面部模型。

我也改进了网格编辑。现在有一些改进的功能允许您在3D空间中选择和移动面网格点。移动鼠标选择和删除面点进行编辑。按键移动选定的点。当您移动鼠标和单击某个点时,请参阅UI上的说明。

 

4.0

4.0版本中,我添加了基于Microsoft Cognitive Services Face API的人脸匹配功能。要使用人API,您需要从以下站点获取API密钥:https://azure.microsoft.com/en-us/try/cognitive-services/?api=face-api。

 

API密钥是一个32个字符的字符串,用于唯一标识人脸API用户。

获得API密钥后,您可以通过Face Matcher Window开始使用Face API

 

要启动Fach Matcher窗口,您可以单击Show Matcher按钮,或右键单击任何拍摄的图像以弹出如上图所示的上下文菜单,然后选择Show Matcher Window菜单项。

窗口启动后,在Key文本框中键入或粘贴32个字符的API密钥,对于Region文本框,您可以保留默认区域westen-central,除非您从其他区域获取API 密钥。然后单击生成人脸服务按钮。API密钥将在线验证并创建人脸服务客户端对象

2个面板,左侧为面板1,右侧为面板2。对于每个面板,您可以使用Browse..按钮加载面部图像,以从您的计算机中选择图像文件。选择文件后,它会通过Web服务调用在线发送到后端的Azure云服务器。该过程是使用await-async机制完成的,因此您可以在处理第一个文件之前在另一个面板上加载另一个文件。

或者,您可以使用主窗口的捕捉面面板将任何捕捉面加载到面匹配器窗口。右键单击任何一张被捕捉的人脸以将其选中并弹出上下文菜单,然后选择子菜单项Load1将人脸图像加载到左侧面板1Load 2将图像加载到右侧面板2。同样,您可以在处理第一个人面之前加载第二个人面。

处理完文件后,人脸匹配器窗口的标题栏将显示检测到的人脸数量。如果检测到任何人脸,面板的图像将显示加载的图像内容,所有人脸都被装箱。对于多张人脸,将选择一个默认用red框起来的人脸进行匹配,否则将选择唯一检测到的人脸。要在多面图像中选择不同的面进行匹配,只需单击任何被框入green的人面,它将被选中并框入red

选择的匹配人面,每个面板中的一个将并排显示在Match按钮旁边的thumbnail images上。点击Match按钮发送人脸进行匹配。匹配Results将由2个返回值反映:Is Identical(布尔值)和Confidence01之间的双精度值)。匹配的人面将具有值:Is Identical设置为trueConfidence0.5或更大。面孔越相似,Confidence得分越高。

请注意,您从https://azure.microsoft.com/en-us/try/cognitive-services/?api=face-api获得的API密钥将在30天后过期。要获得永久密钥,您必须使用Microsoft Azure Services创建一个帐户。目前MicrosoftAzure订阅者的人API提供免费层。

免费试用和免费套餐的限制:

每分钟最多20Web服务调用,每月最多30000个调用。

Face Matcher的代码高亮

创建FaceServiceClient对象

faceServiceClient = 
new FaceClient(
new ApiKeyServiceClientCredentials(Key.Text ),
new System.Net.Http.DelegatingHandler[] { });

使用API密钥Region参数实例化一个FaceServiceClient对象,并将其分配给公开许多面部操作的faceServiceClient IFaceServiceClient接口。在本文中,我们将使用众多操作中的两个:

  • DetectAsync
  • VerifyAsync

将图像文件异步加载到后端服务器进行人脸检测

  private async Task<DetectedFace[]> UploadAndDetectFaces(string imageFilePath)
        {
            // The list of Face attributes to return.
            IList<FaceAttributeType?> faceAttributes =
                new FaceAttributeType?[] { FaceAttributeType.Gender,
                                         FaceAttributeType.Age,
                                         FaceAttributeType.Smile,
                                         FaceAttributeType.Emotion,
                                         FaceAttributeType.Glasses,
                                         FaceAttributeType.Hair,
                                         FaceAttributeType.Blur,
                                         FaceAttributeType.Noise};

            // Call the Face API.
            try
            {
                using (Stream imageFileStream = File.OpenRead(imageFilePath))
                {
                    IList<DetectedFace> faces = 
                    await faceServiceClient.Face.DetectWithStreamAsync(imageFileStream,
                                            returnFaceId: true,
                                            returnFaceLandmarks: false,
                                            returnFaceAttributes: faceAttributes);
                    if (faces.Count > 0)
                    {
                        DetectedFace[] list = new DetectedFace[faces.Count];
                        faces.CopyTo(list, 0);
                        return list;
                    } else
                        return new DetectedFace[0];
                }
            }
            // Catch and display Face API errors.
            catch (APIErrorException f)
            {
                MessageBox.Show(f.Message);
                return new DetectedFace[0];
            }
            // Catch and display all other errors.
            catch (Exception e)
            {
                MessageBox.Show(e.Message, "Error");
                return new DetectedFace[0];
            }
        }

DetectAsync函数将图像数据作为IO流与一些设置参数一起接收,并返回检测到的人脸Face数组。当returnFaceId设置为true时,每个返回Face的对象都将具有一个唯一Guid标识符,用于向后端服务器标识检测到的人脸。returnFaceAttributes用于指定要在Face对象中返回哪些FaceAttributes。在上面的代码中,我们指定了FaceAttributeTypeAgeSex..Hair如果我们还想得到FaceLandmarks, 我们设置returnFaceLandmarkstrueFaceLandmarks是检测到的面部标志的2D坐标:左瞳孔、右瞳孔、鼻尖。

异步人脸匹配

private async Task<VerifyResult> VerifyFaces(Guid faceId1, Guid faceId2)
{
    VerifyResult result = await faceServiceClient.Face.VerifyFaceToFaceAsync(faceId1, faceId2);
    return result;
}

对于人脸匹配,我们调用VerifyAsync函数,传入我们之前检测到的Guid已知的两个人面的Guids。请注意,在我们之前的调用DetectAsync中,我们将returnFaceId设置为true,这样我们就可以得到所有检测到的人脸的GuidGuid用于Face MatchingVeriyAsync函数的返回是一个由属性IsIdentical(bool) Confidence(double)组成的VerifyResult对象。

https://www.codeproject.com/Articles/1182854/D-Face-Viewer-and-Matcher

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值