Gesichter verpixeln: Ruby & ImageMagick
Lokalisieren Sie Gesichter mit Google Cloud Vision und ersetzen Sie diese Bereiche mit RMagick durch sichtbare Pixelblöcke. Das folgende Ruby-Programm dreht die Pixel gemäß der EXIF-Ausrichtung, bevor es sie an Vision sendet. Anschließend bearbeitet es dasselbe Bild und speichert ein PNG zur Überprüfung.
Warum Gesichter verpixeln?
Verpixelung reduziert sichtbare Gesichtsdetails, doch eine Erkennung kann Gesichter übersehen, und ein verpixeltes Gesicht kann weiterhin identifizierbar sein. Betrachten Sie die Ausgabe als Ergebnis, das noch überprüft werden muss. Prüfen Sie jedes Gesicht, auch Personen an den Bildrändern, und berücksichtigen Sie Namen, Kleidung, Spiegelungen und andere identifizierende Details, bevor Sie das Bild teilen. Das Originalbild wird zur Erkennung an Google gesendet.
Ruby und ImageMagick einrichten
Diese Anleitung setzt eine Linux-Shell voraus, in der MRI Ruby 4.0, ImageMagick 7 mit JPEG- und
PNG-Unterstützung, die zugehörigen Entwicklungsheader, ein C++-Compiler,
make und pkg-config bereits installiert sind. Getestet wurden
Ruby 4.0.7, ImageMagick 7.1.2-31, RMagick 7.2.0 und google-cloud-vision 2.2.1.
Verwenden Sie eine gepflegte Ruby-Patchversion von der
Ruby-Downloadseite. Die
Voraussetzungen für RMagick nennen die Systempakete, die Ihre
Distribution benötigt; das Gem erstellt eine native Erweiterung gegen diese Header.
Führen Sie diesen Block in einem beschreibbaren Verzeichnis aus. Er erstellt ein neues Verzeichnis
face-pixelation und legt die Gems darin ab. Existiert das Verzeichnis bereits, wird die
Einrichtung abgebrochen, ohne es zu ersetzen. Die übergeordnete Shell bleibt sowohl bei Erfolg als
auch bei einem Fehler in ihrem ursprünglichen Verzeichnis.
(
command -v ruby >/dev/null || exit 1
command -v make >/dev/null || exit 1
command -v g++ >/dev/null || exit 1
command -v pkg-config >/dev/null || exit 1
ruby -e 'abort "Use MRI Ruby 4.0" unless RUBY_ENGINE == "ruby" && RUBY_VERSION.start_with?("4.0.")' || exit 1
pkg-config --exists MagickCore || exit 1
mkdir face-pixelation || exit 1
cd face-pixelation || exit 1
GEM_SPEC_CACHE="$PWD/gem-spec-cache" ruby -S gem install \
--install-dir "$PWD/gems" --no-user-install --no-document \
rmagick:7.2.0 google-cloud-vision:2.2.1
)
Falls die Installation fehlschlägt, bewahren Sie die Compilerdiagnose auf, ergänzen Sie die fehlende
Voraussetzung und wiederholen Sie den Befehl zur Gem-Installation im erstellten Verzeichnis.
Wiederholen Sie weder mkdir noch löschen Sie ein bestehendes Projekt, um die
Einrichtung zum Erfolg zu bringen. Führen Sie das Beispiel außerhalb von
bundle exec aus; diese Anleitung verwendet ein eigenes Gem-Verzeichnis statt des
Gemfile einer umgebenden Anwendung.
Google Cloud Vision API einrichten
Folgen Sie Googles Anleitung zur Einrichtung von Vision, um ein Projekt mit aktivierter Abrechnung und aktivierter Vision API einzurichten. Die Erkennung ist ein kostenpflichtiger Cloud-Vorgang; prüfen Sie vor einer Stapelverarbeitung die Kontingente und Limits des Projekts. Dieses Beispiel sendet pro Ausführung ein lokales Bild und benötigt keinen Cloud-Storage-Bucket.
Installieren Sie für die lokale Entwicklung die Google Cloud CLI und folgen Sie
Googles ADC-Anleitung.
Führen Sie gcloud auth application-default login und anschließend
gcloud auth application-default set-quota-project PROJECT_ID aus. Ersetzen Sie dabei PROJECT_ID durch Ihr
Abrechnungsprojekt. Die Identität benötigt serviceusage.services.use in diesem Projekt. Diese
Zugangsdaten sind von den gewöhnlichen Anmeldedaten für gcloud getrennt.
Das Programm verwendet Application Default Credentials. Eine vorhandene Umgebungsvariable
GOOGLE_APPLICATION_CREDENTIALS hat Vorrang vor der lokalen ADC-Datei. Prüfen Sie daher, ob sie die
beabsichtigten Zugangsdaten auswählt. Laden Sie für dieses lokale Beispiel keinen
Dienstkontoschlüssel herunter. Für eine Google-Cloud-Workload empfiehlt Google das Dienstkonto,
das dieser Workload zugewiesen ist.
Gesichtserkennung mit der Google Cloud Vision API in Ruby integrieren
Verwenden Sie ein unbeschädigtes, statisches 8-Bit-JPEG oder -PNG in sRGB oder Graustufen, ohne Transparenz oder eingebettetes ICC-Profil. Exportieren Sie andere Farbräume und transparente Bilder zuerst als undurchsichtiges sRGB-Bild. Das Programm akzeptiert Eingaben bis höchstens 2 MiB, 4.096 Pixel pro Seite und insgesamt 4.000.000 Pixel. Außerdem begrenzt es das normalisierte PNG, das an Vision gesendet wird, auf 8 MiB. Eine komprimierte Eingabe kann diese zweite Grenze nach dem Decodieren überschreiten; reduzieren Sie ihre Abmessungen und versuchen Sie es erneut.
Die Dokumentation der Gesichtsantworten von Vision warnt davor, dass bei teilweise sichtbaren Gesichtern Polygonkoordinaten fehlen können. Ganzzahlfelder in Protobuf können beim Auslesen null ergeben, wenn sie nicht vorhanden sind. Werden Minimum und Maximum dieser Werte verwendet, kann der falsche Bereich ausgewählt werden. Dieses Beispiel akzeptiert nur vier Eckpunkte, die ein geordnetes, achsenparalleles Rechteck mit positiven Abmessungen innerhalb des Bildes bilden. Ein vollständiges Rechteck darf die Koordinate null berühren. Unvollständige, invertierte, gedrehte oder anderweitig mehrdeutige Polygone stoppen die gesamte Ausführung, ohne eine Ausgabedatei bereitzustellen. Prüfen Sie solche Bilder manuell, statt ein Rechteck zu erraten.
Verpixelungseffekte mit ImageMagick anwenden
Für jedes akzeptierte Rechteck schneidet das Programm den Bereich aus, reduziert ihn durch
Pixelabtastung auf ungefähr ein Pixel pro 16 Originalpixel entlang jeder Achse und vergrößert ihn
anschließend wieder durch Pixelabtastung. Die
Methode sample von RMagick verwendet
Pixelabtastung statt eines Glättungsfilters und erzeugt so rechteckige Blöcke. Jede zurückgegebene
Erkennung wird verarbeitet, auch solche mit niedrigem Konfidenzwert. Wird das angeforderte
Erkennungslimit erreicht, bricht das Programm ab, da die Antwort unvollständig sein kann.
Ruby-Codebeispiel Schritt für Schritt
Speichern Sie dieses vollständige Programm als face-pixelation/pixelate.rb:
require 'google/cloud/vision/v1'
require 'rmagick'
require 'tempfile'
class ReviewRequired < StandardError; end
MAX_INPUT = 2 * 1024 * 1024
MAX_REQUEST_IMAGE = 8 * 1024 * 1024
MAX_FACES = 100
def face_rectangle(face, width, height)
points = face.bounding_poly&.vertices
raise ReviewRequired, 'Ambiguous face polygon; review the original' unless points&.length == 4
a, b, c, d = points
valid = a.x == d.x && b.x == c.x && a.y == b.y && c.y == d.y &&
a.x >= 0 && a.y >= 0 && b.x > a.x && c.y > a.y &&
b.x <= width && c.y <= height
raise ReviewRequired, 'Ambiguous face polygon; review the original' unless valid
[a.x, a.y, b.x - a.x, c.y - a.y]
end
def pixelate_region(image, rectangle)
region = small = blocks = nil
x, y, width, height = rectangle
region = image.crop(x, y, width, height, true)
small = region.sample([(width / 16.0).ceil, 1].max, [(height / 16.0).ceil, 1].max)
blocks = small.sample(width, height)
image.composite!(blocks, x, y, Magick::CopyCompositeOp)
ensure
[region, small, blocks].compact.each(&:destroy!)
end
def run(input, output)
raise ReviewRequired, 'Output must have a .png extension' unless File.extname(output).downcase == '.png'
raise ReviewRequired, 'Destination already exists; choose a new path' if File.exist?(output) || File.symlink?(output)
# Bound native pixel caches; this is not a limit on the Ruby process's total memory.
Magick.limit_resource(:memory, 256 * 1024 * 1024)
Magick.limit_resource(:map, 0)
Magick.limit_resource(:disk, 0)
blob = File.open(input, 'rb') { |file| file.read(MAX_INPUT + 1) || ''.b }
raise ReviewRequired, 'Input must contain 1 byte to 2 MiB' if blob.empty? || blob.bytesize > MAX_INPUT
format = if blob.start_with?("\x89PNG\r\n\x1a\n".b)
'PNG'
elsif blob.start_with?("\xff\xd8\xff".b)
'JPEG'
end
raise ReviewRequired, 'Use a JPEG or PNG' unless format
# Conservative marker refusals also exclude APNG and multi-picture JPEG containers.
raise ReviewRequired, 'Use a static, single-picture image' if blob.include?('acTL') || blob.include?("MPF\0")
headers = images = nil
Tempfile.create(['face-input-', ".#{format.downcase}"]) do |file|
file.binmode
file.write(blob)
file.rewind
headers = Magick::Image.ping(file) { |info| info.format = format }
raise ReviewRequired, 'Use a single-frame image' unless headers.length == 1
header = headers.first
unless header.columns.between?(1, 4096) && header.rows.between?(1, 4096) &&
header.columns * header.rows <= 4_000_000
raise ReviewRequired, 'Input exceeds the dimension or pixel limit; resize it first'
end
end
images = Magick::Image.from_blob(blob) { |info| info.format = format }
raise ReviewRequired, 'Use a single-frame image' unless images.length == 1
image = images.first
unless image.depth <= 8 && !image.alpha? &&
[Magick::SRGBColorspace, Magick::GRAYColorspace].include?(image.colorspace) &&
image.color_profile.to_s.empty?
raise ReviewRequired, 'Use an opaque 8-bit sRGB or grayscale image without an ICC profile'
end
image.auto_orient!
image.strip!
image.format = 'PNG'
upright_png = image.to_blob
raise ReviewRequired, 'Normalized PNG exceeds 8 MiB; resize the input first' if upright_png.bytesize > MAX_REQUEST_IMAGE
client = Google::Cloud::Vision::V1::ImageAnnotator::Client.new do |config|
config.rpcs.batch_annotate_images.timeout = 15
config.rpcs.batch_annotate_images.retry_policy = { retry_codes: [] }
config.channel_args = { 'grpc.enable_retries' => 0 }
config.logger = nil
end
response = client.batch_annotate_images(
{ requests: [{ image: { content: upright_png },
features: [{ type: :FACE_DETECTION, max_results: MAX_FACES }] }] }
)
raise ReviewRequired, 'Unexpected Vision response' unless response.responses.length == 1
result = response.responses.first
raise ReviewRequired, 'Vision returned a detection error; check project access' if result.error && result.error.code != 0
faces = result.face_annotations
raise ReviewRequired, 'No faces detected; review the original' if faces.empty?
raise ReviewRequired, 'Detection limit reached; review the original' if faces.length >= MAX_FACES
rectangles = faces.map { |face| face_rectangle(face, image.columns, image.rows) }
rectangles.each { |rectangle| pixelate_region(image, rectangle) }
# Write completely before publishing; a hard link refuses an existing destination.
Tempfile.create(['face-output-', '.png'], File.dirname(File.expand_path(output))) do |file|
file.binmode
file.write(image.to_blob)
file.flush
File.link(file.path, output)
end
puts "Wrote #{faces.length} pixelated region(s). Review every face before sharing."
ensure
headers&.each(&:destroy!)
images&.each(&:destroy!)
end
begin
raise ReviewRequired, 'Usage: ruby pixelate.rb INPUT OUTPUT.png' unless ARGV.length == 2
unless ARGV.all? { |path| path.dup.force_encoding(Encoding::UTF_8).valid_encoding? }
raise ReviewRequired, 'Use UTF-8 file paths'
end
run(*ARGV)
rescue ReviewRequired => error
warn error.message
exit 1
rescue StandardError
warn 'Processing failed; check the input, project credentials, network, and writable output directory.'
exit 1
end
Legen Sie Ihr Foto unter face-pixelation/input.jpg ab. Führen Sie im selben übergeordneten
Verzeichnis wie bei der Einrichtung Folgendes aus:
(
cd face-pixelation || exit 1
GEM_HOME="$PWD/gems" GEM_PATH="$PWD/gems" \
ruby pixelate.rb ./input.jpg ./output.png
)
Der Exit-Status null bedeutet, dass das Programm face-pixelation/output.png geschrieben und einen
Hinweis zur Überprüfung ausgegeben hat. Öffnen Sie dieses PNG neben dem Original und prüfen Sie vor
der Veröffentlichung jedes sichtbare Gesicht. Verwenden Sie bei späteren Ausführungen einen neuen
Zielnamen. Das Programm lehnt vorhandene Dateien und symbolische Links ab, lässt die Eingabe
unverändert und entfernt seine temporären Dateien. Führen Sie es nacheinander in einem Verzeichnis
aus, das Sie kontrollieren.
Leistung optimieren und Sonderfälle behandeln
Der Vision-RPC hat ein Zeitlimit von 15 Sekunden; automatische SDK-Retries sind deaktiviert. Dieses Zeitlimit gilt für den RPC und nicht für das gesamte Programm: Das Laden von Abhängigkeiten, das Decodieren, das Abrufen der Zugangsdaten und das Schreiben benötigen zusätzliche Zeit. Ergänzen Sie für einen unbeaufsichtigten Dienst eine Prozessüberwachung und ein geeignetes Gesamtzeitlimit, statt dieses lokale Skript als Worker-Dienst zu behandeln.
Antworten ohne erkannte Gesichter, Anbieterfehler für einzelne Bilder, mehrdeutige Polygone und eine volle Antwort mit 100 Ergebnissen führen zu einem Exit-Status ungleich null und keiner neuen Ausgabedatei. Auch bei einer Transport- oder Zugangsdaten-Exception schlägt die Ausführung fehl, ohne eine Erfolgsmeldung auszugeben. Prüfen Sie das Original, wenn die Erkennung nichts findet; eine leere Antwort bedeutet nicht, dass das Bild keine Personen enthält. Gedrehte oder teilweise sichtbare Gesichter müssen möglicherweise manuell bearbeitet werden, selbst wenn der Detektor andere Gesichter erfolgreich erkennt.
Die Größen- und Pixellimits beschränken dieses Beispiel auf kleine Fotos. Die native
Pixel-Cache-Einstellung von 256 MiB und der deaktivierte Festplatten-Cache können auch unterhalb
dieser Limits zu einem Abbruch wegen Ressourcenmangels führen; sie begrenzen nicht alle
Speicherzuweisungen des Prozesses. ImageMagick kann manche beschädigten Bilder wiederherstellen.
Erfolgreiches Decodieren ist daher keine Integritätsprüfung. Verwenden Sie unbeschädigte Originale
und prüfen Sie die vollständige Ausgabe einschließlich ihrer Ränder. Die konservativen Prüfungen
auf die Marker acTL und MPF können auch eine
gewöhnliche Datei ablehnen, die diese Bytefolgen in ihren Metadaten enthält. Exportieren Sie sie
in diesem Fall erneut als statisches Bild.
Falls das Senden des Originals an einen Cloud-Anbieter ungeeignet ist, wählen Sie einen lokalen Detektor und prüfen Sie dessen Vorgaben für Koordinaten und Ausrichtung. Ein Wechsel der Detektor-API erfordert eine Anpassung der Antwortverarbeitung; die Rechteckverarbeitung hier ist auf die akzeptierte Polygonform von Vision zugeschnitten.
Wie lässt sich ImageMagick zur Bildverarbeitung in Ruby integrieren?
RMagick ruft die nativen Bildoperationen von ImageMagick aus Ruby auf. Hier übernimmt es die
EXIF-Ausrichtung, das Zuschneiden, die Pixelabtastung und das Zusammensetzen; Vision liefert die
Erkennungen. Das normalisierte PNG wird im Arbeitsspeicher aus demselben RMagick-Bild erstellt,
das später bearbeitet wird. Dadurch verwenden Erkennung und Verpixelung dieselben
Pixelabmessungen und dieselbe Ausrichtung. strip! entfernt vor der Anfrage
und der Ausgabe die Profile und Eigenschaften der Eingabe, einschließlich der EXIF-Metadaten.
Der PNG-Encoder kann dennoch eigene Formatfelder hinzufügen.
Datenschutzaspekte
Die gewählte Blockgröße ist ein visueller Effekt und kein Schwellenwert für Anonymität. Ein Gesicht kann nach der Verpixelung erkennbar bleiben, und das Entfernen von Metadaten entfernt keine identifizierenden Bildinhalte. Prüfen Sie die Verarbeitungsbedingungen des Anbieters und Ihre Berechtigung, das Original zu senden, bevor Sie die Erkennung ausführen.
Bewahren Sie überprüfte Ausgaben getrennt von den Originalen auf. Wenn Sie dies in einen Upload-Workflow integrieren, verlangen Sie eine explizite Freigabe nach der Überprüfung, bevor Sie das resultierende PNG öffentlich zugänglich machen.
