Quando alguém abre uma foto no Photoshop, apaga um risco de nada no fundo, salva, e manda pra internet achando que ninguém vai notar, às vezes ninguém nota mesmo observando somente com o olho humano. Só que o ruído da sua câmera notou, e ele vai contar. Esse post é sobre como uma técnica clássica de análise de sinais, com zero linha de deep learning, consegue apontar exatamente onde a imagem foi mexida.
TL;DR: Como uma POC em Python detecta edições em fotos usando apenas análise estatística do ruído gaussiano do sensor, sem modelo treinado, sem AI e sem GPU. Trade-offs e limites explicados.
Stack: Python, NumPy, SciPy, PIL, processamento de sinais
Nível: Intermediário
Tempo de leitura: ~6 min

Toda vez que alguém edita uma foto, deixa rastro. Ferramentas forenses profissionais sabem disso há décadas. Mas a maior parte da conversa sobre detecção de imagem alterada hoje gira em torno de modelos de deep learning treinados em datasets gigantes, com custo de compute proporcional. Funciona, mas cobre um problema que a estatística resolve na maioria dos casos sem chegar perto de uma GPU.
Passei uma tarde brincando com uma foto do meu teclado. Editei alguns riscos pequenos no desk pad, do tipo que ninguém repara. Depois abri o Python pra ver se conseguia denunciar o que eu tinha acabado de fazer, sem passar a imagem original para o algoritmo. Deu certo em duas horas de código, e sem treinar nada.
Por que o ruído da sua câmera te entrega
Sensor CCD ou CMOS não é perfeito. Cada foto que ele tira ganha um padrão de ruído aleatório, invisível a olho nu, que segue uma distribuição gaussiana com sigma característico da câmera, da iluminação da cena e do quanto o JPEG comprimiu. Duas fotos tiradas com a mesma câmera na mesma luz vão ter assinatura de ruído parecida. Uma região colada de outra fonte (Photoshop pincelando, IA gerando, foto colada) traz outra assinatura, e não tem como esconder.
Isolando o ruído com um truque simples
Um blur gaussiano estima o “conteúdo limpo” da imagem, ou seja, bordas, formas, contraste. Se você subtrai a versão suavizada da original, sobra exatamente o que oscilava em alta frequência. Isso é o ruído.
def noise_residual(image, sigma):
smooth = gaussian_filter(image, sigma=sigma)
return image - smooth
Três linhas. Nenhuma dessas é nova. Processamento de sinais faz esse tipo de decomposição desde antes do meu pai nascer. A novidade não está na técnica, está em usá-la pra um problema onde muita gente hoje pularia direto pra rede neural.
Calculando desvio padrão local sem loop em Python
A próxima etapa é varrer a imagem em janelas de 32 por 32 pixels e calcular o desvio padrão do ruído em cada janela. Escrever isso como loop em Python puro leva horas em uma imagem grande. Não precisa. A identidade da variância resolve:
Var(X) = E[X²] − (E[X])²
def local_std(image, window):
mean = uniform_filter(image, size=window)
mean_sq = uniform_filter(image ** 2, size=window)
variance = np.clip(mean_sq - mean ** 2, 0, None)
return np.sqrt(variance)
Duas passadas de média móvel, uma subtração, uma raiz quadrada. Tudo vetorizado via SciPy. Roda em milissegundos numa imagem de 1000 por 1000.
O tamanho da janela é o único trade-off delicado dessa etapa. Janela pequena (16 por 16) dá estatística barulhenta e muitos falsos positivos. Janela grande (64 por 64) perde resolução espacial e o heatmap final mancha demais. Cheguei em 32 por 32 por sentimento, o tipo de sentimento que só vem depois de ter rodado algumas dezenas de análises parecidas antes (obrigado, doutorado em visão computacional).
Comparando cada janela contra a média global
Com o desvio padrão local em mãos, cada janela recebe uma nota que diz o quanto ela está distante da média global do ruído, medido em “sigmas de distância”:
def anomaly_map(std_map, smooth_sigma):
global_mean = std_map.mean()
global_std = std_map.std()
raw = np.abs(std_map - global_mean) / (global_std + 1e-9)
return gaussian_filter(raw, sigma=smooth_sigma)
Regiões acima de dois sigmas são anomalias estatísticas, ou seja, ruído que não pertence à distribuição principal da foto. Um blur final no heatmap suaviza pixels isolados que aparecem por acaso e destaca regiões coerentes que aparecem porque a imagem foi mexida ali.
O resultado
Olhando a comparação, os três primeiros painéis mostram a foto original, a editada e o ground truth (a diferença direta entre as duas, que só existe porque eu tinha as duas imagens em mão). Serve de gabarito. O que importa mesmo é o quarto painel, que é a análise gaussiana rodando sozinha, sem acesso à original, e denunciando exatamente as regiões que eu editei.
O insight que importa aqui
Quando você precisa detectar algo, o primeiro instinto hoje é “vou treinar um modelo”. Isso está errado com uma frequência surpreendente. Antes de treinar, sempre vale a pergunta: existe uma propriedade estatística ou matemática que já denuncia o que eu quero detectar? Se existir, ela custa zero de compute, roda em qualquer lugar, e você mantém controle sobre o que está fazendo. Um modelo é caixa preta que faz outra coisa quando você não olha. Uma equação é sempre a mesma coisa.
Esse ponto conecta direto com o que escrevi sobre o MIDAS neste post aqui. A escolha entre modelo próprio, VLM open, ou API de fronteira é a mesma escolha em outro nível: qual é a ferramenta mais barata que passa na sua barra de qualidade? Aqui a barra passou com três funções de dez linhas cada.
Onde essa técnica falha (e vai falhar)
Vou ser honesto sobre limites, porque post técnico que só conta o que funcionou vira propaganda.
Ela não funciona bem em imagens comprimidas em JPEG duas vezes, porque o segundo JPEG homogeneiza o ruído. Não pega edições muito pequenas (algumas dezenas de pixels), porque a janela de 32 nem chega a envolver a região direito. Não detecta transformações globais como mudar brilho, contraste ou saturação da imagem inteira, porque o ruído continua consistente em toda parte. E falha em fotos com muito grão intencional (ISO alto, filtros analógicos), porque o ruído natural já é grande e a assinatura da região colada some no meio.
Cada uma dessas limitações tem uma técnica que cobre em cima. Análise de PRNU pra assinatura específica de sensor. Análise de coeficientes DCT pra pegar recompressão. Combinação de múltiplos estimadores estatísticos pra ganhar robustez. Mas isso é outro post.
Perguntas ou feedback? Me encontre no LinkedIn ou no GitHub.