Floutage automatisé des visages : confidentialité avec AWS & .NET
Utilisez AWS Rekognition pour localiser les visages, puis floutez ces zones localement avec C# et Magick.NET. Cette application console crée un nouveau PNG que vous devez vérifier avant de le partager. La détection peut manquer des visages, et un floutage ne garantit pas l’anonymat : n’envoyez des photographies à AWS que si vous en avez l’autorisation, et vérifiez si le résultat contient des visages reconnaissables ou d’autres détails permettant d’identifier une personne.
Configurer votre projet .NET
Ces commandes utilisent Bash sous Linux et le SDK .NET 10.
Le SDK testé est la version 10.0.111 ; utilisez un SDK .NET 10 recevant les mises à jour de maintenance
pour vos nouveaux projets. Installez le SDK, plutôt que le seul environnement d’exécution, et
vérifiez que dotnet --version fonctionne avant de commencer.
Magick.NET inclut sa bibliothèque d’images native dans le paquet
NuGet ; cette application n’a donc pas besoin d’un exécutable ImageMagick distinct.
Commencez dans un répertoire accessible en écriture, en dehors d’une arborescence de sources .NET existante. La configuration refuse les configurations de compilation, de paquets, de SDK et de NuGet présentes dans les répertoires parents, afin que ce petit projet n’hérite pas de paramètres sans rapport avec lui. Collez ce bloc ; les parenthèses préservent le répertoire et les options de votre shell :
(
set -eu
command -v dotnet >/dev/null || { printf '%s\n' 'Install the .NET 10 SDK first.' >&2; exit 1; }
parent=$PWD
while :; do
for config in global.json Directory.Build.props Directory.Build.targets Directory.Packages.props NuGet.Config NuGet.config nuget.config; do
[ ! -e "$parent/$config" ] || { printf '%s\n' 'Use a directory outside an existing .NET configuration tree.' >&2; exit 1; }
done
[ "$parent" -ef / ] && break
parent="$parent/.."
done
[ ! -e FaceBlurAutomation ] || { printf '%s\n' 'Choose a new project directory; FaceBlurAutomation already exists.' >&2; exit 1; }
dotnet new console --framework net10.0 --no-restore -n FaceBlurAutomation
dotnet add FaceBlurAutomation package AWSSDK.Rekognition --version 4.0.101.2
dotnet add FaceBlurAutomation package Magick.NET-Q8-AnyCPU --version 14.17.2
)
Si l’installation échoue après la création du projet, conservez ce répertoire et relancez les deux
commandes d’installation des paquets une fois la connexion rétablie. La configuration complète
refuse délibérément de réutiliser un projet existant. Toutes les commandes suivantes s’exécutent
depuis le répertoire d’origine avec --project.
Pour ce tutoriel local, utilisez un profil existant d’informations d’accès partagées ou des
informations d’accès temporaires fournies par votre environnement. Configurez-les via la
chaîne de fournisseurs d’informations d’accès par défaut du SDK.
L’identité doit disposer de rekognition:DetectFaces. Dans votre session Bash, exportez
AWS_PROFILE avec le nom de votre profil d’informations d’accès partagées si
nécessaire, et AWS_REGION avec une région Rekognition telle que
us-east-1. Ne collez pas de clés dans le code.
Les analyses Rekognition entraînent des frais AWS. Cet exemple envoie directement les octets de
l’image ; il ne nécessite ni bucket S3 ni autorisations S3.
Comprendre AWS Rekognition
DetectFaces
renvoie des rectangles englobants pour les 100 plus grands visages au maximum. Chaque coordonnée
est une fraction de la largeur ou de la hauteur de l’image. Les visages situés sur un bord peuvent
produire des coordonnées hors de l’image ; le code arrondit donc les limites vers l’extérieur et
borne les rectangles. Les visages de face donnent les meilleurs résultats ; une réponse vide
justifie une vérification manuelle, mais ne prouve pas que l’image ne contient aucune personne.
Implémenter la détection et le floutage des visages
Utilisez un fichier JPEG ou PNG contenant une seule image, avec des pixels RGB ou en niveaux de gris sur 8 bits. Les PNG ne doivent avoir aucun canal alpha. L’exemple refuse le CMYK, les canaux alpha (y compris entièrement opaques), les profondeurs de couleur supérieures, les PNG animés et les JPEG contenant plusieurs images ; exportez d’abord une seule image au format PNG sRGB sur 8 bits sans alpha dans votre éditeur d’images. Les limites locales d’entrée sont de 20 MiB et de 20 millions de pixels décodés, chaque côté mesurant au moins 80 pixels. Ce sont les limites de ce tutoriel pour des photographies locales de confiance, pas un environnement isolé destiné à des fichiers téléversés malveillants.
L’application oriente les pixels, convertit les couleurs RGB associées à un profil en sRGB, supprime les profils et les commentaires de l’image d’entrée, et réduit les photographies dont le côté le plus long dépasse 1 600 pixels. Elle envoie à AWS un PNG contenant ces mêmes pixels et modifie cette image de travail. Une grande photographie produit donc un résultat plus petit ; examinez soigneusement les visages devenus plus petits. Si le PNG normalisé dépasse encore la limite de 5 MB pour l’envoi direct via Bytes, le message d’erreur vous demande d’exporter une photographie plus petite et de réessayer. La limite distincte de 15 MB pour les objets S3 ne s’applique pas ici.
Remplacez FaceBlurAutomation/Program.cs par ce programme complet :
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Amazon.Rekognition;
using Amazon.Rekognition.Model;
using ImageMagick;
namespace FaceBlurAutomation;
public static class Program
{
public static async Task<int> Main(string[] args)
{
try
{
if (args.Length != 2)
throw new ArgumentException("Usage: FaceBlurAutomation <inputImagePath> <newOutput.png>");
return await ProcessImageAsync(args[0], args[1]);
}
catch (Exception error)
{
string message = error switch
{
ArgumentException or InvalidDataException => error.Message,
OperationCanceledException => "AWS analysis timed out after 45 seconds. Check connectivity and retry with a new output path.",
AmazonRekognitionException => "AWS analysis failed. Check your profile, region, DetectFaces permission, and service availability.",
Amazon.Runtime.AmazonClientException => "AWS credentials or client setup failed. Check your shared-credentials profile and AWS_REGION.",
System.Net.Http.HttpRequestException => "AWS connectivity failed. Check your network and region before retrying.",
MagickException => "Image decoding or processing failed. Re-export an intact 8-bit RGB PNG and retry.",
IOException or UnauthorizedAccessException => "File access or publication failed. Check the input, output directory, and existing destination.",
_ => "Processing failed; no successful result was reported."
};
Console.Error.WriteLine(message);
return 1;
}
}
private static async Task<int> ProcessImageAsync(string inputPath, string outputPath)
{
outputPath = Path.GetFullPath(outputPath);
if (!string.Equals(Path.GetExtension(outputPath), ".png", StringComparison.OrdinalIgnoreCase))
throw new ArgumentException("Choose a .png output path.");
if (File.Exists(outputPath) || Directory.Exists(outputPath))
throw new ArgumentException("Choose an output path that does not already exist.");
if (!Directory.Exists(Path.GetDirectoryName(outputPath)))
throw new ArgumentException("Create the output directory first.");
using var input = new FileStream(inputPath, FileMode.Open, FileAccess.Read, FileShare.Read);
if (input.Length == 0 || input.Length > 20L * 1024 * 1024)
throw new InvalidDataException("Use a nonempty input no larger than 20 MiB; export a smaller image and retry.");
if (HasMultiplePictureIndex(input))
throw new InvalidDataException("This JPEG contains a multi-picture index. Export one picture as an opaque 8-bit RGB PNG first.");
if (HasPngAnimation(input))
throw new InvalidDataException("This PNG contains animation controls. Export one frame as an 8-bit RGB PNG without alpha first.");
using var headers = new MagickImageCollection();
headers.Ping(input);
if (headers.Count != 1)
throw new InvalidDataException("Export a single image as an opaque 8-bit RGB PNG first.");
var header = headers[0];
if (header.Format is not (MagickFormat.Jpeg or MagickFormat.Png)
|| header.Depth != 8 || header.HasAlpha
|| header.ColorSpace is not (ColorSpace.sRGB or ColorSpace.Gray))
throw new InvalidDataException("Use an 8-bit RGB or grayscale JPEG, or a PNG without an alpha channel; convert CMYK or alpha in an image editor first.");
if (header.Width < 80 || header.Height < 80
|| (ulong)header.Width * header.Height > 20_000_000)
throw new InvalidDataException("Use sides of at least 80 pixels and no more than 20 million pixels; resize in your image editor first.");
input.Position = 0;
using var image = new MagickImage();
image.Warning += (_, _) => throw new InvalidDataException("The decoder reported damaged image data. Re-export an intact image first.");
image.Read(input);
image.AutoOrient();
if (image.GetColorProfile() is not null)
image.TransformColorSpace(ColorProfiles.SRGB);
image.ColorSpace = ColorSpace.sRGB;
image.Strip();
if (image.Width > 1600 || image.Height > 1600)
image.Resize(new MagickGeometry(1600, 1600) { Greater = true });
if (image.Width < 80 || image.Height < 80)
throw new InvalidDataException("The resized image has a side below 80 pixels; crop the extreme aspect ratio first.");
using var bytes = new MemoryStream(image.ToByteArray(MagickFormat.Png));
if (bytes.Length > 5L * 1024 * 1024)
throw new InvalidDataException("The normalized PNG exceeds AWS’s 5 MB Bytes limit. Export with a longest side of 1200 pixels or less and retry.");
using var client = new AmazonRekognitionClient(new AmazonRekognitionConfig { MaxErrorRetry = 0 });
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(45));
var response = await client.DetectFacesAsync(new DetectFacesRequest
{
Image = new Amazon.Rekognition.Model.Image { Bytes = bytes },
Attributes = new List<string> { "DEFAULT" }
}, deadline.Token);
var faces = response.FaceDetails ?? new List<FaceDetail>();
if (faces.Count == 0)
{
Console.Error.WriteLine("No faces detected; no output written. Review the original manually.");
return 2;
}
foreach (var face in faces)
{
var box = face.BoundingBox;
if (box?.Left is not float left || box.Top is not float top
|| box.Width is not float width || box.Height is not float height
|| !float.IsFinite(left) || !float.IsFinite(top)
|| !float.IsFinite(width) || !float.IsFinite(height) || width <= 0 || height <= 0)
throw new InvalidDataException("AWS returned an invalid face bounding box; no output written.");
int x1 = (int)Math.Floor(Math.Clamp((double)left * image.Width, 0, image.Width));
int y1 = (int)Math.Floor(Math.Clamp((double)top * image.Height, 0, image.Height));
int x2 = (int)Math.Ceiling(Math.Clamp(((double)left + width) * image.Width, 0, image.Width));
int y2 = (int)Math.Ceiling(Math.Clamp(((double)top + height) * image.Height, 0, image.Height));
if (x2 <= x1 || y2 <= y1)
throw new InvalidDataException("A face box does not intersect the image; no output written.");
using var region = image.Clone();
region.Crop(new MagickGeometry(x1, y1, (uint)(x2 - x1), (uint)(y2 - y1)));
region.ResetPage();
double sigma = Math.Max(12, Math.Min(region.Width, region.Height) / 4.0);
// An explicit kernel radius keeps large-sigma blur effective with the Q8 library.
region.GaussianBlur(Math.Ceiling(3 * sigma), sigma);
image.Composite(region, x1, y1, CompositeOperator.Copy);
}
string staging = Path.Combine(Path.GetDirectoryName(outputPath)!,
$".faceblur-{Guid.NewGuid():N}.tmp");
try
{
using (var output = new FileStream(staging, FileMode.CreateNew, FileAccess.Write))
{
image.Write(output, MagickFormat.Png);
output.Flush(flushToDisk: true);
}
File.Move(staging, outputPath, overwrite: false);
}
finally
{
if (File.Exists(staging)) File.Delete(staging);
}
Console.WriteLine($"Blurred {faces.Count} detected face region(s). Review {outputPath} before sharing.");
return 0;
}
private static bool HasPngAnimation(Stream input)
{
// PNG decoders can expose only an APNG's default image; inspect its control chunks first.
try
{
Span<byte> header = stackalloc byte[8];
ReadOnlySpan<byte> signature = stackalloc byte[] { 137, 80, 78, 71, 13, 10, 26, 10 };
if (input.Read(header) != 8 || !header.SequenceEqual(signature)) return false;
while (input.Length - input.Position >= 12)
{
input.ReadExactly(header);
uint length = BinaryPrimitives.ReadUInt32BigEndian(header[..4]);
if ((long)length + 4 > input.Length - input.Position) break;
if (header[4..].SequenceEqual("acTL"u8)) return true;
// A valid animation control chunk must precede the first image-data chunk.
if (header[4..].SequenceEqual("IDAT"u8) || header[4..].SequenceEqual("IEND"u8)) break;
input.Position += length + 4L;
}
return false;
}
finally
{
input.Position = 0;
}
}
private static bool HasMultiplePictureIndex(Stream input)
{
// Some JPEG decoders expose only the primary picture of an MPF/MPO file.
try
{
if (input.ReadByte() != 0xff || input.ReadByte() != 0xd8) return false;
while (input.Position < input.Length)
{
if (input.ReadByte() != 0xff) break;
int marker;
do { marker = input.ReadByte(); } while (marker == 0xff);
if (marker < 0 || marker == 0xda || marker == 0xd9) break;
if (marker == 0x01 || marker is >= 0xd0 and <= 0xd7) continue;
int high = input.ReadByte();
int low = input.ReadByte();
if (high < 0 || low < 0) break;
int length = (high << 8) + low - 2;
if (length < 0 || length > input.Length - input.Position) break;
long next = input.Position + length;
if (marker == 0xe2 && length >= 4
&& input.ReadByte() == 'M' && input.ReadByte() == 'P'
&& input.ReadByte() == 'F' && input.ReadByte() == 0)
return true;
input.Position = next;
}
return false;
}
finally
{
input.Position = 0;
}
}
}
Enregistrez une photographie que vous avez l’autorisation d’analyser sous le nom
input.jpg dans le répertoire d’origine. Choisissez une nouvelle destination,
puis compilez et exécutez le programme :
dotnet build FaceBlurAutomation &&
dotnet run --no-build --project FaceBlurAutomation -- "./input.jpg" "./output.png"
Une exécution terminée indique le nombre de zones floutées et vous demande de vérifier
output.png.
Ouvrez ce fichier précis dans une visionneuse d’images et comparez-le à l’original : vérifiez chaque
visage visible, la rotation, la taille de l’image et si le floutage est suffisamment fort. Une
personne peut encore être identifiable par ses cheveux, ses vêtements, le contexte ou un visage
que le détecteur a manqué. Utilisez un nouveau nom de sortie pour une autre exécution ; le programme
préserve les fichiers existants.
Fonctionnement
L’orientation et le redimensionnement ont lieu avant la détection ; les coordonnées renvoyées correspondent donc aux pixels modifiés. Chaque rectangle borné est extrait, flouté, puis recopié à sa place ; les pixels situés hors de l’union de ces rectangles restent tels qu’ils étaient dans l’image de travail normalisée. Sigma vaut au moins 12 pixels et augmente avec le côté le plus court du rectangle englobant chaque visage. Le rayon du noyau est défini explicitement pour que ce floutage reste efficace avec le paquet Q8. L’effet est ainsi prononcé pour différentes tailles de visages, mais ce réglage n’est pas un seuil d’anonymat.
Le PNG est d’abord encodé dans un fichier intermédiaire portant un nom unique, à côté du fichier
de destination. Après sa fermeture, un déplacement sans écrasement le publie. Les échecs
d’écriture et de déplacement interceptés suppriment ce fichier intermédiaire ; un arrêt forcé du
processus ou une coupure de courant peut laisser un fichier .faceblur-*.tmp.
Examinez et supprimez uniquement le fichier temporaire appartenant à votre exécution interrompue.
Cet exemple local est prévu pour une seule exécution à la fois.
Gérer les erreurs et tester
Un code de sortie zéro signifie que le PNG a été publié et doit encore être vérifié. Un code de sortie deux signifie qu’aucun visage n’a été détecté et qu’aucun fichier de sortie n’a été écrit. Un code de sortie un couvre les entrées non valides, les échecs AWS et les échecs de traitement ou de publication des fichiers. Le contenu des exceptions AWS et les informations d’accès ne sont pas affichés.
Essayez une photographie avec plusieurs visages et une sans visage. Testez également un JPEG dont la rotation est définie par EXIF, une destination existante, une image corrompue et un répertoire de sortie indisponible. Les refus ne doivent laisser aucun nouveau fichier finalisé et doivent préserver l’entrée. En cas de dépassement d’une limite d’octets ou de pixels, redimensionnez et exportez localement ; en cas de refus lié aux couleurs ou au format, exportez une seule image au format PNG sRGB sur 8 bits sans alpha. Un délai d’expiration s’applique à la requête AWS, pas à l’ensemble de l’exécution du traitement d’image. Vérifiez la connexion avant de réessayer ; les nouvelles tentatives du SDK sont désactivées dans cet exemple.
Étendre votre application
Une API web nécessite ses propres contrôles de validation des téléversements, limites de ressources, autorisations et stockage temporaire privé ; l’exemple ci-dessus, conçu pour des fichiers de confiance, ne fournit pas ces contrôles. Une source d’entrée S3 nécessite également un accès S3, et l’objet doit se trouver dans la même région que Rekognition. Si vous ajoutez du stockage, gardez la détection et le floutage liés à la même image orientée et redimensionnée, plutôt que de détecter les visages dans une version et d’en modifier une autre.
Résumé
Conservez la photographie d’origine jusqu’à ce que vous ayez examiné le PNG et traité manuellement les visages éventuellement manqués. Pour d’autres tâches d’analyse d’images avec AWS, le Robot 🤖 /image/describe de Transloadit renvoie des étiquettes d’objets ; il ne remplace pas les étapes de floutage des visages et de vérification présentées ici.
