Pixelizar rostos: Ruby e ImageMagick
Use o Google Cloud Vision para localizar rostos e o RMagick para substituir essas regiões por blocos visíveis de pixels. O programa Ruby abaixo gira os pixels conforme a orientação EXIF antes de enviá-los ao Vision, depois edita essa mesma imagem e salva um PNG para revisão.
Por que pixelizar rostos?
A pixelização reduz os detalhes faciais visíveis, mas um detector pode deixar rostos passar e um rosto pixelizado ainda pode ser identificável. Trate o resultado como uma versão que precisa de revisão. Confira cada rosto, incluindo pessoas nas bordas da imagem, e considere nomes, roupas, reflexos e outros detalhes de identificação antes de compartilhá-lo. A imagem original é enviada ao Google para detecção.
Configurar Ruby e ImageMagick
Este tutorial pressupõe um shell Linux com MRI Ruby 4.0, ImageMagick 7 com suporte a JPEG e PNG,
seus cabeçalhos de desenvolvimento, um compilador C++, make e pkg-config já instalados. As versões
testadas são Ruby 4.0.7, ImageMagick 7.1.2-31, RMagick 7.2.0 e google-cloud-vision 2.2.1.
Use uma versão de correção do Ruby que ainda receba manutenção, disponível na
página de downloads do Ruby. Consulte os
pré-requisitos do RMagick para saber quais pacotes de sistema sua
distribuição exige; a gem compila uma extensão nativa usando esses cabeçalhos.
Execute este bloco em um diretório com permissão de escrita. Ele cria um novo diretório
face-pixelation e mantém as gems dentro dele. Se o diretório já existir, a configuração
para sem substituí-lo. O shell principal permanece no diretório original tanto em caso de sucesso
quanto de falha.
(
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
)
Se a instalação falhar, preserve o diagnóstico do compilador, corrija o pré-requisito ausente e
repita o comando de instalação das gems dentro do diretório criado. Não repita
mkdir nem exclua um projeto existente para concluir a configuração.
Execute o exemplo fora de bundle exec; esta receita usa seu próprio diretório
de gems em vez do Gemfile de uma aplicação que o englobe.
Configurar a API Google Cloud Vision
Siga o guia de configuração do Vision do Google para um projeto com faturamento e a API Vision ativados. A detecção é uma operação na nuvem sujeita a cobrança; confira as cotas e os limites do projeto antes de processar em lote. Este exemplo envia uma imagem local por execução e não precisa de um bucket do Cloud Storage.
Para desenvolvimento local, instale a CLI do Google Cloud e siga as
instruções de ADC do Google.
Execute gcloud auth application-default login e depois
gcloud auth application-default set-quota-project PROJECT_ID, substituindo PROJECT_ID pelo seu projeto
de faturamento. A identidade precisa de serviceusage.services.use nesse projeto.
Essas credenciais são separadas das credenciais comuns de login do gcloud.
O programa usa Application Default Credentials. Uma variável de ambiente
GOOGLE_APPLICATION_CREDENTIALS existente tem precedência sobre o arquivo ADC local, então confira
se ela seleciona as credenciais pretendidas. Evite baixar uma chave de conta de serviço para este
exemplo local. Para uma carga de trabalho no Google Cloud, o Google recomenda a conta de serviço
vinculada a essa carga de trabalho.
Integrar a detecção de rostos ao Ruby usando a API Google Cloud Vision
Use um JPEG ou PNG íntegro e estático, de 8 bits, em sRGB ou escala de cinza, sem transparência nem perfil ICC incorporado. Primeiro, exporte imagens em outros espaços de cor e imagens transparentes para uma imagem sRGB opaca. O programa aceita no máximo 2 MiB de entrada, 4.096 pixels por lado e 4.000.000 pixels no total. Ele também limita a 8 MiB o PNG normalizado enviado ao Vision. Uma entrada comprimida pode ultrapassar esse segundo limite após a decodificação; reduza suas dimensões e tente novamente.
A documentação de resposta de rostos do Vision alerta que rostos parciais podem ter coordenadas de polígono ausentes. Campos inteiros do Protobuf podem retornar zero quando ausentes. Usar o mínimo e o máximo desses valores pode selecionar a região errada. Este exemplo aceita apenas quatro vértices que formem um retângulo ordenado, positivo e alinhado aos eixos dentro da imagem. Um retângulo completo pode tocar a coordenada zero. Polígonos incompletos, invertidos, girados ou ambíguos de qualquer outra forma interrompem toda a execução sem publicar um resultado; inspecione essas imagens manualmente em vez de tentar adivinhar um retângulo.
Aplicar efeitos de pixelização com ImageMagick
Para cada retângulo aceito, o programa recorta a região, reduz sua resolução por amostragem para
cerca de um pixel a cada 16 em cada eixo e a amplia novamente por amostragem.
O método sample do RMagick usa amostragem de pixels em vez de um
filtro de suavização, produzindo blocos retangulares. Todas as detecções retornadas são processadas,
incluindo as de baixa pontuação de confiança. Atingir o limite de detecções solicitado faz o
programa recusar o processamento, pois a resposta pode estar incompleta.
Exemplo de código Ruby passo a passo
Salve 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
Coloque sua fotografia em face-pixelation/input.jpg. No mesmo diretório pai usado na
configuração, execute:
(
cd face-pixelation || exit 1
GEM_HOME="$PWD/gems" GEM_PATH="$PWD/gems" \
ruby pixelate.rb ./input.jpg ./output.png
)
O status de saída zero significa que o programa gravou face-pixelation/output.png e imprimiu
um lembrete de revisão. Abra esse PNG ao lado do original e inspecione cada rosto visível antes de
publicá-lo. Use um novo nome de destino nas próximas execuções. O programa recusa arquivos
existentes e links simbólicos, mantém a entrada intacta e remove seus arquivos temporários.
Execute-o sequencialmente em um diretório sob seu controle.
Otimizar o desempenho e lidar com casos especiais
A chamada RPC do Vision tem um tempo limite de 15 segundos, e as novas tentativas automáticas do SDK estão desativadas. Esse é um tempo limite da chamada RPC, não um prazo para o programa inteiro: o carregamento de dependências, a decodificação, a obtenção de credenciais e a gravação levam tempo adicional. Para um serviço sem supervisão humana, adicione supervisão de processos e um prazo total adequado em vez de tratar este script local como um serviço de processamento.
Respostas sem rostos, erros do provedor por imagem, polígonos ambíguos e uma resposta que atinja os 100 resultados geram um status de saída diferente de zero e nenhum novo arquivo de saída. Uma exceção de transporte ou de credenciais também causa falha sem imprimir uma mensagem de sucesso. Confira o original quando a detecção não encontrar nada; uma resposta vazia não significa que a imagem não contenha pessoas. Rostos girados ou parcialmente visíveis podem precisar de edição manual mesmo quando o detector encontra outros rostos com sucesso.
Os limites de tamanho e de pixels mantêm este exemplo voltado para fotografias pequenas. A
configuração de 256 MiB do cache nativo de pixels e o cache em disco desativado podem causar uma
recusa por falta de recursos mesmo abaixo desses limites; eles não limitam todas as alocações do
processo. O ImageMagick pode recuperar algumas imagens danificadas, então uma decodificação
bem-sucedida não é uma verificação de integridade. Use originais íntegros e inspecione o resultado
completo, incluindo suas bordas. As verificações conservadoras dos marcadores
acTL e MPF também podem recusar um arquivo comum que contenha essas sequências de bytes nos
metadados; se isso ocorrer, exporte-o novamente como uma imagem estática.
Se enviar o original a um provedor de nuvem não for adequado, escolha um detector local e verifique as regras dele para coordenadas e orientação. Trocar a API de detecção exige adaptar a resposta; o tratamento de retângulos usado aqui é específico para a forma de polígono aceita do Vision.
Como o ImageMagick se integra ao Ruby para processamento de imagens?
O RMagick chama as operações nativas de imagem do ImageMagick a partir do Ruby. Aqui, ele cuida da
orientação EXIF, do recorte, da amostragem e da composição; o Vision fornece as detecções. O PNG
normalizado é criado na memória a partir da mesma imagem do RMagick que será editada depois, então
a detecção e a pixelização compartilham as dimensões em pixels e a orientação.
strip! remove os perfis e as propriedades da entrada antes da requisição e da geração do arquivo de
saída, incluindo os metadados EXIF; o codificador de PNG ainda pode adicionar seus próprios campos
de formato.
Considerações sobre privacidade
O tamanho de bloco selecionado é um efeito visual, não um limiar de anonimato. Um rosto pode continuar reconhecível após a pixelização, e remover metadados não remove o conteúdo da imagem que permite identificação. Revise os termos de processamento do provedor e verifique se você tem permissão para enviar o original antes de executar a detecção.
Mantenha os resultados revisados separados dos originais. Se você adicionar isso a um fluxo de trabalho de upload, exija uma decisão explícita de revisão antes de tornar público o PNG resultante.

