Pixelliser des visages : Ruby & ImageMagick
Utilisez Google Cloud Vision pour localiser les visages et RMagick pour remplacer ces zones par des blocs de pixels visibles. Le programme Ruby ci-dessous fait pivoter les pixels selon l’orientation EXIF avant de les envoyer à Vision, puis modifie cette même image et enregistre un PNG à vérifier.
Pourquoi pixelliser les visages ?
La pixellisation réduit les détails visibles du visage, mais un détecteur peut manquer des visages et un visage pixellisé peut rester identifiable. Considérez le résultat comme une version à vérifier. Examinez chaque visage, y compris ceux des personnes sur les bords de l’image, et tenez compte des noms, des vêtements, des reflets et des autres détails identifiants avant de partager l’image. L’image originale est envoyée à Google pour la détection.
Configurer Ruby et ImageMagick
Ce tutoriel s’adresse aux utilisateurs d’un shell Linux disposant déjà de MRI Ruby 4.0, d’ImageMagick 7
avec prise en charge de JPEG et PNG, de ses en-têtes de développement, d’un compilateur C++, de
make et de pkg-config. Les versions testées sont
Ruby 4.0.7, ImageMagick 7.1.2-31, RMagick 7.2.0 et google-cloud-vision 2.2.1.
Utilisez une version corrective de Ruby encore maintenue, disponible sur la
page de téléchargement de Ruby. Consultez les
prérequis de RMagick pour connaître les paquets système nécessaires
à votre distribution ; le gem compile une extension native à partir de ces en-têtes.
Exécutez ce bloc depuis un répertoire dans lequel vous pouvez écrire. Il crée un nouveau répertoire
face-pixelation et y conserve les gems. Si ce répertoire existe déjà, la configuration
s’arrête sans le remplacer. Le shell parent reste dans son répertoire d’origine, que l’opération
réussisse ou échoue.
(
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
)
Si l’installation échoue, conservez le diagnostic du compilateur, installez le prérequis manquant et
réexécutez la commande d’installation des gems depuis le répertoire créé. Ne répétez pas
mkdir et ne supprimez pas un projet existant pour faire aboutir la
configuration. Exécutez l’exemple en dehors de bundle exec ; cette procédure utilise
son propre répertoire de gems plutôt que le Gemfile d’une application englobante.
Configurer l’API Google Cloud Vision
Suivez le guide de configuration de Vision de Google pour un projet dont la facturation et l’API Vision sont activées. La détection est une opération cloud facturable ; vérifiez les quotas et limites du projet avant tout traitement par lots. Cet exemple envoie une image locale par exécution et ne nécessite aucun bucket Cloud Storage.
Pour le développement local, installez la CLI Google Cloud et suivez les
instructions de Google pour ADC.
Exécutez gcloud auth application-default login, puis
gcloud auth application-default set-quota-project PROJECT_ID, en remplaçant PROJECT_ID par votre
projet de facturation. L’identité doit disposer de serviceusage.services.use dans ce projet.
Ces informations d’authentification sont distinctes de celles utilisées pour une connexion ordinaire
avec gcloud.
Le programme utilise Application Default Credentials. Une variable d’environnement
GOOGLE_APPLICATION_CREDENTIALS existante est prioritaire sur le fichier ADC local ; vérifiez donc
qu’elle sélectionne les informations d’authentification prévues. Évitez de télécharger une clé de
compte de service pour cet exemple local. Pour une charge de travail Google Cloud, Google recommande
le compte de service associé à cette charge de travail.
Intégrer la détection de visages à Ruby avec l’API Google Cloud Vision
Utilisez une image JPEG ou PNG intacte, statique, sur 8 bits, en sRGB ou en niveaux de gris, sans transparence ni profil ICC intégré. Exportez d’abord les images utilisant d’autres espaces colorimétriques et les images transparentes vers une image sRGB opaque. Le programme accepte au maximum 2 MiB en entrée, 4 096 pixels par côté et 4 000 000 pixels au total. Il limite également à 8 MiB le PNG normalisé envoyé à Vision. Une image d’entrée compressée peut dépasser cette seconde limite après décodage ; réduisez ses dimensions et réessayez.
La documentation des réponses de détection de visages de Vision avertit que les coordonnées des polygones peuvent être incomplètes pour les visages partiellement visibles. Les champs entiers Protobuf peuvent renvoyer zéro lorsqu’ils sont absents. Prendre le minimum et le maximum de ces valeurs peut sélectionner la mauvaise zone. Cet exemple n’accepte que quatre sommets formant un rectangle ordonné, de dimensions strictement positives, aligné sur les axes et situé à l’intérieur de l’image. Un rectangle complet peut toucher la coordonnée zéro. Les polygones incomplets, inversés, inclinés ou autrement ambigus interrompent toute l’exécution sans produire de fichier de sortie ; examinez ces images manuellement plutôt que de deviner un rectangle.
Appliquer des effets de pixellisation avec ImageMagick
Pour chaque rectangle accepté, le programme découpe la zone, la sous-échantillonne à environ un pixel
pour 16 pixels d’origine sur chaque axe, puis la rééchantillonne à sa taille initiale.
La méthode sample de RMagick utilise
l’échantillonnage de pixels plutôt qu’un filtre de lissage, ce qui produit des blocs rectangulaires.
Chaque détection renvoyée est traitée, y compris celles dont le score de confiance est faible.
Atteindre la limite de détection demandée entraîne un refus, car la réponse peut être incomplète.
Exemple de code Ruby étape par étape
Enregistrez ce programme complet sous le nom 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
Placez votre photographie à l’emplacement face-pixelation/input.jpg. Depuis le même répertoire
parent que celui utilisé pour la configuration, exécutez :
(
cd face-pixelation || exit 1
GEM_HOME="$PWD/gems" GEM_PATH="$PWD/gems" \
ruby pixelate.rb ./input.jpg ./output.png
)
Un code de sortie nul signifie que le programme a écrit face-pixelation/output.png et affiché un
rappel de vérification. Ouvrez ce PNG à côté de l’original et examinez chaque visage visible avant
publication. Utilisez un nouveau nom de destination pour les exécutions suivantes. Le programme
refuse les fichiers et liens symboliques existants, laisse l’entrée intacte et supprime ses fichiers
temporaires. Exécutez-le de manière séquentielle dans un répertoire que vous contrôlez.
Optimiser les performances et gérer les cas particuliers
L’appel RPC à Vision a un délai d’expiration de 15 secondes et les nouvelles tentatives automatiques du SDK sont désactivées. Ce délai concerne l’appel RPC, pas l’ensemble du programme : le chargement des dépendances, le décodage, l’obtention des informations d’authentification et l’écriture prennent du temps supplémentaire. Pour un service sans surveillance, ajoutez une supervision des processus et une durée maximale totale adaptée plutôt que de considérer ce script local comme un service de traitement en arrière-plan.
Les réponses sans visage, les erreurs du fournisseur propres à une image, les polygones ambigus et une réponse complète de 100 résultats entraînent un code de sortie non nul et aucun nouveau fichier de sortie. Une exception de transport ou d’authentification provoque également un échec sans afficher de message de réussite. Vérifiez l’original lorsque la détection ne trouve rien ; une réponse vide ne signifie pas que l’image ne contient personne. Les visages inclinés ou partiellement visibles peuvent nécessiter des retouches manuelles même lorsque le détecteur trouve d’autres visages.
Les limites de taille et de pixels restreignent cet exemple aux petites photographies. Le réglage
du cache de pixels natif à 256 MiB et la désactivation du cache disque peuvent entraîner un refus lié
aux ressources même en dessous de ces limites ; ils ne plafonnent pas toutes les allocations du
processus. ImageMagick peut récupérer certaines images endommagées ; un décodage réussi ne constitue
donc pas un contrôle d’intégrité. Utilisez des originaux intacts et examinez l’intégralité du résultat,
y compris ses bords. Les vérifications prudentes des marqueurs acTL et
MPF peuvent aussi refuser un fichier ordinaire contenant ces séquences
d’octets dans ses métadonnées ; dans ce cas, réexportez-le sous forme d’image statique.
Si l’envoi de l’original à un fournisseur cloud ne convient pas, choisissez un détecteur local et vérifiez ses propres règles concernant les coordonnées et l’orientation. Changer d’API de détection nécessite d’adapter la réponse ; la gestion des rectangles présentée ici est spécifique à la forme de polygone acceptée pour Vision.
Comment ImageMagick s’intègre-t-il à Ruby pour le traitement d’images ?
RMagick appelle les opérations d’image natives d’ImageMagick depuis Ruby. Ici, il gère l’orientation
EXIF, le recadrage, l’échantillonnage et la composition ; Vision fournit les détections. Le PNG
normalisé est créé en mémoire à partir de la même image RMagick que celle qui est ensuite modifiée.
La détection et la pixellisation utilisent donc les mêmes dimensions en pixels et la même orientation.
strip! supprime les profils et propriétés de l’entrée avant la requête et la
production du résultat, y compris les métadonnées EXIF ; l’encodeur PNG peut néanmoins ajouter ses
propres champs de format.
Considérations relatives à la confidentialité
La taille des blocs sélectionnée est un effet visuel, pas un seuil d’anonymat. Un visage peut rester reconnaissable après pixellisation, et la suppression des métadonnées ne supprime pas le contenu identifiant de l’image. Examinez les conditions de traitement du fournisseur et vérifiez que vous êtes autorisé à envoyer l’original avant de lancer la détection.
Conservez les résultats vérifiés séparément des originaux. Si vous intégrez ce traitement à un processus de téléversement, exigez une décision de validation explicite avant de rendre public le PNG obtenu.
