Detectar deepfake em Python - forensics de IA com FFT, Error Level Analysis e MediaPipe - Eleicoes 2026 - Meu Universo Nerd

Em março de 2026 o TSE publicou a Resolução 23.755, que pela primeira vez colocou regras de inteligência artificial dentro da campanha eleitoral: conteúdo sintético tem que ser rotulado, deepfake fica proibido na reta final do pleito e quem não cumpre paga multa de R$ 5 mil a R$ 30 mil. Em junho, o próprio tribunal já tinha registrado denúncias envolvendo o uso de IA. A régua mudou, e de quebra virou um problema de engenharia.

Se você constrói qualquer plataforma onde mídia circula, detectar conteúdo sintético deixou de ser curiosidade e passou a ser requisito. E o primeiro passo é o mais contraintuitivo: parar de confiar no olho humano. Detecção que funciona não acontece na percepção, acontece no domínio da frequência e na geometria das sombras. Bora ver como provar isso com Python, na prática.

Por que o olho humano falha (e os modelos sabem disso)

O instinto manda procurar o piscar estranho, a boca dessincronizada, a textura de pele plástica. Esse foi exatamente o conjunto de pistas que as primeiras gerações de deepfake deixavam, lá por 2019. Só que cada uma dessas pistas virou alvo de treino. Os geradores de 2026 aprenderam a piscar certo, a sincronizar lábios e a renderizar poro de pele. Quando a defesa depende de percepção, ela perde para um sistema que treina justamente contra a percepção.

A ameaça também mudou de lugar. O vídeo "trocando o rosto de alguém" é o caso mais comentado, mas hoje o risco maior é o conteúdo gerado do zero: uma voz clonada com poucos minutos de áudio, um retrato que nunca existiu, um print de conversa fabricado. Nesses casos não existe "original" para comparar. Sobra a evidência que o próprio processo de geração deixa no arquivo.

E aqui está a boa notícia para quem é técnico: todo gerador deixa rastro. Redes que sintetizam imagem usam camadas de upsampling que imprimem um padrão periódico no espectro de frequência. Recompressão e montagem deixam níveis de erro diferentes em regiões diferentes. Reflexos nos olhos e direção de sombra raramente fecham a geometria. Nada disso o olho vê, mas o NumPy enxerga numa boa. Se você quer entender o terreno antes de codar, vale revisar nossos conteúdos de fundamentos de segurança e cibersegurança, porque forensics de mídia é prima direta de análise forense de incidentes.

Rastro 1: análise de frequência com FFT

Imagens fotográficas reais têm um espectro de frequência relativamente suave. Já imagens geradas por GANs e por muitos modelos de difusão carregam artefatos periódicos, um "quadriculado" fino que vem das operações de upsampling (convoluções transpostas). Esse padrão fica invisível no domínio espacial, mas salta no espectro quando você aplica a Transformada Rápida de Fourier.

A ideia do código: carregar a imagem em tons de cinza, calcular a FFT 2D, centralizar o espectro e olhar a magnitude em escala logarítmica. Picos regulares fora do centro são suspeitos. Dá para reduzir isso a um número fazendo a média radial (azimutal) do espectro e medindo a energia nas altas frequências.

import numpy as np
import cv2

def assinatura_espectral(caminho):
    # 1) imagem em tons de cinza, valores 0..1
    img = cv2.imread(caminho, cv2.IMREAD_GRAYSCALE).astype(np.float32) / 255.0

    # 2) FFT 2D + shift para centralizar a frequencia zero
    f = np.fft.fftshift(np.fft.fft2(img))
    magnitude = np.log(np.abs(f) + 1e-8)  # log evita estourar a escala

    # 3) media radial: energia media por faixa de frequencia
    h, w = magnitude.shape
    cy, cx = h // 2, w // 2
    y, x = np.indices((h, w))
    raio = np.hypot(x - cx, y - cy).astype(np.int32)
    perfil = np.bincount(raio.ravel(), magnitude.ravel()) / np.bincount(raio.ravel())

    # 4) razao de energia nas altas frequencias (ultimo terco do espectro)
    corte = len(perfil) // 3
    energia_alta = perfil[2 * corte:].mean()
    energia_baixa = perfil[:corte].mean()
    return energia_alta / energia_baixa  # quanto maior, mais suspeito

razao = assinatura_espectral("suspeito.jpg")
print(f"Razao de energia alta/baixa: {razao:.3f}")
# Imagens sinteticas tendem a marcar razao mais alta por causa do upsampling

Esse valor sozinho não condena nada. O certo é calibrar: rode a função numa pasta de fotos reais e numa pasta de imagens geradas, veja onde cada distribuição cai, e defina um limiar para o seu domínio. É o tipo de baseline que muda de câmera para câmera e de modelo para modelo.

Rastro 2: Error Level Analysis em imagens

Error Level Analysis (ELA) explora um detalhe do JPEG: cada vez que você salva, regiões da imagem perdem qualidade em ritmos diferentes. Numa foto autêntica e salva uma vez, o erro de recompressão é bem uniforme. Quando alguém cola um rosto gerado, insere um texto ou recompõe um print, a região editada normalmente tem um histórico de compressão diferente do resto, e isso aparece como um "brilho" no mapa de erro.

from PIL import Image, ImageChops
import numpy as np

def error_level_analysis(caminho, qualidade=90):
    original = Image.open(caminho).convert("RGB")

    # 1) regrava o JPEG numa qualidade conhecida
    original.save("_tmp_ela.jpg", "JPEG", quality=qualidade)
    recomprimida = Image.open("_tmp_ela.jpg")

    # 2) diferenca pixel a pixel entre original e recomprimida
    diff = ImageChops.difference(original, recomprimida)

    # 3) normaliza para enxergar as regioes de maior erro
    arr = np.asarray(diff).astype(np.float32)
    escala = 255.0 / max(arr.max(), 1.0)
    mapa = (arr * escala).clip(0, 255).astype(np.uint8)

    Image.fromarray(mapa).save("ela_resultado.png")
    return arr.mean(), arr.std()  # media e desvio do erro

media, desvio = error_level_analysis("print_suspeito.jpg")
print(f"Erro medio: {media:.2f} | desvio: {desvio:.2f}")
# Desvio alto + regiao isolada brilhando no mapa = sinal de montagem

ELA é ótima para print fabricado e montagem grosseira, e fraca para imagem 100% sintética salva uma única vez (sem região "estranha" para contrastar). Por isso ela entra como uma evidência, nunca como veredito. Quem já lidou com resposta a incidentes reconhece o padrão: você junta indícios independentes até a conclusão ficar robusta sozinha.

Rastro 3: consistência facial e reflexo nos olhos com MediaPipe

Em vídeo, dá para ir além do pixel e olhar a coerência biológica e física do rosto ao longo do tempo. Duas pistas seguem valendo muito: a taxa e a simetria do piscar, e o reflexo de luz na córnea. Num rosto real, os dois olhos enxergam o mesmo ambiente, então os pontos de brilho (reflexo especular) caem em posições coerentes. Síntese costuma tratar cada olho de forma independente e bagunça isso.

O exemplo abaixo usa o MediaPipe Face Mesh para extrair 468 landmarks e calcular o Eye Aspect Ratio (EAR), métrica clássica para medir abertura do olho quadro a quadro. Sequências sem nenhum piscar, ou com os dois olhos sempre em EAR idêntico, são suspeitas.

import cv2
import mediapipe as mp
import numpy as np

malha = mp.solutions.face_mesh.FaceMesh(refine_landmarks=True)

# indices de landmarks do olho esquerdo no Face Mesh
OLHO_ESQ = [33, 160, 158, 133, 153, 144]

def eye_aspect_ratio(pontos):
    # razao entre distancias verticais e horizontais do olho
    vertical = np.linalg.norm(pontos[1] - pontos[5]) + np.linalg.norm(pontos[2] - pontos[4])
    horizontal = np.linalg.norm(pontos[0] - pontos[3])
    return vertical / (2.0 * horizontal)

cap = cv2.VideoCapture("video_suspeito.mp4")
historico, piscadas = [], 0

while cap.isOpened():
    ok, frame = cap.read()
    if not ok:
        break
    res = malha.process(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
    if res.multi_face_landmarks:
        h, w = frame.shape[:2]
        lm = res.multi_face_landmarks[0].landmark
        pts = np.array([[lm[i].x * w, lm[i].y * h] for i in OLHO_ESQ])
        ear = eye_aspect_ratio(pts)
        historico.append(ear)
        if ear < 0.18:  # olho praticamente fechado = uma piscada
            piscadas += 1

cap.release()
print(f"Piscadas detectadas: {piscadas} | EAR medio: {np.mean(historico):.3f}")
# Adulto saudavel pisca ~15-20x por minuto. Quase zero piscada e bandeira vermelha.

Para o reflexo de córnea, a lógica é parecida: recortar a região de cada olho pelos landmarks, achar o ponto mais brilhante (o reflexo) em cada um e medir se as posições relativas são compatíveis com uma mesma fonte de luz. Divergência grande entre os dois olhos é um forte indício de síntese, porque o gerador não modela a física da cena, ele só preenche pixels plausíveis.

O que isso muda no seu dia a dia (e os limites)

Repare no fio condutor: nenhuma técnica sozinha resolve. FFT pega imagem sintética e erra em foto muito comprimida. ELA pega montagem e passa batido em síntese pura. MediaPipe é forte em vídeo de rosto e inútil num áudio clonado. A engenharia séria de detecção é um ensemble: você roda vários detectores independentes, normaliza cada saída e combina num score final, do mesmo jeito que um antifraude soma sinais fracos até formar uma decisão forte.

def score_sintetico(caminho_img):
    # combina evidencias independentes em uma probabilidade unica (0..1)
    razao_fft = assinatura_espectral(caminho_img)
    _, desvio_ela = error_level_analysis(caminho_img)

    # normalizacao simples por limiares calibrados no SEU dataset
    sinal_fft = min(razao_fft / 1.5, 1.0)
    sinal_ela = min(desvio_ela / 12.0, 1.0)

    # pesos definidos por validacao, nao por achismo
    score = 0.6 * sinal_fft + 0.4 * sinal_ela
    return round(score, 2)

print(f"Probabilidade de sintese: {score_sintetico('suspeito.jpg'):.0%}")

Esse número, "78% de probabilidade de geração por IA", é justamente o tipo de saída que aparece nas análises técnicas que embasam denúncias. E ele só tem valor se você puder defender como foi calculado: qual dataset calibrou os limiares, qual a taxa de falso positivo, qual versão do modelo. Detecção sem metodologia auditável é chute com cara de ciência.

Tem também o lado de quem constrói plataforma sob a Resolução 23.755. A norma fala em rotular conteúdo sintético e em responder rápido a denúncia. Na prática, isso vira pipeline: ingestão de mídia, fila de análise, score automático, revisão humana para os casos de borda e trilha de auditoria de cada decisão. Para quem cuida de segurança de aplicações, é mais um requisito não funcional entrando no backlog, ao lado de LGPD e logs de acesso.

Onde estudar a fundo? A documentação do MediaPipe Face Landmarker cobre os 468 pontos e o refinamento de íris, e o texto oficial da Resolução 23.755 do TSE traz as definições jurídicas de conteúdo sintético e os prazos. Vale ler os dois lado a lado: um te dá a ferramenta, o outro te dá o requisito.

Takeaways e próximo passo

  • O olho humano é o detector errado: os geradores de 2026 treinam contra a percepção. Use frequência, erro de compressão e física da cena.
  • Combine evidências: FFT para síntese, ELA para montagem, MediaPipe para vídeo de rosto. Nenhuma sozinha é veredito, o ensemble é.
  • Score só vale com metodologia: limiar calibrado, dataset conhecido, taxa de falso positivo medida. Sem isso, não defende uma denúncia.

Você já precisou provar que um vídeo ou print era gerado por IA? Qual técnica usou, e o que funcionou na vida real? Conta nos comentários, quero montar um segundo artigo com os casos de vocês, focado em detecção de voz clonada, que é a fronteira mais quente das eleições 2026.

Perguntas frequentes

Detectar deepfake em Python serve para uso jurídico, tipo uma denúncia no TSE?
Serve como evidência técnica, não como prova final isolada. O score ajuda a priorizar e a fundamentar, mas precisa vir com metodologia auditável: dataset de calibração, limiares, taxa de falso positivo e versão das ferramentas. Laudo sério combina o resultado automático com revisão humana qualificada.

Essas técnicas pegam imagem feita por modelo de difusão, não só por GAN?
Em parte. A assinatura de frequência é mais nítida em GANs, mas muitos pipelines de difusão também deixam artefatos de upsampling e padrões de ruído detectáveis. O ponto fraco é a evolução rápida dos modelos, por isso a calibração precisa ser refeita com frequência e o ensemble precisa de detectores novos.

Preciso de GPU ou de um modelo treinado para começar?
Para FFT, ELA e MediaPipe, não. Tudo roda em CPU com NumPy, OpenCV, Pillow e MediaPipe. GPU e modelos CNN dedicados (tipo detectores treinados em FaceForensics++) entram quando você quer subir a acurácia e processar volume, mas o baseline auditável você levanta hoje no seu notebook.

Qual o maior risco de falso positivo?
Imagens muito comprimidas, com filtro pesado ou reescaladas por redes sociais. A recompressão das plataformas bagunça tanto a frequência quanto o ELA. Por isso o limiar tem que ser calibrado no mesmo tipo de mídia que você analisa, e nunca em foto "de laboratório".

E o áudio com voz clonada, dá para detectar com Python também?
Dá, com outra caixa de ferramentas: análise espectral do áudio, detecção de artefatos de vocoder e modelos anti-spoofing. É a fronteira mais difícil das eleições 2026 porque a clonagem de voz ficou barata e convincente. Esse é o tema que pretendo abrir no próximo artigo da série.