Pixelar rostros: Ruby e ImageMagick
Usa Google Cloud Vision para localizar rostros y RMagick para reemplazar esas regiones por bloques visibles de píxeles. El programa en Ruby que aparece a continuación gira los píxeles según la orientación EXIF antes de enviarlos a Vision; después edita esa misma imagen y guarda un PNG para su revisión.
¿Por qué pixelar rostros?
El pixelado reduce los detalles faciales visibles, pero un detector puede pasar por alto algunos rostros y un rostro pixelado puede seguir siendo identificable. Trata el resultado como un candidato para revisión. Revisa cada rostro, incluidas las personas en los bordes de la imagen, y considera los nombres, la ropa, los reflejos y otros detalles identificativos antes de compartirlo. La imagen original se envía a Google para la detección.
Configurar Ruby e ImageMagick
Este tutorial está pensado para un shell de Linux con MRI Ruby 4.0, ImageMagick 7 con soporte para
JPEG y PNG, sus cabeceras de desarrollo, un compilador de C++, make y pkg-config ya instalados. Las versiones
probadas son Ruby 4.0.7, ImageMagick 7.1.2-31, RMagick 7.2.0 y google-cloud-vision 2.2.1.
Usa una versión de parche de Ruby que siga recibiendo mantenimiento, disponible en la
página de descargas de Ruby.
Consulta los requisitos previos de RMagick para conocer los paquetes del sistema
que necesita tu distribución; la gema compila una extensión nativa utilizando esas cabeceras.
Ejecuta este bloque desde un directorio con permisos de escritura. Crea un directorio nuevo llamado
face-pixelation y mantiene las gemas dentro de él. Si el directorio ya existe, la configuración
se detiene sin reemplazarlo. El shell principal permanece en su directorio original tanto si la
operación tiene éxito como si falla.
(
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 la instalación falla, conserva el diagnóstico del compilador, resuelve el requisito previo que
falta y repite el comando de instalación de gemas desde el directorio creado. No repitas
mkdir ni elimines un proyecto existente para que la configuración funcione. Ejecuta el
ejemplo fuera de bundle exec; esta guía usa su propio directorio de gemas en lugar del
Gemfile de una aplicación que lo contenga.
Configurar la API de Google Cloud Vision
Sigue la guía de configuración de Vision de Google para un proyecto que tenga habilitadas la facturación y la API de Vision. La detección es una operación en la nube sujeta a cargos; revisa las cuotas y los límites del proyecto antes de procesar por lotes. Este ejemplo envía una imagen local por ejecución y no necesita ningún bucket de Cloud Storage.
Para el desarrollo local, instala la CLI de Google Cloud y sigue las
instrucciones de Google para ADC.
Ejecuta gcloud auth application-default login y después
gcloud auth application-default set-quota-project PROJECT_ID, reemplazando PROJECT_ID por tu
proyecto de facturación. La identidad necesita serviceusage.services.use en ese proyecto. Estas credenciales
son independientes de las credenciales habituales de inicio de sesión de gcloud.
El programa usa Application Default Credentials. Si existe la variable de entorno
GOOGLE_APPLICATION_CREDENTIALS, tiene prioridad sobre el archivo ADC local, así que comprueba que seleccione
las credenciales previstas. Evita descargar una clave de cuenta de servicio para este ejemplo
local. Para una carga de trabajo de Google Cloud, Google recomienda la cuenta de servicio asociada
a esa carga de trabajo.
Integrar la detección de rostros con Ruby mediante la API de Google Cloud Vision
Usa un JPEG o PNG íntegro y estático de 8 bits, en sRGB o escala de grises, sin transparencia ni un perfil ICC incorporado. Primero exporta las imágenes con otros espacios de color o con transparencia a una imagen sRGB opaca. El programa acepta como máximo 2 MiB de entrada, 4.096 píxeles por lado y 4.000.000 píxeles en total. También limita a 8 MiB el PNG normalizado que se envía a Vision. Una entrada comprimida puede superar ese segundo límite tras la decodificación; reduce sus dimensiones e inténtalo de nuevo.
La documentación de las respuestas de detección de rostros de Vision advierte que los rostros parciales pueden tener coordenadas de polígono ausentes. Los campos enteros de Protobuf pueden leerse como cero cuando están ausentes. Tomar el mínimo y el máximo de esos valores puede seleccionar la región equivocada. Este ejemplo solo acepta cuatro vértices que formen un rectángulo ordenado, positivo y alineado con los ejes dentro de la imagen. Un rectángulo completo puede tocar la coordenada cero. Los polígonos incompletos, invertidos, girados o ambiguos por cualquier otro motivo detienen toda la ejecución sin publicar un resultado; inspecciona esas imágenes manualmente en lugar de adivinar un rectángulo.
Aplicar efectos de pixelado con ImageMagick
Para cada rectángulo aceptado, el programa recorta la región, reduce sus dimensiones mediante
muestreo hasta aproximadamente un píxel por cada 16 originales en cada eje y vuelve a
ampliarla mediante muestreo. El
método sample de RMagick usa muestreo de píxeles en lugar de un
filtro de suavizado, lo que produce bloques rectangulares. Se procesa cada detección devuelta,
incluidas las de baja confianza. Alcanzar el límite de detecciones solicitado provoca un rechazo,
ya que la respuesta puede estar incompleta.
Ejemplo de código Ruby paso a paso
Guarda este programa completo como 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
Coloca tu fotografía en face-pixelation/input.jpg. Desde el mismo directorio principal usado para la
configuración, ejecuta:
(
cd face-pixelation || exit 1
GEM_HOME="$PWD/gems" GEM_PATH="$PWD/gems" \
ruby pixelate.rb ./input.jpg ./output.png
)
Un estado de salida cero significa que el programa escribió face-pixelation/output.png y mostró un
recordatorio de revisión. Abre ese PNG junto al original e inspecciona cada rostro visible antes
de publicarlo. Usa un nombre de destino nuevo en las ejecuciones posteriores. El programa rechaza
los archivos y enlaces simbólicos existentes, deja intacta la entrada y elimina sus archivos
temporales. Ejecútalo secuencialmente en un directorio que controles.
Optimizar el rendimiento y gestionar casos límite
La RPC de Vision tiene un tiempo de espera de 15 segundos y los reintentos automáticos del SDK están deshabilitados. Este es un tiempo de espera de la RPC, no un plazo para todo el programa: la carga de dependencias, la decodificación, la obtención de credenciales y la escritura requieren tiempo adicional. Para un servicio sin supervisión humana, añade supervisión de procesos y un plazo total adecuado en lugar de tratar este script local como un servicio de procesamiento.
Las respuestas sin rostros, los errores del proveedor por imagen, los polígonos ambiguos y una respuesta que alcance los 100 resultados producen un estado de salida distinto de cero y ningún archivo de salida nuevo. Una excepción de transporte o de credenciales también provoca un fallo sin mostrar un mensaje de éxito. Revisa el original cuando la detección no encuentre nada; una respuesta vacía no significa que la imagen no contenga personas. Los rostros girados o parcialmente visibles pueden necesitar edición manual aunque el detector encuentre otros rostros correctamente.
Los límites de tamaño y píxeles mantienen este ejemplo centrado en fotografías pequeñas. La
configuración de 256 MiB para la caché nativa de píxeles y la desactivación de la caché en disco
pueden provocar un rechazo por falta de recursos incluso por debajo de esos límites; no limitan
todas las asignaciones de memoria del proceso. ImageMagick puede recuperar algunas imágenes
dañadas, por lo que una decodificación correcta no es una comprobación de integridad. Usa
originales íntegros e inspecciona todo el resultado, incluidos sus bordes. Las comprobaciones
conservadoras de los marcadores acTL y MPF también pueden rechazar un
archivo normal que contenga esas secuencias de bytes en los metadatos; si ocurre, vuelve a
exportarlo como imagen estática.
Si no es adecuado enviar el original a un proveedor de la nube, elige un detector local y verifica su propio contrato de coordenadas y orientación. Cambiar de API de detección requiere adaptar la respuesta; el manejo de rectángulos de este ejemplo es específico de la forma de polígono de Vision que se acepta.
¿Cómo se integra ImageMagick con Ruby para el procesamiento de imágenes?
RMagick llama a las operaciones nativas de imagen de ImageMagick desde Ruby. Aquí se encarga de
la orientación EXIF, el recorte, el muestreo y la composición; Vision proporciona las detecciones.
El PNG normalizado se crea en memoria a partir de la misma imagen de RMagick que se edita después,
por lo que la detección y el pixelado comparten las dimensiones en píxeles y la orientación.
strip! elimina los perfiles y las propiedades de la entrada antes de la solicitud y de
la salida, incluidos los metadatos EXIF; el codificador PNG aún puede añadir sus propios campos
de formato.
Consideraciones de privacidad
El tamaño de bloque seleccionado es un efecto visual, no un umbral de anonimato. Un rostro puede seguir siendo reconocible después del pixelado, y eliminar los metadatos no elimina el contenido identificativo de la imagen. Revisa las condiciones de procesamiento del proveedor y tu autorización para enviar el original antes de ejecutar la detección.
Mantén los resultados revisados separados de los originales. Si incorporas esto a un flujo de trabajo de subida, exige una decisión explícita de revisión antes de hacer público el PNG resultante.
