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.