SSIM e VMAF: medindo qualidade como o olho humano de verdade

SSIM e VMAF: medindo qualidade como o olho humano de verdade

Olá leitor, seja muito bem vindo a mais um conteúdo aqui no Portal da Micilini! 😊

Este é um artigo extra da nossa série de CODECs. Ele não tem um número fixo na sequência, mas encaixa perfeitamente logo depois do artigo 10 (Quantização), onde a gente falou sobre o PSNR como métrica de qualidade. 📐

E olha, eu preciso ser honesto com você: lá no artigo 10, eu meio que soltei um spoiler. 😅

Falei que "existem métricas mais modernas como SSIM e VMAF" e deixei no ar. Pois é, chegou a hora de pagar essa dívida. Porque pro nosso CODEC, a gente não quer só o "suficiente", a gente quer o que existe de melhor no mundo pra medir qualidade de imagem. 🏆

Então pega o café ☕ e vem entender por que o PSNR, apesar de útil, não é confiável sozinho, e como o SSIM e o VMAF resolvem isso. 🚀

O problema fundamental do PSNR

Vamos relembrar o que o PSNR faz. Ele mede o erro matemático pixel a pixel entre a imagem original e a comprimida:

MSE  = média de (pixel_original - pixel_reconstruído)²
PSNR = 10 × log₁₀( 255² / MSE )

O problema? Ele trata todos os erros como iguais. Pra ele, um erro de 10 níveis de brilho num pixel é sempre 10, não importa onde esse erro aconteça nem que tipo de erro seja.

Só que o olho humano não funciona assim. Vou te dar dois exemplos que quebram o PSNR:

Exemplo 1: deslocamento. Imagina que você pega uma imagem e desloca ela 1 pixel pra direita. Visualmente, é idêntica — ninguém percebe. Mas o PSNR despenca, porque agora quase todo pixel está "no lugar errado" quando comparado casa a casa. 😵

Exemplo 2: onde o erro acontece. Um errinho numa área de textura caótica (grama, cabelo) é invisível pro olho. O mesmo errinho numa área lisa (céu, parede) salta aos olhos na hora. O PSNR dá o mesmo peso pros dois. ⚖️❌

Ou seja: duas imagens com o mesmo PSNR podem parecer completamente diferentes pro olho humano. E é aí que entram as métricas perceptuais. 🧠

SSIM: comparando estrutura, não pixels soltos

O SSIM (Structural Similarity Index — Índice de Similaridade Estrutural) nasceu em 2004 de uma sacada genial: o olho humano não percebe imagem pixel por pixel, ele percebe estruturas (bordas, texturas, padrões e contornos). 🖼️

Então, em vez de perguntar "quão diferente é cada pixel?", o SSIM pergunta "quão parecida é a estrutura das duas imagens?". Pra isso, ele compara três coisas separadamente:

  • ☀️ Luminância (brilho médio): as duas regiões têm o mesmo nível de claro/escuro?
  • 🌗 Contraste (variação): as duas regiões têm a mesma "amplitude" de variação?
  • 🧩 Estrutura (correlação): os padrões variam juntos, na mesma direção?

E o mais importante: essa comparação não é feita na imagem inteira de uma vez. Ela é feita numa janelinha deslizante (tipicamente 11×11 pixels com peso gaussiano) que percorre a imagem toda. No final, você tira a média de todas as janelas, o que chamamos de MSSIM (Mean SSIM).

Isso faz a métrica ser local, do jeito que o olho realmente é.

A fórmula do SSIM

Não fuja, ela é mais amigável do que parece:

SSIM(x,y) = [ (2·μx·μy + C1) · (2·σxy + C2) ]
            ─────────────────────────────────────
            [ (μx² + μy² + C1) · (σx² + σy² + C2) ]

onde:
  μx, μy   = média (brilho) de cada janela
  σx², σy² = variância (contraste) de cada janela
  σxy      = covariância entre as duas janelas (a estrutura!)
  C1, C2   = constantes pequenas pra evitar divisão por zero

As constantes são padronizadas assim:

C1 = (0.01 × 255)² = 6.5025
C2 = (0.03 × 255)² = 58.5225

O que você precisa levar deste bloco de matemática:

  • O termo 2·μx·μy compara brilho. ☀️
  • O termo com σx² e σy² compara contraste. 🌗
  • O termo σxy (a covariância) é a alma da coisa: mede se as duas imagens variam juntas. Se onde uma sobe a outra sobe, e onde uma desce a outra desce, a estrutura é preservada. 🧩

E a melhor parte: o resultado é um número de -1 a 1 que é super fácil de se ler. 

Como ler o SSIM

Sua leitura se dá de forma super fácil, vejamos:

  • SSIM = 1.0 → imagens idênticas. Perfeição. 🤩
  • SSIM > 0.95 → diferença praticamente imperceptível. 😍
  • SSIM entre 0.90 e 0.95 → boa qualidade, perdas leves. 🙂
  • SSIM entre 0.80 e 0.90 → perdas visíveis, começa a incomodar. 😕
  • SSIM < 0.80 → degradação clara. 😬

Diferente do PSNR (que vai de 0 a infinito em dB e é meio abstrato), o SSIM tem uma escala limitada e intuitiva. "0.96" já te diz na hora que a imagem está muito boa.

SSIM na prática: o código em C

Vamos implementar o SSIM do zero e rodar ele nos mesmos blocos do artigo 10, pra você ver como ele se compara ao PSNR com números reais.

Pra manter o exemplo direto ao ponto, vou calcular o SSIM global (uma janela só, cobrindo o bloco 8×8 inteiro). Numa imagem real, você deslizaria a janela 11×11 e tiraria a média, mas o cálculo interno de cada janela é exatamente este:

#include <stdio.h>
#include <math.h>

#define BLOCK_SIZE 8

// ============================================================
// SSIM entre dois blocos (janela única, global)
// ============================================================
//
// Compara luminância (média), contraste (variância) e
// estrutura (covariância) entre os blocos x e y.
// Retorna um valor de -1 a 1 (1 = idênticos).

double calcular_ssim(double x[BLOCK_SIZE][BLOCK_SIZE],
                     double y[BLOCK_SIZE][BLOCK_SIZE]) {
    // Constantes padrão do SSIM (baseadas em L=255)
    double C1 = 6.5025;    // (0.01 * 255)^2
    double C2 = 58.5225;   // (0.03 * 255)^2

    int n = BLOCK_SIZE * BLOCK_SIZE;

    // Passo 1: médias (μx, μy) — o "brilho" de cada bloco
    double mx = 0.0, my = 0.0;
    for (int r = 0; r < BLOCK_SIZE; r++)
        for (int c = 0; c < BLOCK_SIZE; c++) {
            mx += x[r][c];
            my += y[r][c];
        }
    mx /= n;
    my /= n;

    // Passo 2: variâncias (σx², σy²) e covariância (σxy)
    double vx = 0.0, vy = 0.0, cxy = 0.0;
    for (int r = 0; r < BLOCK_SIZE; r++)
        for (int c = 0; c < BLOCK_SIZE; c++) {
            double dx = x[r][c] - mx;
            double dy = y[r][c] - my;
            vx  += dx * dx;   // variância de x (contraste)
            vy  += dy * dy;   // variância de y (contraste)
            cxy += dx * dy;   // covariância (estrutura!)
        }
    vx  /= (n - 1);
    vy  /= (n - 1);
    cxy /= (n - 1);

    // Passo 3: a fórmula do SSIM
    double numerador   = (2*mx*my + C1) * (2*cxy + C2);
    double denominador = (mx*mx + my*my + C1) * (vx + vy + C2);

    return numerador / denominador;
}

int main() {
    // Bloco original (o clássico do artigo 9/10)
    double original[BLOCK_SIZE][BLOCK_SIZE] = {
        {52, 55, 61, 66, 70, 61, 64, 73},
        {63, 59, 55, 90,109, 85, 69, 72},
        {62, 59, 68,113,144,104, 66, 73},
        {63, 58, 71,122,154,106, 70, 69},
        {67, 61, 68,104,126, 88, 68, 70},
        {79, 65, 60, 70, 77, 68, 58, 75},
        {85, 71, 64, 59, 55, 61, 65, 83},
        {87, 79, 69, 68, 65, 76, 78, 94}
    };

    // SSIM de um bloco com ele mesmo = 1.0 (prova que a fórmula está certa)
    printf("SSIM (bloco vs ele mesmo) = %.4f\n", calcular_ssim(original, original));

    return 0;
}

Pra compilar:

gcc ssim.c -o ssim -lm
./ssim

SSIM vs PSNR: os números lado a lado

No meu caso, vamos validar o PSNR nos três níveis de qualidade do artigo 10 (o bloco passa por DCT → quantização → dequantização → IDCT, e comparamos com o original).

Estes são os resultados reais (validados rodando 😉):

SSIM (bloco vs ele mesmo) = 1.0000

Qualidade  90 -> PSNR: 40.10 dB | SSIM: 0.9933
Qualidade  50 -> PSNR: 32.60 dB | SSIM: 0.9629
Qualidade  10 -> PSNR: 25.69 dB | SSIM: 0.8113

Olha como o SSIM conta a história de um jeito mais intuitivo:

  • Qualidade 90: SSIM 0.9933 — quase 1, imagem excelente. 💎
  • Qualidade 50: SSIM 0.9629 — ainda muito bom, perdas discretas. 🙂
  • Qualidade 10: SSIM 0.8113 — caiu bem, degradação visível. 😬

Repara que enquanto o PSNR fala em "40 dB, 32 dB, 25 dB" (uma escala que você precisa ter experiência pra interpretar), o SSIM fala em "0.99, 0.96, 0.81" — e qualquer um entende na hora que 0.81 é bem pior que 0.99.

VMAF: quando a Netflix entra em campo

Se o SSIM é bom, o VMAF é o estado da arte. E ele tem uma história bacana. 📺

VMAF significa Video Multi-Method Assessment Fusion (Fusão de Avaliação Multi-Método de Vídeo).

Foi criado pela Netflix (em parceria com a Universidade de Austin) por uma necessidade bem prática: a Netflix precisava decidir, pra cada filme e cada velocidade de internet, qual nível de compressão usar, sem estragar a experiência de milhões de assinantes. 🌍

O PSNR e o SSIM não eram bons o suficiente pra essa decisão de bilhões de dólares. Então eles construíram algo novo. 💡

Mas dai você pode estar se perguntando: "O que torna o VMAF diferente?".

A sacada do VMAF é que ele não é uma fórmula única. Ele é um modelo de machine learning que combina (faz a "fusão" de) várias métricas diferentes, cada uma boa em capturar um tipo de degradação:

  • 🔬 VIF (Visual Information Fidelity): mede quanta informação visual foi preservada, em várias escalas.
  • 📐 DLM (Detail Loss Metric): mede especificamente a perda de detalhe e o quanto isso incomoda.
  • 🎞️ Motion: mede o movimento entre quadros (isso é específico de vídeo — imagem parada não tem).

Essas métricas viram "ingredientes" que alimentam um modelo de ML (originalmente um SVM — Support Vector Machine).

E aqui está o pulo do gato:

O modelo do VMAF foi treinado com notas dadas por humanos reais assistindo a vídeos degradados. Ou seja, ele aprendeu a prever o que uma pessoa de verdade acharia da qualidade.

Por isso o VMAF é considerado a métrica que mais se aproxima da percepção humana real. Ele não está medindo matemática, está prevendo a nota que você daria.

O resultado do VMAF é um número de 0 a 100, onde 100 é qualidade perfeita e valores acima de ~93 são geralmente indistinguíveis do original pro olho humano. 👁️

Como usar o VMAF de verdade

Aqui eu preciso ser 100% honesto com você...

Diferente do PSNR e do SSIM, que a gente implementou do zero em pouquíssimas linhas, o VMAF não é algo que você reimplementa numa tarde.

Ele depende de um modelo de ML pré-treinado (arquivos de milhares de parâmetros) e de várias métricas complexas por baixo. Tentar recriar isso do zero seria reinventar a roda de um jeito pior.

A boa notícia: a Netflix abriu o código do VMAF (a libvmaf), e ela já vem integrada no FFmpeg. Então, na prática, você usa assim:  

ffmpeg -i original.mp4 -i comprimido.mp4 -lavfi libvmaf -f null -

// ou pra imagens/frames:
ffmpeg -i original.png -i comprimido.png -lavfi libvmaf -f null -

E ele te devolve o score VMAF direto. Pro nosso CODEC, é exatamente assim que vamos avaliar: geramos a imagem comprimida com o nosso CODEC, e rodamos o VMAF (via libvmaf) contra o original pra ter uma nota que reflete a percepção humana.

O quadro comparativo: PSNR vs SSIM vs VMAF

Deixa eu resumir os três num comparativo direto:

┌──────────┬─────────────┬──────────────┬─────────────────┬──────────────┐
│ Métrica  │ Escala      │ O que mede   │ Perceptual?     │ Complexidade │
├──────────┼─────────────┼──────────────┼─────────────────┼──────────────┤
│ PSNR     │ 0 a ∞ (dB)  │ Erro pixel   │ ❌ Não          │ Trivial      │
│          │             │ a pixel      │                 │ (fórmula)    │
├──────────┼─────────────┼──────────────┼─────────────────┼──────────────┤
│ SSIM     │ -1 a 1      │ Estrutura    │ ⚠️ Parcialmente │ Simples      │
│          │             │ local        │                 │ (fórmula)    │
├──────────┼─────────────┼──────────────┼─────────────────┼──────────────┤
│ VMAF     │ 0 a 100     │ Percepção    │ ✅ Sim (treinado│ Alta         │
│          │             │ humana       │ com humanos)    │ (modelo ML)  │
└──────────┴─────────────┴──────────────┴─────────────────┴──────────────┘

Nenhum deles é "o único certo". Cada um tem seu papel.

Porque o nosso CODEC vai usar os 3?

Aqui está a decisão de arquitetura pro nosso CODEC, e o motivo de cada escolha:

  • 📐 PSNR — é a nossa bússola rápida. Trivial de calcular, ótimo pra um sanity check durante o desenvolvimento ("será que quebrei alguma coisa?"). Se o PSNR despencou de repente, tem bug.
  • 🏗️ SSIM — é a nossa régua estrutural. Fácil de implementar, roda rápido, e já captura muito melhor o que o olho percebe. Vai ser o nosso indicador do dia a dia de "a qualidade está boa?".
  • 🎬 VMAF — é o nosso juiz final. Quando a gente for comparar o nosso CODEC contra JPEG, PNG e WebP (lá no artigo 17, o benchmark 💪), o VMAF vai dar a palavra final sobre qualidade percebida, porque é a métrica que mais se aproxima de um humano de verdade avaliando.

Por que os três em vez de só um? Porque medir qualidade é difícil, e cada métrica tem um ponto cego.

Usando os três juntos, a gente cobre os pontos cegos uns dos outros. É a diferença entre um CODEC "amador" (que confia cegamente num único número) e um CODEC feito com rigor de verdade. 🎯

E como eu tinha dito anteriormente... a gente quer tudo do bom e do melhor pro nosso CODEC!

Resumo

  • 🔴 O PSNR mede erro matemático pixel a pixel e trata todos os erros como iguais — o que não reflete como o olho funciona.
  • 🏗️ O SSIM compara estrutura (brilho, contraste e correlação) em janelas locais, resultando num valor de -1 a 1 muito mais intuitivo e perceptual.
  • 🤖 O VMAF é um modelo de machine learning da Netflix que funde várias métricas (VIF, DLM, motion) e foi treinado com notas de humanos reais, e é o mais próximo da percepção humana, numa escala de 0 a 100.
  • ⚙️ PSNR e SSIM a gente implementa do zero; VMAF a gente usa via libvmaf/FFmpeg (não vale a pena reimplementar um modelo de ML pré-treinado).
  • 🏆 O nosso CODEC vai usar os três: PSNR como sanity check rápido, SSIM como régua estrutural do dia a dia, e VMAF como juiz final no benchmark. Cada um cobre o ponto cego do outro.

Agora sim estamos armados com as ferramentas certas pra avaliar o nosso CODEC com o rigor que ele merece. 🛠️

Voltando pra série principal: no próximo artigo da sequência, retomamos de onde o artigo 10 parou — a varredura em zig-zag e o RLE, transformando aquele mar de zeros da quantização em bytes de verdade num arquivo.

Até lá!

Criadores de Conteúdo

Foto do William Lima
William Lima
Fundador da Micilini

Inventor nato, escreve conteudos de programação para o portal da micilini.

Torne-se um MIC 🤖

Mais de 100 mic's já estão conectados na plataforma.