Skip to content

Latest commit

 

History

History
984 lines (643 loc) · 70.3 KB

README-pt.md

File metadata and controls

984 lines (643 loc) · 70.3 KB

🇺🇸 🇨🇳 🇯🇵 🇮🇹 🇰🇷 🇷🇺 🇧🇷 🇪🇸

license

Introdução

Essa é uma rápida introdução a tecnologia de vídeo para pessoas desenvolvedoras de software. Entretanto, queremos que esse documento seja fácil o suficiente para qualquer pessoa aprender. A idéia nasceu de uma pequena oficina para pessoas interessadas em tecnologia de vídeo.

O objetivo principal do documento é introduzir alguns conceitos de vídeo digital utilizando um vocabulário simples, elementos visuais e exemplos práticos. Além disso, gostaríamos que esse conhecimento estivesse disponível em qualquer lugar. Por favor, fique à vontade para enviar correções, sugestões e melhorias.

Haverão exercícios práticos que irão necessitar do docker instalado e esse repositório clonado.

git clone https://github.com/leandromoreira/digital_video_introduction.git
cd digital_video_introduction
./setup.sh

ATENCÃO: quando você ver um comando ./s/ffmpeg ou ./s/mediainfo, isso quer dizer que estamos executando uma versão do programa dentro de um container, sendo assim, esse programa já possui todas as dependências instaladas e configuradas para sua execução.

Todos os exercícios práticos devem ser executados da pasta que você clonou originalmente esse repositório. Para os exemplos que utilizam Jupyter, você deve inicializar o servidor utilizando ./s/start_jupyter.sh e acessar a URL no seu browser.

Registro de alterações

  • Adicionado detalhes sobre sistemas de DRM (Digital Rights Management)
  • Versão 1.0.0 lançada
  • Adicionado tradução para chinês simplificado
  • Adicionado exemplo de filtro osciloscópio de FFmpeg

Índice

Terminologia Básica

Uma imagem podem ser pensada como uma matriz 2d. Se nós começamos a pensar sobre cores, podemos extrapolar essa idéia inicial e ver essa imagem como uma matriz 3d onde as dimensões adicionais são utilizadas para armazenar informações de cor.

Se escolhemos representar essas cores com cores primárias (vermelho, verde and azul), podemos definir três planos: o primeiro plano para o vermelho, o segundo para o verde e o último para a cor azul.

uma imagem é uma matriz 3D RGB

Iremos chamar cada ponto nessa matriz de um pixel (picture element). Um pixel representa a intensidade (geralmente um valor numérico) de uma dada cor. Por exemplo, um pixel vermelho significa 0 de verde, 0 de azul e o máximo de vermelho. O pixel cor rosa pode ser formado a partir de uma combinação de três cores. Usando uma representação numérica de 0 até 255, onde o pixel rosa é definido por Vermelho=255, Verde=192 e Azul=203.

Outras maneiras de codificar uma imagem colorida

Muitos outros possíveis modelos podem ser utilizados para representar uma imagem colorida. Nós podemos, por exemplo, utilizar uma paleta indexada de cores, onde um único byte representa cada pixel, ao invés de três bytes quando utilizando o modelo RGB. Em um modelo como esse, podemos utilizar uma matriz 2D ao invés de uma matriz 3D para representar cor. Dessa maneira, nós iremos salvar memória mas teremos potencialmente menos opções de cores.

paleta do NES

Por exemplo, olhe para a figura abaixo. O primeiro rosto ali é totalmente colorido. Os outros rostos são planos vermelho, verde e azul (mostrados em tons de cinza).

Intensidade de canais de cores RGB

Nós podemos ver que a cor vermelha será a cor que contribuíra mais (as partes mais brilhantes no segundo rosto) para a cor final, enquanto que a contribuição da cor azul pode ser vista mais concentrada nos olhos do Mário (último rosto) e em algumas partes de suas roupas. Perceba como todos os planos de cores (partes escuras) contribuem menos para o bigode do Mário.

Cada intensidade de cor requer um determinada quantidade de bits, essa quantidade é chamada de profundidade de bit. Digamos que vamos gastar 8 bits (tendo como valores válidos 0 até 255) por cor (plano), dessa forma, nós temos uma profundidade de cor de 24 bits (8 bits vezes 3 planos RGB). Também podemos inferir que podemos utilizar 2 na 24 (2^24) diferentes cores.

É ótimo para aprender como uma imagem se transforma em bits.

Uma outra propriedade de uma imagem é a resolução, que é basicamente o número de pixels em uma dimensão. É geralmente representada por altura x largura, por exemplo: a imagem 4x4 abaixo:

resolução da imagem

Prática: brincando com imagens e cores

Você pode brincar com imagens e cores utilizando jupyter (python, numpy, matplotlib, etc).

Você também pode aprender como filtros de imagem (detecção de bordas, nitidez, desfoque...) funcionam.

Uma outra propriedade que podemos perceber enquanto trabalhamos com imagens ou vídeo é a proporção da tela que descreve um relacionamento proporcional entre altura e largura de uma imagem ou pixel.

Quando as pessoas falam que um filme ou foto é 16x9 geralmente elas estão se referindo ao Display Aspect Ratio (DAR), entretanto nós também podemos ter diferentes formas de pixels e podemos chamar isso de Pixel Aspect Ratio (PAR).

display aspect ratio

pixel aspect ratio

DVD é DAR 4:3

Mesmo que a resolução real de um DVD seja 704x480, a proporção é mantida em 4:3 considerando que esse filme possui PAR de 10:11 (704x10/480x11)

Finalmente, podemos definir um vídeo como uma sucessão de n frames no tempo que podemos perceber como uma outra dimensão, onde n é a quantidade de quadros por segundo ou frames per second (FPS).

video

O número de bits por segundo necessários para exibir um vídeo é sua taxa de bits.

taxa de bits = largura * altura * profundidade de bits * quadros por segundo

Por exemplo, um vídeo com 30 frames por segundo, 24 bits por pixel, resolução de 480x240 necessitará de 82.944.000 bits por segundo ou 82.944 Mbps (30x480x240x24) se não empregarmos nenhum tipo de compressão.

Quando a taxa de bits é quase constante, ela é chamada de taxa de bits constante (CBR), mas também pode variar, chamada de taxa de bits variável (VBR).

Este gráfico mostra um VBR restrito que não gasta muitos bits enquanto o quadro é preto.

constrained vbr

No início, os engenheiros criaram uma técnica para dobrar a taxa de quadros percebida de uma exibição de vídeo sem consumir largura de banda extra. Essa técnica é conhecida como vídeo entrelaçado; basicamente envia metade da tela em 1 "quadro" e a outra metade no próximo "quadro".

Hoje, as telas são renderizadas principalmente usando a técnica de varredura progressiva. Progressivo é uma forma de exibir, armazenar ou transmitir imagens em movimento em que todas as linhas de cada quadro são desenhadas em sequência.

interlaced vs progressive

Agora temos uma ideia de como uma imagem é representada digitalmente, como suas cores são arranjadas, quantos bits por segundo gastamos para mostrar um vídeo, se é constante (CBR) ou variável (VBR), com uma determinada resolução usando uma determinada taxa de quadros e muitos outros termos como entrelaçado, PAR e outros.

Exercício prático: Verifique as propriedades do vídeo

Você pode verificar a maioria das propriedades explicadas com ffmpeg ou mediainfo.

Remoção de redundância

Aprendemos que não é viável usar vídeo sem compressão; um único vídeo de uma hora em resolução de 720p com 30fps requer 278 GB*. Como usar apenas algoritmos de compactação de dados sem perdas como DEFLATE (usado em PKZIP, Gzip e PNG), não diminuirá suficientemente a largura de banda necessária, precisamos encontrar outras maneiras de compactar o vídeo.

* Encontramos esse número multiplicando 1280 x 720 x 24 x 30 x 3600 (largura, altura, bits por pixel, fps e tempo em segundos)

Para fazer isso, podemos explorar como nossa visão funciona. Distinguimos melhor o brilho do que as cores, as repetições no tempo, um vídeo contém muitas imagens com poucas alterações, e as repetições dentro da imagem, cada quadro também contém muitas áreas usando o mesmo ou cor semelhante.

Cores, Luminância e nossos olhos

Nossos olhos são mais sensíveis à luminosidade do que às cores, você pode testar isso por si mesmo, olhe para essa imagem.

luminance vs color

Se você não consegue ver que as cores dos quadrados A e B são idênticas no lado esquerdo, tudo bem, é nosso cérebro nos enganando para prestarmos mais atenção à luz e à escuridão do que à cor. Há um conector, da mesma cor, no lado direito para que possamos (nosso cérebro) facilmente perceber que, na verdade, são a mesma cor.

Explicação simplista de como nossos olhos funcionam O olho é um órgão complexo, é composto por muitas partes, mas estamos principalmente interessados nas células cones e bastonetes. O olho contém cerca de 120 milhões de bastonetes e 6 milhões de células cones.

Para simplificar bastante, vamos tentar colocar as cores e a luminosidade na função das partes do olho. As células bastonetes são principalmente responsáveis pela luminosidade enquanto as células cones são responsáveis pela cor, existem três tipos de cones, cada um com um pigmento diferente, a saber: S-cones (azul), M-cones (verde) e L-cones (vermelho).

Uma vez que temos muitas mais células bastonetes (luminosidade) do que células cones (cor), pode-se inferir que somos mais capazes de distinguir entre claro e escuro do que cores.

eyes composition

Funções de sensibilidade ao contraste

Pesquisadores da psicologia experimental e muitos outros campos desenvolveram muitas teorias sobre a visão humana. E uma delas é chamada de funções de sensibilidade ao contraste. Elas estão relacionadas ao espaço e ao tempo da luz e seu valor indica, para uma determinada intensidade de luz inicial, quanto mudança é necessária antes que um observador informe que houve uma mudança. Observe o plural da palavra "função", isso ocorre porque podemos medir as funções de sensibilidade ao contraste não apenas em preto e branco, mas também em cores. O resultado desses experimentos mostra que, na maioria dos casos, nossos olhos são mais sensíveis ao brilho do que à cor.

Uma vez que sabemos que somos mais sensíveis à luma (a luminosidade em uma imagem), podemos tentar explorá-la.

Modelo de cor

Aprendemos primeiro como funcionam as imagens coloridas usando o modelo RGB (consulte terminologia básica), mas existem outros modelos também. Na verdade, existe um modelo que separa a luminância (brilho) da crominância (cores) e é conhecido como YCbCr*.

* existem mais modelos que fazem a mesma separação.

Este modelo de cor usa Y para representar o brilho e dois canais de cor Cb (azul cromático) e Cr (vermelho cromático). O YCbCr pode ser derivado do RGB e também pode ser convertido de volta para o RGB. Usando este modelo, podemos criar imagens totalmente coloridas, como podemos ver abaixo.

ycbcr example

Conversão entre YCbCr e RGB

Alguns podem argumentar, como podemos produzir todas as cores sem usar o verde?

Para responder a essa pergunta, vamos percorrer uma conversão de RGB para YCbCr. Usaremos os coeficientes do standard BT.601 recomendado pelo grupo ITU-R*. O primeiro passo é calcular a luminância, usaremos as constantes sugeridas pelo ITU e substituiremos os valores RGB.

Y = 0.299R + 0.587G + 0.114B

Uma vez que temos a luminância, podemos separar as cores (crominância azul e vermelha):

Cb = 0.564(B - Y)
Cr = 0.713(R - Y)

E também podemos convertê-la de volta e até mesmo obter o verde usando YCbCr.

R = Y + 1.402Cr
B = Y + 1.772Cb
G = Y - 0.344Cb - 0.714Cr

* grupos e padrões são comuns em vídeo digital, eles geralmente definem quais são os padrões, por exemplo, o que é 4K? qual taxa de quadros devemos usar? resolução? modelo de cores?

Geralmente, monitores (monitores, televisores, telas, etc.) utilizam apenas o modelo RGB, organizado de maneiras diferentes, veja alguns deles ampliados abaixo:

pixel geometry

Subamostragem Chroma

Com a imagem representada como componentes de luminância e crominância, podemos aproveitar a maior sensibilidade do sistema visual humano para a resolução de luminância em vez de crominância para remover seletivamente informações. A subamostragem de crominância é a técnica de codificação de imagens usando menor resolução para crominância do que para luminância.

ycbcr subsampling resolutions

Quanto devemos reduzir a resolução de crominância? Descobriu-se que já existem alguns esquemas que descrevem como lidar com a resolução e a combinação (cor final = Y + Cb + Cr).

Esses esquemas são conhecidos como sistemas de subamostragem e são expressos como uma razão de 3 partes - a:x:y - que define a resolução de crominância em relação a um bloco a x 2 de pixels de luminância.

  • a é a referência de amostragem horizontal (geralmente 4)
  • x é o número de amostras de crominância na primeira linha de pixels a (resolução horizontal em relação a a)
  • y é o número de mudanças de amostras de crominância entre a primeira e a segunda linhas de pixels a.

Uma exceção a isso existe com 4:1:0, que fornece uma única amostra de crominância dentro de cada bloco 4 x 4 de resolução de luma.

Os esquemas comuns usados em codecs modernos são: 4:4:4 (sem subsampling), 4:2:2, 4:1:1, 4:2:0, 4:1:0 e 3:1:1.

Você pode acompanhar algumas discussões para aprender mais sobre a subsampling de crominância.

YCbCr 4:2:0 merge

Aqui está um trecho de uma imagem mesclada usando YCbCr 4:2:0, observe que gastamos apenas 12 bits por pixel.

YCbCr 4:2:0 merge

Aqui está um trecho de uma imagem mesclada usando YCbCr 4:2:0, observe que gastamos apenas 12 bits por pixel.

Você pode ver a mesma imagem codificada pelos principais tipos de subsampling de croma, as imagens na primeira linha são o YCbCr final, enquanto a última linha de imagens mostra a resolução croma. É realmente uma grande vitória para uma perda tão pequena.

chroma subsampling examples

Anteriormente, tínhamos calculado que precisávamos de 278GB de armazenamento para manter um arquivo de vídeo com uma hora de resolução 720p e 30fps. Se usarmos YCbCr 4:2:0 podemos cortar pela metade esse tamanho (139 GB)*, mas ainda está longe do ideal.

* encontramos esse valor multiplicando largura, altura, bits por pixel e fps. Anteriormente, precisávamos de 24 bits, agora precisamos apenas de 12.


Prática: Verifique um histograma YCbCr

Você pode verificar o histograma YCbCr com o ffmpeg. Essa cena tem uma contribuição maior de azul, como mostrado pelo histograma.

ycbcr color histogram

Cor, luma, luminância, revisão de vídeo gamma

Assista a este incrível vídeo explicando o que é luma e aprenda sobre luminância, gamma e cor. Analog Luma - Uma história e explicação do vídeo

Pratica: Verificar a intensidade do YCbCr

Você pode visualizar a intensidade do Y para uma determinada linha de um vídeo usando o filtro osciloscópio do FFmpeg.

ffplay -f lavfi -i 'testsrc2=size=1280x720:rate=30000/1001,format=yuv420p' -vf oscilloscope=x=0.5:y=200/720:s=1:c=1

y color oscilloscope

Tipos de Frames

Agora podemos prosseguir e tentar eliminar a redundância temporal, mas antes disso vamos estabelecer algumas terminologias básicas. Suponha que temos um filme com 30fps, aqui estão seus primeiros 4 quadros.

ball 1 ball 2 ball 3 ball 4

Podemos ver muitas repetições dentro dos quadros, como o fundo azul, que não muda do quadro 0 ao quadro 3. Para lidar com esse problema, podemos categorizá-los abstratamente em três tipos de quadros.

I Frame (intra, keyframe)

Um quadro I (referência, chave, intra) é um quadro autocontido. Ele não depende de nada para ser renderizado, um quadro I se parece com uma foto estática. O primeiro quadro geralmente é um quadro I, mas veremos quadros I inseridos regularmente entre outros tipos de quadros.

ball 1

P Frame (predicted)

Um quadro P (previsão) aproveita o fato de que quase sempre a imagem atual pode ser renderizada usando o quadro anterior. Por exemplo, no segundo quadro, a única mudança foi a bola que se moveu para frente. Podemos reconstruir o quadro 1, usando apenas a diferença e referenciando o quadro anterior.

ball 1 <- ball 2

Hands-on: A video with a single I-frame

Já que um quadro P usa menos dados, por que não codificar um vídeo com apenas um quadro I e todos os outros quadros sendo P?

Depois de codificar esse vídeo, comece a assisti-lo e faça uma busca por uma parte avançada do vídeo, você notará que leva algum tempo para realmente ir para essa parte. Isso acontece porque um quadro P precisa de um quadro de referência (como um quadro I, por exemplo) para ser renderizado.

Outro teste rápido que você pode fazer é codificar um vídeo usando apenas um quadro I e, em seguida, codificá-lo inserindo um quadro I a cada 2s e verificar o tamanho de cada versão.

B Frame (bi-predictive)

E quanto a referenciar quadros passados e futuros para proporcionar uma compressão ainda melhor?! Isso é basicamente o que um quadro B faz.

ball 1 <- ball 2 -> ball 3

Hands-on: Compare videos with B-frame

Você pode gerar duas versões, uma com quadros B e outra com nenhum quadro B e verificar o tamanho do arquivo, bem como a qualidade.

Sumário

Esses tipos de quadros são usados para proporcionar uma melhor compressão. Veremos como isso acontece na próxima seção, mas por enquanto podemos pensar que o quadro I (I-frame) é mais caro enquanto o P é mais barato, mas o mais barato é o quadro B (B-frame).

frame types example

Redundância Temporal (inter prediction)

Vamos explorar as opções que temos para reduzir as repetições no tempo, esse tipo de redundância pode ser resolvido com técnicas de inter-prediction.

Vamos tentar usar menos bits para codificar a sequência de frames 0 e 1.

original frames

Uma coisa que podemos fazer é subtração, simplesmente subtraindo o frame 1 do frame 0, obtemos apenas o que precisamos para codificar o residual.

delta frames

Mas e se eu te disser que há um método melhor que usa ainda menos bits?! Primeiro, vamos tratar o frame 0 como uma coleção de partições bem definidas e, em seguida, tentaremos combinar os blocos do frame 0 no frame 1. Podemos pensar nisso como estimativa de movimento.

Wikipedia - compensação de movimento por blocos

"A compensação de movimento por blocos divide o quadro atual em blocos não sobrepostos, e o vetor de compensação de movimento indica de onde esses blocos vêm (uma crença comum é que o quadro anterior é dividido em blocos não sobrepostos, e os vetores de compensação de movimento indicam para onde esses blocos se movem). Os blocos de origem geralmente se sobrepõem no quadro de origem. Alguns algoritmos de compressão de vídeo montam o quadro atual a partir de peças de vários quadros transmitidos anteriormente."

delta frames

Nós poderíamos estimar que a bola se moveu de x=0, y=25 para x=6, y=26, os valores x e y são os vetores de movimento. Um passo adicional que podemos fazer para economizar bits é codificar apenas a diferença do vetor de movimento entre a última posição do bloco e a prevista, então o vetor de movimento final seria x=6 (6-0), y=1 (26-25).

Em uma situação do mundo real, essa bola seria dividida em n partições mas o processo é o mesmo.

Os objetos na cena se movem de forma 3D, a bola pode ficar menor quando se move para o fundo. É normal que não encontremos a correspondência perfeita para o bloco que tentamos encontrar. Aqui está uma visão superposta de nossa estimativa em comparação com a imagem real.

motion estimation

Mas podemos ver que quando aplicamos a estimativa de movimento, os dados para codificar são menores do que quando usamos apenas as técnicas de quadro delta.

motion estimation vs delta

Como seria a compensação de movimento real

Essa técnica é aplicada a todos os blocos, muitas vezes uma bola seria dividida em mais de um bloco. real world motion compensation Fonte: https://web.stanford.edu/class/ee398a/handouts/lectures/EE398a_MotionEstimation_2012.pdf

Você pode praticar esses conceitos usando o jupyter.

Prática: Veja os vetores de movimento

Podemos gerar um vídeo com a interpolação (vetores de movimento) usando o ffmpeg.

inter prediction (motion vectors) with ffmpeg

Ou podemos usar o Intel Video Pro Analyzer (que é pago, mas há uma versão de teste gratuita que limita você a trabalhar apenas com os primeiros 10 quadros).

inter prediction intel video pro analyzer

Redundância Espacial (intra prediction)

Se analisarmos cada quadro em um vídeo, veremos que também há muitas áreas que estão correlacionadas.

Vamos caminhar por um exemplo. Esta cena é composta principalmente por cores azuis e brancas.

Este é um I-frame e não podemos usar quadros anteriores para fazer previsões, mas ainda assim podemos comprimi-lo. Vamos codificar a seleção do bloco vermelho. Se olharmos para seus vizinhos, podemos estimar que há uma tendência de cores ao redor dele.

Vamos prever que o quadro continuará a espalhar as cores verticalmente, o que significa que as cores dos pixels desconhecidos terão os valores de seus vizinhos.

Nossa previsão pode estar errada, por essa razão, precisamos aplicar essa técnica (intra-prediction) e depois subtrair os valores reais, o que nos dá o bloco residual, resultando em uma matriz muito mais compressível em comparação com a original.

Existem muitos tipos diferentes desse tipo de previsão. O que você vê aqui na imagem é uma forma de previsão planar direta, onde os pixels da linha acima do bloco são copiados linha por linha dentro do bloco. A previsão planar também pode envolver um componente angular, onde pixels tanto da esquerda quanto da parte superior são usados para ajudar a prever o bloco atual. E há também a previsão DC, que envolve a média das amostras imediatamente acima e à esquerda do bloco.

Prática: Verifique intra prediction

Você pode gerar um vídeo com macroblocos e suas previsões usando o ffmpeg. Por favor, verifique a documentação do ffmpeg para entender o significado de cada cor de bloco.

intra prediction (macro blocks) with ffmpeg

Ou podemos usar o Intel Video Pro Analyzer (que é pago, mas há uma versão de avaliação gratuita que limita você a trabalhar apenas com os primeiros 10 quadros).

intra prediction intel video pro analyzer

Como um codec de vídeo funciona?

O quê? Por quê? Como?

O quê? É um software / hardware que comprime ou descomprime vídeo digital. Por quê? O mercado e a sociedade exigem vídeos de alta qualidade com largura de banda ou armazenamento limitado. Lembra quando calculamos a largura de banda necessária para 30 quadros por segundo, 24 bits por pixel, resolução de um vídeo de 480x240? Era 82,944 Mbps sem aplicar compressão. É a única maneira de fornecer HD / FullHD / 4K em TVs e na Internet. Como? Daremos uma breve olhada nas principais técnicas aqui.

CODEC vs Container

Um erro comum que iniciantes frequentemente cometem é confundir o codec de vídeo digital e o formato de contêiner de vídeo digital. Podemos pensar em contêineres como um formato de invólucro que contém metadados do vídeo (e possivelmente também de áudio), e o vídeo comprimido pode ser visto como sua carga útil.

Geralmente, a extensão de um arquivo de vídeo define seu contêiner de vídeo. Por exemplo, o arquivo video.mp4 provavelmente é um contêiner MPEG-4 Part 14 e um arquivo chamado video.mkv provavelmente é um matroska. Para ter certeza absoluta sobre o codec e o formato do contêiner, podemos usar o ffmpeg ou mediainfo.

História

Antes de entrarmos nos detalhes de funcionamento interno de um codec genérico, vamos dar uma olhada para entender um pouco melhor sobre alguns codecs de vídeo antigos.

O codec de vídeo H.261 nasceu em 1990 (tecnicamente em 1988), e foi projetado para trabalhar com taxas de dados de 64 kbit/s. Ele já usava ideias como subsampling de croma, bloco macro, entre outros. No ano de 1995, o padrão de codec de vídeo H.263 foi publicado e continuou a ser estendido até 2001.

Em 2003, a primeira versão do H.264/AVC foi concluída. No mesmo ano, a On2 Technologies (anteriormente conhecida como Duck Corporation) lançou seu codec de vídeo como uma compressão de vídeo lossy e livre de royalties chamada VP3. Em 2008, o Google comprou esta empresa, lançando o VP8 no mesmo ano. Em dezembro de 2012, o Google lançou o VP9 e é suportado por aproximadamente ¾ do mercado de navegadores (incluindo mobile).

AV1 é um novo codec de vídeo livre de royalties e de código aberto que está sendo projetado pela Alliance for Open Media (AOMedia), que é composta por empresas como Google, Mozilla, Microsoft, Amazon, Netflix, AMD, ARM, NVidia, Intel e Cisco, entre outras. A primeira versão 0.1.0 do codec de referência foi publicada em 7 de abril de 2016.

codec history timeline

O nascimento de AV1

No momento do início de 2015, a Google estava trabalhando no VP10, a Xiph (Mozilla) estava trabalhando no Daala e a Cisco havia disponibilizado como software livre seu codec de vídeo livre de royalties chamado Thor.

Então a MPEG LA anunciou pela primeira vez limites anuais para o HEVC (H.265) e taxas oito vezes mais altas que a do H.264, mas logo em seguida eles mudaram as regras novamente:

  • sem limite anual,
  • taxa de conteúdo (0,5% da receita) e
  • taxas por unidade cerca de 10 vezes mais altas que o h264.

O alliance for open media foi criada por empresas fabricantes de hardware (Intel, AMD, ARM, Nvidia, Cisco), fornecedoras de conteúdo (Google, Netflix, Amazon), mantenedoras de navegadores (Google, Mozilla) e outras. As empresas tinham um objetivo comum, um codec de vídeo sem royalties e então nasceu o AV1 com uma licença de patente muito mais simples. Timothy B. Terriberry fez uma apresentação incrível, que é a fonte desta seção, sobre a concepção, modelo de licença e estado atual do AV1.

Você ficará surpreso ao saber que pode analisar o codec AV1 pelo seu navegador, acesse https://arewecompressedyet.com/analyzer/

av1 browser analyzer

PS: Se você quiser saber mais sobre a história dos codecs, é preciso aprender o básico por trás das patentes de compressão de vídeo.

Um codec genérico

Vamos apresentar os principais mecanismos por trás de um codec de vídeo genérico, mas a maioria desses conceitos são úteis e usados em codecs modernos, como VP9, AV1 e HEVC. Certifique-se de entender que vamos simplificar MUITO as coisas. Às vezes, usaremos um exemplo real (principalmente H.264) para demonstrar uma técnica.

Primeiro passo - particionando uma figura

O primeiro passo é dividir o quadro em várias partições, subpartições e além.

picture partitioning

Mas por quê? Existem muitas razões, por exemplo, quando dividimos a imagem, podemos trabalhar com previsões mais precisas, usando partições menores para as partes móveis menores, enquanto usamos partições maiores para um fundo estático.

Geralmente, os CODECs organizam essas partições em fatias (ou azulejos), macroblocos (ou unidades de árvore de codificação) e muitas subpartições. O tamanho máximo dessas partições varia, o HEVC define 64x64 enquanto o AVC usa 16x16, mas as subpartições podem atingir tamanhos de 4x4.

Lembra que aprendemos como são tipos de quadros?! Bem, você pode aplicar essas ideias a blocos também, portanto podemos ter I-Slice, B-Slice, I-Macroblock e etc.

Prática: Verifique as partições

Também podemos usar o Intel Video Pro Analyzer (que é pago, mas há uma versão de teste gratuita que limita você a trabalhar apenas com os primeiros 10 quadros). Aqui estão as partições VP9 analisadas.

VP9 partitions view intel video pro analyzer

Segundo passo - previsões

Uma vez que temos as partições, podemos fazer previsões sobre elas. Para a predição inter, precisamos enviar os vetores de movimento e o residual e para a predição intra, nós enviaremos a direção da predição e o residual também.

Terceiro passo - transformação

Depois de obtermos o bloco residual (partição prevista - partição real), podemos transformá-lo de uma maneira que nos permite saber quais pixels podemos descartar enquanto mantemos a qualidade geral. Existem algumas transformações para esse comportamento exato.

Embora existam outras transformações, vamos olhar mais de perto a transformada discreta de cosseno (DCT). As principais características da DCT são:

  • converte blocos de pixels em blocos do mesmo tamanho de coeficientes de frequência.
  • compacta energia, tornando fácil eliminar a redundância espacial.
  • é reversível, ou seja, você pode voltar para os pixels.

Em 2 de fevereiro de 2017, Cintra, R. J. e Bayer, F. M publicaram seu artigo DCT-like Transform for Image Compression Requires 14 Additions Only.

Não se preocupe se você não entendeu os benefícios de cada ponto, tentaremos fazer alguns experimentos para ver o valor real disso.

Vamos pegar o seguinte bloco de pixels (8x8):

pixel values matrix

Que renderiza a seguinte imagem de bloco (8x8):

pixel values matrix

Quando aplicamos a DCT a este bloco de pixels, obtemos o bloco de coeficientes (8x8):

coefficients values

E se renderizarmos este bloco de coeficientes, obteremos esta imagem:

dct coefficients image

Como você pode ver, não se parece em nada com a imagem original, podemos perceber que o primeiro coeficiente é muito diferente de todos os outros. Este primeiro coeficiente é conhecido como coeficiente DC que representa todas as amostras na matriz de entrada, algo semelhante a uma média.

Este bloco de coeficientes tem uma propriedade interessante que é separar os componentes de alta frequência dos de baixa frequência.

dct frequency coefficients property

Em uma imagem, a maior parte da energia estará concentrada nas frequências mais baixas, então se transformarmos uma imagem em seus componentes de frequência e descartarmos os coeficientes de frequência mais alta, podemos reduzir a quantidade de dados necessários para descrever a imagem sem sacrificar muito a qualidade da imagem.

frequência significa quão rápido um sinal está mudando

Vamos tentar aplicar o conhecimento que adquirimos no teste convertendo a imagem original para sua frequência (bloco de coeficientes) usando a DCT e depois descartando parte dos coeficientes menos importantes.

Primeiro, convertemos para o seu domínio de frequência.

coefficients values

Em seguida, descartamos parte (67%) dos coeficientes, principalmente a parte inferior direita.

zeroed coefficients

Finalmente, reconstruímos a imagem a partir deste bloco de coeficientes descartados (lembre-se, precisa ser reversível) e comparamos com o original.

original vs quantized

Como podemos ver, a imagem resultante se assemelha à imagem original, mas apresenta muitas diferenças em relação à mesma. Jogamos fora 67,1875% e ainda conseguimos algo semelhante ao original. Poderíamos descartar os coeficientes de forma mais inteligente para obter uma melhor qualidade de imagem, mas esse é o próximo tópico.

Cada coeficiente é formado usando todos os pixels

É importante observar que cada coeficiente não mapeia diretamente para um único pixel, mas é uma soma ponderada de todos os pixels. Este gráfico incrível mostra como o primeiro e o segundo coeficiente são calculados, usando pesos únicos para cada índice.

dct calculation

Fonte: https://web.archive.org/web/20150129171151/https://www.iem.thm.de/telekom-labor/zinke/mk/mpeg2beg/whatisit.htm

Você também pode tentar visualizar a DCT observando a formação de uma imagem simples na base da DCT. Por exemplo, aqui está o caractere A sendo formado usando o peso de cada coeficiente.


Prática: jogando com diferentes coeficientes

Você pode experimentar com a transformação DCT.

Quarto passo - quantização

Quando descartamos alguns dos coeficientes, na última etapa (transformação), de alguma forma fizemos uma forma de quantização. Nessa etapa, escolhemos perder informações (a parte perdida) ou, em termos simples, quantizamos os coeficientes para alcançar a compressão.

Como podemos quantizar um bloco de coeficientes? Um método simples seria uma quantização uniforme, em que pegamos um bloco, dividimos por um único valor (10) e arredondamos esse valor.

quantize

Como podemos reverter (re-quantizar) esse bloco de coeficientes? Podemos fazer isso multiplicando o mesmo valor (10) que dividimos primeiro.

re-quantize

Essa abordagem não é a melhor porque não leva em conta a importância de cada coeficiente, poderíamos usar uma matriz de quantizadores em vez de um único valor. Essa matriz pode explorar a propriedade da DCT, quantizando a maior parte do canto inferior direito e menos o canto superior esquerdo, a JPEG usa uma abordagem semelhante, você pode verificar o código-fonte para ver essa matriz.

Prática: quantização

Você pode experimentar quantização.

Quinto passo - Codificando entropia

Depois de quantizarmos os dados (blocos/fatias/quadros de imagem), ainda podemos comprimi-los de forma lossless. Existem muitas maneiras (algoritmos) de comprimir dados. Vamos experimentar brevemente alguns deles, para uma compreensão mais profunda, você pode ler o incrível livro Understanding Compression: Data Compression for Modern Developers.

Código VLC:

Vamos supor que temos uma sequência dos símbolos: a, e, r e t e sua probabilidade (de 0 a 1) é representada por esta tabela.

a e r t
probabilidade 0.3 0.3 0.2 0.2

Podemos atribuir códigos binários exclusivos (preferencialmente pequenos) para os símbolos mais prováveis e códigos maiores para os menos prováveis.

a e r t
probabilidade 0.3 0.3 0.2 0.2
código binário 0 10 110 1110

Vamos comprimir a sequência eat, assumindo que gastaríamos 8 bits para cada símbolo, gastaríamos **24 bits*8 sem qualquer compressão. Mas, se substituirmos cada símbolo por seu código, podemos economizar espaço.

O primeiro passo é codificar o símbolo e que é 10 e o segundo símbolo é a que é adicionado (não de uma maneira matemática) [10][0] e, finalmente, o terceiro símbolo t, que faz com que nosso bitstream comprimido final seja [10][0][1110] ou 1001110, que requer apenas 7 bits (3.4 vezes menos espaço que o original).

Observe que cada código deve ser um código prefixado único Huffman pode ajudá-lo a encontrar esses números. Embora tenha alguns problemas, existem codecs de vídeo que ainda oferecem esse método e é o algoritmo para muitas aplicações que requerem compressão.

Tanto o codificador quanto o decodificador precisam saber a tabela de símbolos com seus códigos, portanto, você também precisa enviar a tabela.

Código Aritmético:

Vamos supor que temos um fluxo de símbolos: a, e, r, s e t e suas probabilidades são representadas por esta tabela.

a e r s t
probabilidade 0.3 0.3 0.15 0.05 0.2

Com essa tabela em mente, podemos construir intervalos contendo todos os símbolos possíveis classificados pelos mais frequentes.

initial arithmetic range

Agora vamos codificar a sequência eat, escolhemos o primeiro símbolo e, que está dentro da subfaixa 0,3 a 0,6 (mas não incluso) e pegamos essa subfaixa e a dividimos novamente usando as mesmas proporções usadas antes, mas dentro dessa nova faixa.

second sub range

Continuando a codificar nossa sequência eat, agora pegamos o segundo símbolo a, que está dentro da nova subfaixa 0,3 a 0,39, e depois pegamos nosso último símbolo t e fazemos o mesmo processo novamente e obtemos a última subfaixa 0,354 a 0,372.

final arithmetic range

Agora precisamos escolher um número dentro da última subfaixa 0,354 a 0,372, vamos escolher 0,36, mas poderíamos escolher qualquer número dentro dessa subfaixa. Com apenas esse número, seremos capazes de recuperar nossa sequência original eat. Se você pensar sobre isso, é como se estivéssemos desenhando uma linha dentro de faixas de faixas para codificar nossa sequência.

final range traverse

O processo reverso (também conhecido como decodificação) é igualmente fácil, com nosso número 0,36 e nossa faixa original, podemos executar o mesmo processo, mas agora usando esse número para revelar o fluxo codificado por trás desse número.

Com a primeira faixa, percebemos que nosso número se encaixa na fatia, portanto, é o nosso primeiro símbolo, agora dividimos essa subfaixa novamente, fazendo o mesmo processo anterior e perceberemos que 0,36 se encaixa no símbolo a e depois de repetirmos o processo, chegamos ao último símbolo t (formando nosso fluxo codificado original eat).

Tanto o codificador quanto o decodificador devem conhecer a tabela de probabilidade de símbolos, portanto, você precisa enviar a tabela.

Bastante legal, não é? As pessoas são muito inteligentes para criar uma solução dessas, alguns codecs de vídeo usam essa técnica (ou pelo menos a oferecem como uma opção).

A ideia é comprimir sem perda o fluxo de bits quantizado, com certeza este artigo está perdendo toneladas de detalhes, razões, compensações e etc. Mas você deve aprender mais como desenvolvedor. Novos codecs estão tentando usar diferentes algoritmos de codificação de entropia como ANS.

Prática: CABAC vs CAVLC

Você pode gerar dois fluxos, um com CABAC e outro com CAVLC e comparar o tempo que cada um leva para ser gerado, bem como o tamanho final.

Sexto passo - formato bitstream

Depois de termos concluído todas essas etapas, precisamos empacotar os quadros comprimidos e o contexto dessas etapas. Precisamos informar explicitamente ao decodificador sobre as decisões tomadas pelo codificador, como profundidade de bits, espaço de cores, resolução, informações de previsões (vetores de movimento, direção de previsão intra), perfil, nível, taxa de quadros, tipo de quadro, número de quadros e muito mais.

Vamos estudar superficialmente o bitstream H.264. Nosso primeiro passo é gerar um bitstream H.264 mínimo *, podemos fazer isso usando nosso próprio repositório e ffmpeg.

./s/ffmpeg -i /files/i/minimal.png -pix_fmt yuv420p /files/v/minimal_yuv420.h264

* Por padrão, o ffmpeg adiciona todos os parâmetros de codificação como um NAL SEI, em breve definiremos o que é um NAL.

Este comando irá gerar um bitstream H.264 bruto com um único quadro, 64x64, com espaço de cor yuv420 e usando a seguinte imagem como quadro.

used frame to generate minimal h264 bitstream

H.264 bitstream

O padrão AVC (H.264) define que as informações serão enviadas em macro quadros (no sentido de rede), chamados de NAL (Camada de Abstração de Rede). O principal objetivo do NAL é fornecer uma representação de vídeo "amigável à rede", este padrão deve funcionar em TVs (baseadas em fluxo), na Internet (baseadas em pacotes) e em outros.

NAL units H.264

Existe um marcador de sincronização para definir os limites das unidades NAL. Cada marcador de sincronização contém o valor 0x00 0x00 0x01, exceto o primeiro, que é 0x00 0x00 0x00 0x01. Se executarmos o hexdump no bitstream h264 gerado, podemos identificar pelo menos três NALs no início do arquivo.

synchronization marker on NAL units

Como já dissemos antes, o decodificador precisa saber não apenas os dados da imagem, mas também os detalhes do vídeo, quadro, cores, parâmetros usados e outros. O primeiro byte de cada NAL define sua categoria e tipo.

id tipo NAL Descrição
0 Indefinido
1 Fatia codificada de uma imagem não-IDR
2 Dados de fatia codificada, partição A
3 Dados de fatia codificada, partição B
4 Dados de fatia codificada, partição C
5 IDR Fatia codificada de uma imagem IDR
6 SEI Informação adicional de melhoria
7 SPS Conjunto de parâmetros de sequência
8 PPS Conjunto de parâmetros de imagem
9 Delimitador de unidade de acesso
10 Fim da sequência
11 Fim do fluxo
... ...

Geralmente, o primeiro NAL de um fluxo de bits é um SPS, esse tipo de NAL é responsável por informar as variáveis gerais de codificação como perfil, nível, resolução e outras.

Se pulamos o primeiro marcador de sincronização, podemos decodificar o primeiro byte para saber qual tipo de NAL é o primeiro.

Por exemplo, o primeiro byte após o marcador de sincronização é 01100111, onde o primeiro bit (0) é para o campo forbidden_zero_bit, os próximos 2 bits (11) nos dizem o campo nal_ref_idc que indica se este NAL é um campo de referência ou não e os próximos 5 bits (00111) nos informam o campo nal_unit_type, neste caso, é um NAL de SPS (7).

O segundo byte (binário=01100100, hex=0x64, dec=100) de um SPS NAL é o campo profile_idc que mostra o perfil que o codificador usou, neste caso, usamos o perfil alto. O terceiro byte também contém várias flags que determinam o perfil exato (como restrito ou progressivo). Mas, no nosso caso, o terceiro byte é 0x00 e, portanto, o codificador usou apenas o perfil alto.

SPS binary view

Quando lemos a especificação do bitstream H.264 para um SPS NAL, encontramos muitos valores para o nome do parâmetro, categoria e uma descrição, por exemplo, vamos olhar para os campos pic_width_in_mbs_minus_1 e pic_height_in_map_units_minus_1.

Nome do parâmetro Categoria Descrição w
pic_width_in_mbs_minus_1 0 ue(v)
pic_height_in_map_units_minus_1 0 ue(v)

ue(v): Inteiro sem sinal (unsigned integer) Exp-Golomb-coded

Se fizermos algumas contas com o valor desses campos, acabaremos com a **resolução*8. Podemos representar um 1920 x 1080 usando um pic_width_in_mbs_minus_1 com o valor de 119 ( (119 + 1) * macroblock_size = 120 * 16 = 1920), economizando espaço, em vez de codificar 1920, fizemos isso com 119.

Se continuarmos a examinar nosso vídeo criado com uma visualização binária (ex: xxd -b -c 11 v/minimal_yuv420.h264), podemos pular para o último NAL que é o quadro em si.

h264 idr slice header

Podemos ver seus primeiros 6 bytes: 01100101 10001000 10000100 00000000 00100001 11111111. Como já sabemos, o primeiro byte nos informa sobre o tipo de NAL que é, neste caso, (00101) é um IDR Slice (5) e podemos inspecioná-lo ainda mais:

h264 slice header spec

Usando as informações da especificação, podemos decodificar qual tipo de slice (slice_type), o número do quadro (frame_num) entre outros campos importantes.

Para obter os valores de alguns campos (ue(v), me(v), se(v) ou te(v)), precisamos decodificá-los usando um decodificador especial chamado Exponential-Golomb, este método é muito eficiente para codificar valores variáveis, principalmente quando há muitos valores padrão.

Os valores de slice_type e frame_num deste vídeo são 7 (I slice) e 0 (o primeiro quadro).

Podemos ver o bitstream como um protocolo, e se você quiser ou precisar aprender mais sobre esse bitstream, consulte a especificação ITU H.264. Aqui está um diagrama macro que mostra onde os dados da imagem (YUV comprimido) residem.

h264 bitstream macro diagram

Podemos explorar outros bitstreams como o VP9 bitstream, H.265 (HEVC) ou até mesmo nosso novo melhor amigo AV1 bitstream, eles parecem semelhantes? Não, mas uma vez que você aprende um, pode facilmente aprender os outros.

Prática: Inspecionar o H.264 bitstream

Podemos gerar um vídeo de um único quadro e usar o mediainfo para inspecionar seu bitstream H.264. Na verdade, você pode até ver o código fonte que analisa o bitstream h264 (AVC).

mediainfo details h264 bitstream

Também podemos usar o Intel Video Pro Analyzer, que é pago, mas há uma versão de teste gratuita que limita o uso apenas aos primeiros 10 quadros, mas isso é ok para fins de aprendizado.

intel video pro analyzer details h264 bitstream

Revisão

Vamos notar que muitos dos codecs modernos usam o mesmo modelo que aprendemos. Na verdade, vamos olhar o diagrama de blocos do codec de vídeo Thor, ele contém todos os passos que estudamos. A ideia é que agora você deve ser capaz de entender melhor as inovações e os artigos para a área.

thor_codec_block_diagram

Anteriormente, calculamos que precisávamos de 139 GB de armazenamento para manter um arquivo de vídeo com uma hora de duração em resolução 720p e 30fps se usarmos as técnicas que aprendemos aqui, como previsão inter e intra, transformação, quantização, codificação de entropia e outras podemos alcançar, assumindo que estamos gastando 0,031 bit por pixel, o mesmo vídeo de qualidade perceptível requer apenas 367,82 MB em vez de 139 GB de armazenamento.

Escolhemos usar 0,031 bit por pixel com base no exemplo de vídeo fornecido aqui.

Como H.265 consegue uma melhor razão de compressão comparado com H.264?

Agora que sabemos mais sobre como os codecs funcionam, fica fácil entender como os novos codecs são capazes de entregar resoluções mais altas com menos bits.

Vamos comparar AVC e HEVC, lembrando que quase sempre é um equilíbrio entre mais ciclos de CPU (complexidade) e taxa de compressão.

HEVC tem opções de partições (e sub-partições) maiores e mais numerosas do que AVC, mais direções/ângulos de predições intra, codificação de entropia melhorada e muito mais, todas essas melhorias tornaram o H.265 capaz de comprimir 50% mais do que o H.264.

h264 vs h265

Transmissão online

Arquitetura geral

general architecture

[TODO]

Download progressivo e transmissão adaptativa

progressive download

adaptive streaming

[TODO]

Proteção de conteúdo

Podemos utilizar um sistema simples de token para proteger o conteúdo. O usuário sem um token tenta solicitar um vídeo e o CDN o impede, enquanto um usuário com um token válido pode reproduzir o conteúdo. Isso funciona de maneira bastante semelhante à maioria dos sistemas de autenticação da web.

token protection

O uso exclusivo deste sistema de tokens ainda permite que um usuário baixe um vídeo e o distribua. Então, os sistemas de gerenciamento de direitos digitais (DRM) podem ser usados para tentar evitar isso.

drm

Na vida real, em sistemas de produção, as pessoas frequentemente usam ambas as técnicas para fornecer autorização e autenticação.

DRM

Principais sistemas

O que?

DRM significa gerenciamento de direitos digitais, é uma forma de fornecer proteção de direitos autorais para mídia digital, como vídeo e áudio digital. Embora seja usado em muitos lugares, não é universalmente aceito.

Por quê?

Os criadores de conteúdo (principalmente estúdios) querem proteger sua propriedade intelectual contra cópias para evitar redistribuição não autorizada de mídia digital.

Como?

Vamos descrever de forma abstrata e genérica uma forma simplificada de DRM.

Dado um conteúdo C1 (ou seja, um streaming de vídeo hls ou dash), com um reprodutor P1 (como shaka-clappr, exo-player ou ios) em um dispositivo D1 (como um smartphone, TV, tablet ou desktop/notebook) usando um sistema DRM1 (widevine, playready ou FairPlay).

O conteúdo C1 é criptografado com uma chave simétrica K1 do sistema DRM1, gerando o conteúdo criptografado C'1.

drm general flow

O reprodutor P1, de um dispositivo D1, tem duas chaves (assimétricas), uma chave privada PRK1 (essa chave é protegida1 e conhecida apenas por D1) e uma chave pública PUK1.

1protegida: essa proteção pode ser via hardware, por exemplo, essa chave pode ser armazenada dentro de um chip especial (somente leitura) que funciona como uma caixa preta para fornecer a decodificação, ou por software (menos seguro), o sistema DRM fornece meios para saber qual tipo de proteção um determinado dispositivo possui.

Quando o reprodutor P1 deseja reproduzir o conteúdo C'1, ele precisa lidar com o sistema DRM1, fornecendo sua chave pública PUK1. O sistema DRM1 retorna a chave K1 criptografada com a chave pública do cliente PUK1. Em teoria, essa resposta é algo que apenas D1 é capaz de decifrar.

K1P1D1 = enc(K1, PUK1)

P1 usa seu sistema local de DRM pode ser um SOC, um hardware ou software especializado, este sistema é capaz de decifrar o conteúdo usando sua chave privada PRK1, ele pode decifrar a chave simétrica K1 do K1P1D1 e reproduzir C'1. No melhor dos casos, as chaves não são expostas através da RAM.

K1 = dec(K1P1D1, PRK1)

P1.play(dec(C'1, K1))

drm decoder flow

Como user o Jupyter

Certifique-se de ter o docker instalado e execute ./s/start_jupyter.sh e siga as instruções no terminal.

Conferências

Referências

O conteúdo mais rico está aqui, é onde todas as informações que vimos neste texto foram extraídas, baseadas ou inspiradas. Você pode aprofundar seu conhecimento com esses incríveis links, livros, vídeos, etc.

Cursos Onlines e Tutoriais:

Livros:

Em material de integração:

Especificações de fluxo de bits:

Programas:

Codecs não-ITU:

Conceitos de Codificação:

Sequências de vídeo para testes:

Diversos: