返回文章列表文章索引阅读全文
STARBLOG / 阅读手记
C#

C# integrates the ViewFaceCore face detection and recognition library.

Face detection and recognition are now quite mature. There's a very convenient library called ViewFaceCore available for C#, but libraries involving native calls usually have some pitfalls. This article documents the development and deployment process.

曦远 (DealiAxy)约 32 分钟
目录14 个章节

Preface

Face detection and recognition are now very mature. On C#, there is a convenient library called ViewFaceCore. However, libraries that involve native calls typically come with some gotchas. This article documents the development and deployment process.

The project for this article is AIHub. For details on the development process of this project, you can refer to my previous post: Project Summary: Building a Large Model Client with Blazor and gRPC.

Furthermore, after the past month's work, I have refactored this project. The interface has been switched to Ant Design, and many new features have been added. I will share more details in future blog posts.

See the Result First

For multi-object detection, I immediately thought of "Girls' Generation" (少女时代), which was very popular back in my school days.

Introduction to ViewFaceCore

This is a .NET face recognition solution based on SeetaFace6.

SeetaFace6 is the latest open-source commercial official version from CogniVisio (中科视拓). It breaks through the previous situation where the community and enterprise versions were not released in sync. This time, the open-source v6 version is officially synchronized with the commercial version.

The primary work involved creating a .NET wrapper for SeetaFace6.

It supports the following features:

  • Age prediction
  • Eye state detection
  • Gender prediction
  • Face detection
  • Mask detection / Masked face keypoint localization, 5 key points
  • Face keypoint localization (5 points / 68 points)
  • Face recognition (5 points / 68 points)
  • Liveness detection
  • Head pose estimation
  • Quality assessment

Using it in C# is very straightforward. However, since it calls a C++ library, there can be some minor pitfalls during deployment. This article documents these issues and their solutions.

Add Dependencies

First, add the following dependencies:

<PackageReference Include="ViewFaceCore" Version="0.3.8" />
<PackageReference Include="ViewFaceCore.all_models" Version="6.0.7" />
<PackageReference Include="ViewFaceCore.Extension.ImageSharp" Version="0.3.7" />
<PackageReference Include="ViewFaceCore.runtime.ubuntu.20.04.x64" Version="6.0.7" />
<PackageReference Include="ViewFaceCore.runtime.win.x64" Version="6.0.7" />

The runtimes starting with ViewFaceCore.runtime. need to be installed based on the development and deployment environment. In this case, I have installed both the Windows and Linux versions.

Note that the Linux version also depends on the libgomp1 library. If you encounter errors during usage, you need to install it first.

Face Detection

It's quite simple—first, create a FaceDetector object.

Since this model uses unmanaged resources, remember to use using or manually call the Dispose method to release resources.

using FaceDetector _faceDetector = new();

Then, pass in an image object to perform detection. If a face is detected, it will return the four coordinates of the bounding box.

It supports three image libraries:

  • SkiaSharp
  • ImageSharp
  • System.Drawing

The third one is Microsoft's official library, but it's rumored to be deprecated. That's why I generally use ImageSharp—it's a pure C# implementation and works well cross-platform.

You need to install the ViewFaceCore.Extension.ImageSharp dependency to support ImageSharp images.

Simple Example

Let's start with the simplest example: detecting a face and drawing a frame around it.

public async Task<byte[]> DrawFaceFrame(byte[] imageBuffer, string format = "jpg") {
  using var inputStream = new MemoryStream(imageBuffer);
  using var image = await Image.LoadAsync(inputStream);
  var faceInfos = await _faceDetector.DetectAsync(image);
  foreach (var face in faceInfos) {
    image.Mutate(x => {
      x.Draw(
        Color.HotPink, 2.5f,
        new RectangleF(face.Location.X, face.Location.Y, face.Location.Width, face.Location.Height)
      );
    });
  }

  using var outputStream = new MemoryStream();
  await image.SaveAsync(outputStream, image.DetectEncoder($"demo.{format}"));
  return outputStream.ToArray();
}

The code above takes an image stream of type byte[] as input and outputs an image with the face frame drawn, also of type byte[].

It's quite simple, but ImageSharp's documentation is sparse. You still have to explore a bit to figure out how to draw a rectangular frame.

Complete Usage

Taking the previous "Preview the Effect" as an example, first define the data structure.

public class FaceItem {
  public FaceInfo FaceInfo { get; set; }
  public FaceMarkPoint[] FaceMarkPoints { get; set; }
  public float[]? FaceFeatures { get; set; }
  public byte[]? ImageBuffer { get; set; }
}

public class FaceDetectResult {
  public List<FaceItem> FaceItems { get; set; }
  public byte[] ImageBuffer { get; set; }
}

Three objects from ViewFaceCore are needed.

// Face detection
private readonly FaceDetector _faceDetector = new();

// Face landmark points
private readonly FaceLandmarker _faceMark = new();

// Face recognition
private readonly FaceRecognizer _faceRecognizer = new();

Key Code

PS: The code is quite rough, with average performance, just barely usable.

/// <summary>
/// Face Detection
/// </summary>
/// <param name="extractFeatures">Whether to extract face features</param>
/// <param name="cropEveryFace">Whether to crop each face into a small image</param>
/// <returns></returns>
public async Task<FaceDetectResult> Detect(
  byte[] imageBuffer, string format = "jpg",
  bool extractFeatures = false, bool cropEveryFace = false
) {
  var font = GetFont("segoeui.ttf");

  using var inputStream = new MemoryStream(imageBuffer);
  using var image = await Image.LoadAsync<Rgba32>(inputStream);
  using var resultImage = image.Clone();

  var result = new FaceDetectResult { FaceItems = new List<FaceItem>() };
  var faceInfos = await _faceDetector.DetectAsync(image);

  foreach (var face in faceInfos) {
    var faceItem = new FaceItem {
      FaceInfo = face,
      FaceMarkPoints = await _faceMark.MarkAsync(image, face)
    };

    // Extract face features
    if (extractFeatures) {
      faceItem.FaceFeatures = await _faceRecognizer.ExtractAsync(image, faceItem.FaceMarkPoints);    
    }

    // Crop face small image
    if (cropEveryFace) {
      using var faceImage = image.Clone();
      var cropRect = GetCropRect(face, 5);
      try {
        faceImage.Mutate(x => x.Crop(cropRect));
      }
      catch (ArgumentException ex) {
        faceImage.Mutate(x => x.Crop(GetCropRect(face, 0)));
      }

      using (var faceImageStream = new MemoryStream()) {
        await faceImage.SaveAsync(faceImageStream, faceImage.DetectEncoder($"demo.{format}"));
        faceItem.ImageBuffer = faceImageStream.ToArray();
      }
    }

    result.FaceItems.Add(faceItem);

    // Draw face bounding box
    resultImage.Mutate(x => {
      x.Draw(
        Color.HotPink, 2.5f,
        new RectangleF(face.Location.X, face.Location.Y, face.Location.Width, face.Location.Height)
      );
      x.DrawText(
        $"face:{face.Score}", font, Color.HotPink,
        new PointF(face.Location.X, face.Location.Y - 20)
      );
    });
  }

  using var outputStream = new MemoryStream();
  await resultImage.SaveAsync(outputStream, resultImage.DetectEncoder($"demo.{format}"));
  result.ImageBuffer = outputStream.ToArray();

  return result;
}

Font and rectangle generation code

ImageSharp's documentation is very lacking; each step requires extensive searching...

private Font GetFont(string fontFileName) {
  var path = !string.IsNullOrWhiteSpace(_pathPrefix) ? Path.Combine(_pathPrefix, fontFileName) : fontFileName;
  FontCollection collection = new();
  FontFamily family = collection.Add(path);
  return family.CreateFont(20, FontStyle.Bold);
}

private static Rectangle GetCropRect(FaceInfo faceInfo, int cropOffset) {
  return new Rectangle(faceInfo.Location.X - cropOffset, faceInfo.Location.Y - cropOffset,
                       faceInfo.Location.Width + cropOffset * 2, faceInfo.Location.Height + cropOffset * 2);
}

Face Recognition

The approach to face recognition:

  • Detect the face
  • Determine the positions of facial landmarks (5-point/68-point)
  • Extract features based on the landmarks
  • Search for the person corresponding to that feature in a vector database

The last step uses a vector database, but it's not strictly necessary. The extracted face features are of type float[] and can theoretically be stored anywhere. During recognition, you can retrieve the face features and compare them by traversing your stored feature database.

The FaceRecognizer object provides a Compare function to calculate the similarity between two facial feature sets.

This feature is essentially a vector, so theoretically, you can calculate similarity using other algorithms yourself, such as

  • Distance-based methods like Euclidean distance, Manhattan distance
  • Cosine similarity
  • Pearson correlation coefficient

In the "Complete Usage" section on face detection above, the steps for detecting faces, locating landmarks, and extracting features are already completed.

What needs to be done next:

  • Entering face information: This requires a traditional relational database combined with a vector database, or a database like PostgreSQL that supports vector storage.
  • Comparing face information: Use the vector search functionality of a vector database, or implement your own vector search algorithm.

Due to space limitations, this article will not expand on the face recognition topic. I will write a separate article when I have time.

Deployment

Next, we'll address the pending tasks.

Deploying the application using Docker

This project uses .Net Core 7.0, so we correspondingly use the mcr.microsoft.com/dotnet/aspnet:7.0 base image.

This image is based on the Debian 11 system.

By default, it does not include the libgomp1 library, which needs to be added manually.

APT Software Sources

First, prepare the sources.list file to modify the APT repository to a domestic source.

# By default, the source code mirror is commented out to improve apt update speed; uncomment it if needed
deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye main contrib non-free
# deb-src https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye main contrib non-free

deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-updates main contrib non-free
# deb-src https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-updates main contrib non-free

deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-backports main contrib non-free
# deb-src https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-backports main contrib non-free
# deb https://mirrors.tuna.tsinghua.edu.cn/debian-security bullseye-security main contrib non-free
# # deb-src https://mirrors.tuna.tsinghua.edu.cn/debian-security bullseye-security main contrib non-free

deb https://security.debian.org/debian-security bullseye-security main contrib non-free
# deb-src https://security.debian.org/debian-security bullseye-security main contrib non-free

dockerfile

During the base build stage, install the libgomp1 library.

FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
COPY ./sources.list /etc/apt/sources.list
RUN apt update && apt install libgomp1
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM base AS final
WORKDIR /app
COPY . .
ENTRYPOINT ["./AIHub.Blazor"]

docker-compose.yml

version: '3.6'

services:
  web:
    image: ${DOCKER_REGISTRY-}web
    container_name: aihub
    restart: always
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
      - ASPNETCORE_URLS=http://+:80
    build:
      context: .
    volumes:
      - .:/app
    networks:
      - default
      - swag

networks:
  swag:
    name: swag
    external: true
  default:
    name: aihub

Start!

Everything is ready.

C#, start!

docker compose up --build -d

References

_EOF_

C# integrates the ViewFaceCore face detection and recognition library.

本文作者:
程序设计实验室
版权声明:
本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

相关文章

C#中的Attribute与Python-TypeScript的装饰器是同个东西吗

前言 最近成功把「前端带师」带入C#的坑~~(实际是前端带师开始从cocos转unity游戏开发了)~~ 某天,「前端带师」看到这段代码后问了个问题:[这个是装饰器]? [HttpGet] public Response Get() { return ... } 我第一反应觉得不是,这玩意在C#中叫“特性”(英文名Attribute,下文统称为特性),在Java中叫注解,虽然写法和Pyth

2022-06-26 · C# · 约 1 分钟

C#移除字符串中的不可见Unicode字符

背景 最近发现某个数据采集的系统拿下来的数据,有些字段的JSON被莫名截断了,导致后续数据分析的时候解析JSON失败。 类似这样 {title: 你好 或者这样,多了个双引号啥的 {title:你好} 因为数据库是Oracle,起初以为是Oracle这老古董出问题了,结果一番折腾,把每条写入数据的SQL语句都拿出来,看起来里面的JSON格式都没问题。 这也太诡异了吧,看起来没毛病,但就为啥JSON

2023-02-02 · C# · 约 1 分钟

讨论

共 0 条

良言一句三冬暖,恶语伤人六月寒。

加载评论中…

C# integrates the ViewFaceCore face detection and recognition library. · StarBlog