Trabalhei em um projeto para uma instituição financeira cujo o objetivo era minimizar os problemas de fraude nas aberturas de conta. Quando iniciamos o projeto buscamos alguns fornecedores, mas ou tinham uma solução muito cara ou não atendia nossas expectativas.

Eu já tinha ouvido falar de uma solução de detecção de rostos da AWS, foi aí que em uma simples pesquisa descobrimos o AWS Rekognition.

Quando começamos a trabalhar com esta ferramenta eu fiquei alucinado. A implementação é muito simples e o resultado foi muito melhor do que esperávamos e já fazia tempo que queria escrever e compartilhar sobre este assunto.

O AWS Rekognition facilita a adição de análises de imagem e vídeo usando tecnologia escalável de aprendizagem profunda e não requer conhecimentos de machine learning para usar. Em outras palavras, era o que precisávamos.

Neste projeto em questão nós utilizamos os recursos de CompareFaces, mas aqui neste artigo vamos falar sobre CompareFaces e DetectFaces.

Estruturando o projeto

Para iniciar nosso projeto vamos começar criando uma Solution e, em seguida, criar um projeto WebApi e anexar a Solution criada.

dotnet new sln -n AwsRekognitionProject
dotnet new webapi -n AwsRekognitionProject.Api
dotnet sln add AwsRekognitionProject.ApiAwsRekognitionProject.Api.csproject

Em seguida vamos criar os diretórios do projeto.

  • Domain: Armazena as interfaces que vamos utilizar na aplicação;
  • Entities: Armazena as classes de entidade;
  • Images: Armazena as imagens que serão geradas pela aplicação;
  • Services: Armazena as classes de lógica de negócio.

O resultado deve ser este:

Importante lembrar que não tenho a intenção de ser um DDD purista neste projeto. Por isto algumas classes podem não estar (e não estão) no seu devido lugar.

Instalando os packages

Após configurarmos a estrutura do nosso projeto vamos instalar os packages que vamos utilizar.

Visual Studio

Install-Package AWSSDK.Rekognition
Install-Package System.Drawing.Common

VSCode

dotnet add package AWSSDK.Rekognition
dotnet add package System.Drawing.Common

A biblioteca Libgdiplus

Aqui eu precisei incluir uma biblioteca para o Linux poder utilizar o System.Drawing.Common. Se você também utiliza Linux execute o comando abaixo no terminal para instalar o libgdiplus.

O libgdiplus é um biblioteca Mono que fornece uma API GDI + compatível para sistemas operacionais não Windows.

sudo apt install libgdiplus

Entities

Vamos iniciar a configuração da nossa aplicação pelas entidades. Aqui vamos utilizar o conceito de entidades para Request e Response. Inclusive a AWS utiliza o mesmo conceito para os serviços das suas APIs.

Vamos trabalhar com o conceito de duas entidades, mas para cada uma teremos uma classe Request e uma Response.

FaceMatchRequest.cs e FaceMatchResponse.cs

public class FaceMatchRequest
{
    public string SourceImage { get; set; }
    public string TargetImage { get; set; }
}
public class FaceMatchResponse
{
    public FaceMatchResponse(bool match, float? similarity, string fileName)
    {
        Match = match;
        Similarity = similarity;
        DrawnImage = fileName;
    }

    public bool Match { get; private set; }
    public float? Similarity { get; private set; }
    public string DrawnImage { get; private set; }
}

Depois criamos mais duas classes para a entidade FindFaces.

FindFacesRequest.cs e FindFacesResponse.cs

public class FindFacesRequest
{
    public string SourceImage { get; set; }
}
public class FindFacesResponse
{
    public FindFacesResponse(string fileName)
    {
        DrawnImage = fileName;
    }

    public string DrawnImage { get; private set; }
}

Como o próprio nome das classes já dizem, teremos uma para receber as requisições (Request) e outra para devolver o resultado para o cliente (Response).

Esta é só uma das formas de se trabalhar com APIs. Também poderíamos usar DTOs, mas preferi, neste projeto, manter o conceito da API da AWS.

Domain

Nesta camada vamos colocar as interfaces do projeto. Todas as chamadas de funções serão feitas através das Interfaces. Isto facilita na implementação de testes e na abstração da implementação.

IServiceUtils.cs

public interface IServiceUtils
{
    MemoryStream ConvertImageToMemoryStream(string imageBase64);
    string Drawing(MemoryStream imageSource, aws.ComparedSourceImageFace faceMatch);
    string Drawing(MemoryStream imageSource, List<aws.FaceDetail> faceDetails);
}

IServiceDetectFaces.cs

public interface IServiceDetectFaces
{
    Task<FindFacesResponse> DetectFacesAsync(string sourceImage);
}

IServiceCompareFaces.cs

public interface IServiceCompareFaces
{
    Task<FaceMatchResponse> CompareFacesAsync(string sourceImage, string targetImage);
}

Services

Aqui vamos trabalhar com a lógica da nossa aplicação. Como eu havia dito, o intuito aqui não é sermos puristas em DDD, portanto, vamos fazer o mais simples possível para focarmos nas funcionalidades do AWS Rekognition.

Vamos começar com o serviço de utilitários, o ServiceUtils. Nesta classe vamos colocar os métodos que vão servir as outras classes.

Antes de irmos para a implementação vamos falar um pouco sobre um objeto do AWS Rekognition, o BoundingBox.

Calculando o BoundingBox

Quando fazemos uma chamada no CompareFaces ou no DetectFaces o AWS Rekognition nos retorna um objeto longo. Uma parte deste objeto é BoudingBox, que mostra as posições dos elementos encontrados nas imagens.

O Left (coordenada x) e o Top (coordenada y) são coordenadas que representam a posição do quadrado que delimita o elemento encontrado. Consideramos o canto superior esquerdo como origem da imagem (0.0).

Os valores Top e Left retornados são uma proporção do tamanho total da imagem. Por exemplo, se a imagem fonte enviada for 700×200 pixels e a coordenada Top e Left da caixa delimitadora for 350×50 pixels, a API retornará um valor Left de 0.5 (350/700) e um valor Top de 0.25 (50/200).

Os valores Width e Height representam as dimensões da caixa delimitadora também como uma proporção da dimensão total da imagem. Por exemplo, se a imagem fonte enviada tiver 700 x 200 pixels e o Width da caixa delimitadora for 70 pixels, o Width retornado será 0.1.

Fonte da documentação do BoundingBox aqui.

Agora sim, vamos para as implementações da aplicação.

ServiceUtils.cs

public class ServiceUtils : IServiceUtils
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public ServiceUtils(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }
    
    // Converte uma imagem string base64 em MemoryStream
    public MemoryStream ConvertImageToMemoryStream(string imageBase64)
    {
        var bytes = Convert.FromBase64String(imageBase64);
        return new MemoryStream(bytes);
    }

    // Função sobrecarga que recebe o parâmetro da CompareFaces
    public string Drawing(MemoryStream imageSource, aws.ComparedSourceImageFace faceMatch)
    {
        // Cria uma lista de quadrados que será desenhada na imagem
        var squares = new List<aws.BoundingBox>();
        
        // Adiciona as posições dos quadrados que serão desenhados
        squares.Add(
            new aws.BoundingBox{
                Left = faceMatch.BoundingBox.Left,
                Top = faceMatch.BoundingBox.Top,
                Width = faceMatch.BoundingBox.Width,
                Height = faceMatch.BoundingBox.Height
            }
        );

        return Drawing(imageSource, squares);
    }

    // Função sobrecarga que recebe o parâmetro da DetectFaces
    public string Drawing(MemoryStream imageSource, List<aws.FaceDetail> faceDetails)
    {
        // Cria uma lista de quadrados que será desenhada na imagem
        var squares = new List<aws.BoundingBox>();
        
        // Adiciona as posições dos quadrados que serão desenhados
        faceDetails.ForEach(f => {
            squares.Add(
                new aws.BoundingBox{
                    Left = f.BoundingBox.Left,
                    Top = f.BoundingBox.Top,
                    Width = f.BoundingBox.Width,
                    Height = f.BoundingBox.Height
                }
            );
        });
        
        return Drawing(imageSource, squares);
    }

    // Função que desenha os quadrados na imagem fonte
    private string Drawing(MemoryStream imageSource, List<aws.BoundingBox> squares)
    {
        //Converte o MemoryStream da imagem fonte em Imagem (System.Drawing)
        var image = Image.FromStream(imageSource);
        // O Graphics permite desenhar novos recursos na imagem fonte
        var graphic = Graphics.FromImage(image);
        // Objeto caneta que será usada para desenhar os quadrados
        var pen = new Pen(Brushes.Red, 5f);

        // Desenha os quadrados na imagem fonte
        squares.ForEach(b => {
            graphic.DrawRectangle(
                pen, 
                b.Left * image.Width, 
                b.Top * image.Height, 
                b.Width * image.Width, 
                b.Height * image.Height
            );
        });

        // Cria o nome do arquivo com o Guid
        var fileName = $"{Guid.NewGuid()}.jpg";

        // Salva a nova imagem desenhada
        image.Save($"Images/{fileName}", ImageFormat.Jpeg);

        // Chama a função e retorna a URL com a imagem gerada
        return GetUrlImage(fileName);
    }

    // Gera uma URL com a imagem criada
    private string GetUrlImage(string fileName)
    {
        var request = _httpContextAccessor.HttpContext.Request;
        var urlImage = $"{request.Scheme}://{request.Host.ToUriComponent()}/images/{fileName}";
        
        return urlImage;
    }
}

ServiceCompareFaces.cs

public class ServiceCompareFaces : IServiceCompareFaces
{
    private readonly IServiceUtils _serviceUtils;
    private readonly AmazonRekognitionClient _rekognitionClient;

    public ServiceCompareFaces(IServiceUtils serviceUtils)
    {
        _serviceUtils = serviceUtils;
        _rekognitionClient = new AmazonRekognitionClient();
    }

    public async Task<FaceMatchResponse> CompareFacesAsync(string sourceImage, string targetImage)
    {
        // Converte a imagem fonte em um objeto MemoryStream
        var imageSource = new Amazon.Rekognition.Model.Image();
        imageSource.Bytes = _serviceUtils.ConvertImageToMemoryStream(sourceImage);

        // Converte a imagem alvo em um objeto MemoryStream
        var imageTarget = new Amazon.Rekognition.Model.Image();
        imageTarget.Bytes = _serviceUtils.ConvertImageToMemoryStream(targetImage);

        // Configura o objeto que fará o request para o AWS Rekognition
        // A propriedade SimilarityThreshold ajusta o nível de similaridade na comparação das imagens
        var request = new CompareFacesRequest
        {
            SourceImage = imageSource,
            TargetImage = imageTarget,
            SimilarityThreshold = 80f
        };

        // Faz a chamada do serviço de CompareFaces
        var response = await _rekognitionClient.CompareFacesAsync(request);
        // Verifica se houve algum match nas imagens
        var hasMatch = response.FaceMatches.Any();

        // Se não houve match ele retorna um objeto não encontrado
        if (!hasMatch)
        {
            return new FaceMatchResponse(hasMatch, null, string.Empty);
        }

        // Com a imagem fonte e os parâmetros de retorno do match contornamos o rosto encontrado na imagem
        var fileName = _serviceUtils.Drawing(imageSource.Bytes, response.SourceImageFace);
        // Pega o percentual de similaridade da imagem encontrada
        var similarity = response.FaceMatches.FirstOrDefault().Similarity;

        // Retorna o objeto com as informações encontradas e com a URL para verificar a imagem
        return new FaceMatchResponse(hasMatch, similarity, fileName);
    }
}

ServiceDetectFaces.cs

public class ServiceDetectFaces : IServiceDetectFaces
{
    private readonly IServiceUtils _serviceUtils;
    private readonly AmazonRekognitionClient _rekognitionClient;

    public ServiceDetectFaces(IServiceUtils serviceUtils)
    {
        _serviceUtils = serviceUtils;
        _rekognitionClient = new AmazonRekognitionClient();
    }

    public async Task<FindFacesResponse> DetectFacesAsync(string sourceImage)
    {
        // Converte a imagem fonte em um objeto MemoryStream
        var imageSource = new Image();
        imageSource.Bytes = _serviceUtils.ConvertImageToMemoryStream(sourceImage);

        // Configura o objeto que fará o request para o AWS Rekognition
        var request = new DetectFacesRequest
        {
            Attributes = new List<string>{ "DEFAULT" },
            Image = imageSource
        };

        // Faz a chamada do serviço de DetectFaces            
        var response = await _rekognitionClient.DetectFacesAsync(request);
        // Chama a função de desenhar quadrados e pega a URL gerada
        var fileName = _serviceUtils.Drawing(imageSource.Bytes, response.FaceDetails);

        // Retorna o objeto com a URL gerada
        return new FindFacesResponse(fileName);
    }
}

Controllers

Agora vamos configurar a Controller que fará as chamadas para as funções que criamos até agora.

FacesController.cs

[ApiController]
[Route("api/[controller]")]
public class FacesController : ControllerBase
{
    private readonly IServiceDetectFaces _serviceDetectFaces;
    private readonly IServiceCompareFaces _serviceCompareFaces;

    public FacesController(
        IServiceDetectFaces serviceDetectFaces,
        IServiceCompareFaces serviceCompareFaces)
    {
        _serviceDetectFaces = serviceDetectFaces;
        _serviceCompareFaces = serviceCompareFaces;
    }

    [HttpGet("facematch")]
    public async Task<IActionResult> GetFaceMatches([FromBody] FaceMatchRequest faceMatchRequest)
    {
        try
        {
            var result = await _serviceCompareFaces.CompareFacesAsync(
                faceMatchRequest.SourceImage, 
                faceMatchRequest.TargetImage
            );

            return StatusCode(HttpStatusCode.OK.GetHashCode(), result);
        }
        catch (Exception ex)
        {
            return StatusCode(HttpStatusCode.InternalServerError.GetHashCode(), ex.Message);
        }
    }

    [HttpGet("findfaces")]
    public async Task<IActionResult> GetFaceMatches([FromBody] FindFacesRequest request)
    {
        try
        {
            var response = await _serviceDetectFaces.DetectFacesAsync(
                request.SourceImage
            );

            return StatusCode(HttpStatusCode.OK.GetHashCode(), response);
        }
        catch (Exception ex)
        {
            return StatusCode(HttpStatusCode.InternalServerError.GetHashCode(), ex.Message);
        }
    }
}

Configurando Startup

Por fim, vamos fazer alguns ajustes no Startup.cs para nossa aplicação ficar pronta para uso.

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
        
        // Habilita a API a exibir arquivos em diretórios pelo navegador
        services.AddDirectoryBrowser();

        // Injeção de dependência das classes e interfaces
        services.AddTransient<IServiceUtils, ServiceUtils>();
        services.AddTransient<IServiceDetectFaces, ServiceDetectFaces>();
        services.AddTransient<IServiceCompareFaces, ServiceCompareFaces>();

        // Singleton da classe que utilizamos para criar a URL
        services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        // Configura o diretório Images para fornecer arquivos estáticos
        app.UseStaticFiles(new StaticFileOptions
        {
            FileProvider = new PhysicalFileProvider(
                Path.Combine(Directory.GetCurrentDirectory(), "Images")),
            RequestPath = "/images"
        });

        // Configura o diretório Images para exibir as imagens pelo navegador
        app.UseDirectoryBrowser(new DirectoryBrowserOptions
        {
            FileProvider = new PhysicalFileProvider(
                Path.Combine(Directory.GetCurrentDirectory(), "Images")),
            RequestPath = "/images"
        });

        app.UseHttpsRedirection();
        app.UseRouting();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}

Testando a API

Agora é só executar o projeto e abrir chamarmos as funções pelo Postman.

Neste exemplo vamos utilizar Imagens da atriz Meryl Streep. Vamos pegar uma imagem com o rosto somente e vamos converter em base64. O resultado vamos colocar na propriedade targetImage do body da nossa requisição

Em seguida vamos pegar outras imagens da Meryl Streep com outras pessoas. Vamos converter a imagem em base64 e colocar na propriedade sourceImage.

Para converter as imagens em base64 https://www.base64-image.de/

Resultados do Compare Faces

Para o Detect Faces vamos pegar uma imagem com vários rostos, converter em base64 e fazer a chamada do Detect Faces.

Resultados do Detect Faces

Eu ainda estou explorando bastante o AWS Rekognition e cada vez fico mais impressionado.

Quando utilizei o CompareFaces no projeto de antifraude nós resolvemos de forma simples e barata um problema que antes era complexo e caro.

Os fontes deste artigo podem ser encontrados no meu GitHub.