Gesichter automatisch weichzeichnen: Datenschutz mit AWS & .NET
Erkennen Sie Gesichter mit AWS Rekognition und zeichnen Sie diese Bereiche anschließend lokal mit C# und Magick.NET weich. Diese Konsolenanwendung erstellt ein neues PNG, das Sie vor dem Teilen prüfen können. Die Erkennung kann Gesichter übersehen, und Weichzeichnen garantiert keine Anonymität: Senden Sie Fotos nur mit entsprechender Erlaubnis an AWS und prüfen Sie das Ergebnis auf erkennbare Gesichter und andere identifizierende Details.
Ihr .NET-Projekt einrichten
Diese Befehle verwenden Bash unter Linux und das .NET 10 SDK.
Die getestete SDK-Version ist 10.0.111; verwenden Sie für neue Projekte ein aktuell gewartetes
.NET 10 SDK. Installieren Sie das SDK statt nur der Laufzeitumgebung und prüfen Sie vor dem Start,
ob dotnet --version funktioniert.
Magick.NET enthält seine native Bildbibliothek im NuGet-Paket.
Diese Anwendung benötigt daher keine separate ausführbare ImageMagick-Datei.
Beginnen Sie in einem beschreibbaren Verzeichnis außerhalb eines bestehenden .NET-Quellbaums. Das Setup akzeptiert keine Build-, Paket-, SDK- oder NuGet-Konfigurationen aus übergeordneten Verzeichnissen, damit dieses kleine Projekt keine fremden Einstellungen übernimmt. Fügen Sie folgenden Block ein; die Klammern lassen das Verzeichnis und die Optionen Ihrer Shell unverändert:
(
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
)
Falls die Installation nach dem Erstellen des Projekts fehlschlägt, behalten Sie das Verzeichnis bei
und führen Sie die beiden Paketbefehle erneut aus, sobald die Verbindung wiederhergestellt ist.
Das gesamte Setup lehnt die Wiederverwendung eines bestehenden Projekts bewusst ab. Alle späteren
Befehle werden mit --project aus dem ursprünglichen Verzeichnis ausgeführt.
Verwenden Sie für diese lokale Anleitung ein bestehendes Profil aus der gemeinsamen
Zugangsdaten-Datei oder temporäre Zugangsdaten aus Ihrer Umgebung. Konfigurieren Sie diese mithilfe
von der Standardkette der Zugangsdatenanbieter im SDK.
Die Identität benötigt rekognition:DetectFaces. Exportieren Sie bei Bedarf
AWS_PROFILE mit dem Namen Ihres Profils aus der gemeinsamen Zugangsdaten-Datei
sowie AWS_REGION mit einer Rekognition-Region wie
us-east-1 in Ihrer Bash-Sitzung. Fügen Sie keine Schlüssel in den Code ein.
Rekognition-Analysen verursachen AWS-Gebühren. Dieses Beispiel sendet Bildbytes direkt und benötigt
weder einen S3-Bucket noch S3-Berechtigungen.
AWS Rekognition verstehen
DetectFaces
liefert Begrenzungsrahmen für bis zu 100 der größten Gesichter. Jede Koordinate ist ein Bruchteil
der Bildbreite oder Bildhöhe. Gesichter am Rand können Koordinaten außerhalb des Bildes erzeugen.
Der Code rundet daher nach außen und begrenzt die Rechtecke auf das Bild. Frontalansichten von
Gesichtern funktionieren am besten. Eine leere Antwort ist ein Anlass zur manuellen Prüfung und
kein Beweis dafür, dass ein Bild keine Menschen enthält.
Gesichtserkennung und Weichzeichnen implementieren
Verwenden Sie eine JPEG- oder PNG-Datei mit genau einem Bild und 8-Bit-RGB- oder Graustufenpixeln. PNGs dürfen keinen Alphakanal enthalten. Das Beispiel lehnt CMYK, Alphakanäle (auch vollständig undurchsichtige), höhere Bittiefen, animierte PNGs und JPEGs mit mehreren Bildern ab. Exportieren Sie zunächst in Ihrem Bildeditor ein einzelnes Bild als 8-Bit-sRGB-PNG ohne Alphakanal. Die lokalen Eingabegrenzen betragen 20 MiB und 20 Millionen decodierte Pixel; beide Seiten müssen mindestens 80 Pixel lang sein. Diese Grenzen dienen der Anleitung für vertrauenswürdige lokale Fotos und bilden keine Sandbox für bösartige Uploads.
Die Anwendung richtet die Pixel aus, konvertiert RGB-Farben mit Farbprofil nach sRGB, entfernt Eingabeprofile und Kommentare und verkleinert Fotos, deren längste Seite 1.600 Pixel überschreitet. Sie sendet ein PNG mit genau diesen Pixeln an AWS und bearbeitet dieses Arbeitsbild. Ein großes Foto ergibt daher eine kleinere Ausgabe; prüfen Sie die kleineren Gesichter sorgfältig. Wenn das normalisierte PNG weiterhin die Grenze von 5 MB für direkte Bytes überschreitet, fordert die Fehlermeldung Sie auf, ein kleineres Foto zu exportieren und es erneut zu versuchen. Die separate Grenze von 15 MB für S3-Objekte gilt hier nicht.
Ersetzen Sie FaceBlurAutomation/Program.cs durch dieses vollständige Programm:
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;
}
}
}
Speichern Sie ein Foto, das Sie analysieren dürfen, als input.jpg im
ursprünglichen Verzeichnis. Wählen Sie einen neuen Zielpfad, kompilieren Sie dann das Programm
und führen Sie es aus:
dotnet build FaceBlurAutomation &&
dotnet run --no-build --project FaceBlurAutomation -- "./input.jpg" "./output.png"
Ein abgeschlossener Durchlauf meldet die Anzahl der weichgezeichneten Bereiche und fordert Sie auf,
output.png zu prüfen. Öffnen Sie genau diese Datei in einem Bildbetrachter und
vergleichen Sie sie mit dem Original: Prüfen Sie jedes sichtbare Gesicht, die Drehung, die Bildgröße
und ob das Weichzeichnen stark genug ist. Eine Person kann weiterhin anhand von Haaren, Kleidung,
Kontext oder einem Gesicht erkennbar sein, das die Erkennung übersehen hat. Verwenden Sie für einen
weiteren Durchlauf einen neuen Ausgabenamen; das Programm bewahrt bestehende Dateien.
So funktioniert es
Ausrichtung und Größenänderung erfolgen vor der Erkennung. Die zurückgegebenen Koordinaten beziehen sich daher auf die Pixel, die bearbeitet werden. Jeder begrenzte rechteckige Bereich wird zugeschnitten, weichgezeichnet und zurückkopiert. Pixel außerhalb der Vereinigung dieser Rechtecke bleiben wie im normalisierten Arbeitsbild. Sigma beträgt mindestens 12 Pixel und wächst mit der kürzeren Seite jedes Gesichtsrahmens. Der Kernelradius wird explizit festgelegt, damit das Weichzeichnen mit dem Q8-Paket wirksam bleibt. Dadurch ist der Effekt bei unterschiedlichen Gesichtsgrößen deutlich, doch die Einstellung ist kein Schwellenwert für Anonymität.
Das PNG wird zunächst in eine Zwischendatei mit eindeutigem Namen neben der Zieldatei codiert.
Nachdem die Zwischendatei geschlossen wurde, wird sie ohne Überschreiben an den Zielpfad verschoben.
Bei abgefangenen Schreib- und Verschiebefehlern wird diese Zwischendatei entfernt. Ein erzwungener
Prozessabbruch oder Stromausfall kann eine Datei namens .faceblur-*.tmp zurücklassen.
Prüfen und entfernen Sie nur die temporäre Datei, die zu Ihrem unterbrochenen Durchlauf gehört.
Dieses lokale Beispiel ist für jeweils einen Durchlauf vorgesehen.
Fehlerbehandlung und Tests
Exit-Code null bedeutet, dass das PNG am Ziel bereitgestellt wurde und noch geprüft werden muss. Exit-Code zwei bedeutet, dass keine Gesichter erkannt wurden und keine Ausgabe geschrieben wurde. Exit-Code eins umfasst ungültige Eingaben, AWS-Fehler sowie Fehler bei der Dateiverarbeitung oder beim Bereitstellen am Ziel. AWS-Ausnahmetexte und Zugangsdaten werden nicht ausgegeben.
Probieren Sie ein Foto mit mehreren Gesichtern und eines ohne Gesichter aus. Testen Sie außerdem ein per EXIF gedrehtes JPEG, einen bestehenden Zielpfad, ein beschädigtes Bild und ein nicht verfügbares Ausgabeverzeichnis. Bei einer Ablehnung sollte keine neue endgültige Datei entstehen, und die Eingabe sollte erhalten bleiben. Wird eine Byte- oder Pixelgrenze erreicht, verkleinern und exportieren Sie das Bild lokal. Wird eine Farbe oder ein Format abgelehnt, exportieren Sie ein Bild als 8-Bit-sRGB-PNG ohne Alphakanal. Ein Timeout gilt für die AWS-Anfrage, nicht für den gesamten Bildverarbeitungsdurchlauf. Prüfen Sie vor einem erneuten Versuch die Verbindung; die Retries des SDK sind in diesem Beispiel deaktiviert.
Ihre Anwendung erweitern
Eine Web-API benötigt eine eigene Upload-Validierung, Ressourcenlimits, Autorisierung und privaten temporären Speicher. Das obige Beispiel für vertrauenswürdige Dateien bietet diese Schutzmaßnahmen nicht. Ein S3-Eingabepfad benötigt außerdem S3-Zugriff, und das Objekt muss sich in derselben Region wie Rekognition befinden. Wenn Sie Speicher hinzufügen, binden Sie Erkennung und Weichzeichnen an dasselbe ausgerichtete Bild mit angepasster Größe, statt eine Version zu analysieren und eine andere zu bearbeiten.
Zusammenfassung
Bewahren Sie das Originalfoto auf, bis Sie das PNG geprüft und übersehene Gesichter manuell bearbeitet haben. Für andere AWS-Aufgaben zur Bildanalyse liefert der 🤖 /image/describe Robot von Transloadit Objektlabels; er ersetzt nicht das Weichzeichnen von Gesichtern und die hier beschriebenen Prüfschritte.
