0.2.0: o que a janelinha da câmera faz com a detecção de slides
Publicamos hoje no npm a versão 0.2.0 do video-slide-extractor. O pacote é um detector que compara a imagem por blocos: ele amostra o vídeo em intervalos fixos e decide, quadro a quadro, se o conteúdo na tela passou para o slide seguinte. É o mesmo detector que o Video2Any executa dentro do seu navegador. O código é aberto, não tem dependências, usa licença MIT e roda tanto no navegador quanto no Node.
A 0.1.x que publicamos em julho era uma cópia do código de produto tirada em 16 de julho. Ela acerta numa gravação de tela limpa, mas erra em boa parte das gravações que as pessoas realmente têm. Existem duas situações que a fazem falhar; o produto resolveu as duas ao longo do mês seguinte e o pacote não as conhecia. A 0.2.0 incorpora essas duas.
A seguir explicamos três coisas, nesta ordem: como eram de fato os vídeos que as pessoas enviaram, que conclusão o banco de testes já dava antes de mexermos em qualquer coisa, e o que medimos depois da mudança.
Nesta página
Como eram de fato os vídeos que as pessoas enviaram
O Video2Any registra eventos de extração desde 16 de julho. Até hoje, 96 pessoas iniciaram 411 conversões; 354 chegaram ao fim e geraram 291 arquivos: 188 PowerPoint, 95 PDFs e 8 pacotes de imagens. Quase tudo é arquivo local: em 398 das 411 conversões, o usuário escolheu um vídeo que já estava na mesma máquina onde a aba estava aberta. (Os dois dias em que rodamos nossos testes automatizados ficam de fora de todos os números deste artigo.)
Nas 106 conversões cujo arquivo informava a própria duração, o vídeo tinha em média 53 minutos. Não são demos de produto de cinco minutos: são aulas, reuniões e gravações de curso. O detector de julho nunca tinha sido medido com esse tipo de vídeo.
O que merece mais atenção é quantos slides saem por conversão. As conversões concluídas deram 64 em média, um número que parece normal e que esconde a distribuição: 64 conversões passaram de 100 slides, sete passaram de 300 e quatro devolveram exatamente 900. 900 é o número máximo de amostras que a varredura pode tirar, ou seja, nessas quatro cada amostra foi tratada como um slide novo e o detector não descartou nenhuma. Um desses vídeos tinha 30 minutos.
O ritmo de saída também não é um valor fixo: no mesmo ajuste, os vídeos com menos de uma hora deram cerca de 6.5 slides por minuto e os com mais de uma hora cerca de 1.0. Parte dessa diferença é real, porque uma reunião de duas horas troca de tela menos vezes por minuto do que uma demo curta. O problema é que o número sozinho não diz qual dos dois lados está certo. As quatro conversões que bateram no teto dizem: se todas as amostras são mantidas, o detector não está decidindo nada.
O banco de testes já dava essa conclusão
Antes de corrigir qualquer coisa, a 0.2.0 acrescentou ao pacote o diretório bench/. Os vídeos de teste são gerados a partir de uma sequência de slides conhecida, então o segundo exato em que cada troca acontece fica definido na renderização e ninguém precisa rotular à mão. Cada apresentação é renderizada de três formas: uma versão limpa, uma comprimida a crf 45 para ter ruído de compressão e uma com a janelinha da câmera animada num canto.
Na apresentação do MIT (46 slides, uma amostra a cada 2 segundos em 160×90), a versão limpa obteve F1 de 0.966. Com a câmera sobreposta, o F1 cai para 0.538: o detector tirou 125 capturas de um conteúdo de 46 slides, a precisão ficou em 0.368 e 63% do que ele devolveu era um slide que já tinha capturado antes.
O motivo é aritmético, não é azar. A condição padrão é mudar mais de 2% dos blocos. Uma janelinha de câmera, um cursor ou um logo animado em loop ocupam sozinhos mais de 2% dos blocos enquanto continuam se mexendo. Assim, todo quadro amostrado passa do limiar mesmo que nenhum slide tenha mudado. No fim, quantos slides você recebe depende de quem falava ter ligado a câmera, e não de quantas páginas a apresentação tem.
Publicamos essa linha do jeito que ela é. As quatro conversões de 900 slides são essa mesma linha vista em material real, e corrigi-la é o que a 0.2.0 faz.
O que a 0.2.0 acrescenta
Três funções novas exportadas, uma opção e as declarações de tipo de tudo isso.
buildActivityMask
Esta função recebe os quadros amostrados, localiza os blocos que mudam em quase todo par de quadros consecutivos — a janelinha, o cursor, o relógio do canto — e devolve uma máscara que os exclui inteiramente da comparação. Se a máscara fosse cobrir a maior parte do quadro, a função devolve
nullem vez de máscara. Esse caminho importa: quando quase toda a imagem se mexe, o que se mexe é justamente o conteúdo que precisa ser olhado, e excluí-lo equivale a desistir de detectar.chooseThreshold
Um mesmo limiar fixo não serve para uma apresentação e para um close de alguém falando. A apresentação fica parada, então uma mudança real se distingue com clareza do ruído. A imagem de câmera se mexe o tempo todo e ali 0.02 transforma cada quadro num slide novo. Esta função analisa a distribuição da mudança entre quadros e devolve
{ changedRatio, mode }. O campomodeestá exposto de propósito: valebimodal,static,motionoudefaulte indica que tipo de material o detector acha que está vendo, de modo que, se o resultado for ruim, você consegue ver em que ele se baseou em vez de supor.analyzeSamples
Esta função executa os dois passos acima sobre os mesmos quadros e devolve
{ mask, choice }. É disso que a maioria de quem chama precisa: você tem amostras e quer saber com quais parâmetros detectar, sem precisar descobrir antes que "com quais parâmetros" são na verdade duas decisões independentes.frameDiff({ collect: true })
Com esta opção,
frameDiffdevolve tambémflags: um byte por bloco indicando em que áreas aquele quadro mudou. Serve para desenhar as áreas que o detector considerou alteradas. Só é devolvido se você passarcollect, porque um campo que normalmente não existe é mais fácil de usar corretamente do que um campo que quase sempre vale null.
Na ordem em que você chamaria, isto é tudo:
import { analyzeSamples, createSlideDetector } from 'video-slide-extractor';
// frames: amostras RGBA, uma a cada ~2 s, reduzidas para 160x90
const { mask, choice } = analyzeSamples(frames, 160, 90);
console.log(choice.mode); // 'bimodal' | 'static' | 'motion' | 'default'
const detect = createSlideDetector(160, 90, {
mask,
changedRatio: choice.changedRatio
});
const kept = [];
frames.forEach((frame, i) => {
if (detect(frame).keep) kept.push(i); // amostras a capturar em resolução cheia
});Quanto melhora, medido
Mesmos vídeos de teste e mesmo protocolo: apresentação do MIT, 46 slides, versão com câmera, uma amostra de 160×90 a cada 2 s, e consideramos uma detecção correta se ela cair a menos de ±2.5 s da troca rotulada.
| Detecção | Capturas | Precisão | Revocação | F1 | Duplicados |
|---|---|---|---|---|---|
| Padrões da 0.1.x (changedRatio 0.02) | 125 | 0.368 | 1.000 | 0.538 | 0.632 |
| Com máscara de atividade | 54 | 0.815 | 0.957 | 0.880 | 0.185 |
| Com máscara e limiar automático | 35 | 1.000 | 0.761 | 0.864 | 0.000 |
Só de acrescentar a máscara, as capturas caem de 125 para 54, os duplicados de 63% para 19% e o F1 sobe de 0.538 para 0.880. Além disso ela não afeta o material que não precisa dela: nas versões limpa e com ruído ela devolve null, então essas duas linhas dão exatamente o mesmo resultado da 0.1.1.
O limiar, por outro lado, tem vantagens e desvantagens, e para explicar isso convém começar pela linha que sai mal: na apresentação limpa do MIT, o limiar calculado é mais conservador que a constante antiga e a revocação cai de 0.935 para 0.761. É por isso que DEFAULTS não mudou nesta versão. analyzeSamples serve para quando você não sabe que tipo de material recebeu. Se você sabe, as constantes continuam lá e quem decide é você.
Medimos as duas linhas novas com a ferramenta do banco de testes, no commit da publicação. O RESULTS.md publicado ainda reporta apenas o conjunto de métodos da 0.1.x; incluir a máscara ali como método próprio é a próxima coisa a fazer em bench/.
O que deixamos de fora de propósito
A unificação global de duplicados e a suavização de transições não estão no pacote e não vamos acrescentar. As duas exigem ver o vídeo inteiro de uma vez: qual tomada voltou depois, ou se três capturas vieram na verdade de uma mesma dissolvência. Isso é trabalho do pipeline de processamento. Um detector que só responde se a imagem mudou não deveria além disso guardar o vídeo inteiro.
A densidade de slides também não entra aqui: quantos slides uma aula de 90 minutos deveria render é uma decisão de produto, não de detecção. O Video2Any tem o critério dele e oferece um controle para ajustar. Uma biblioteca não precisa tomar essa decisão por você; basta dizer onde a imagem mudou.
Três leituras relacionadas:
- Slides demais de um vídeo só — como o Video2Any trata a densidade de slides
- Decodificar vídeo em paralelo, numa aba — o pipeline completo em que este detector está montado
- Gravação do Zoom para PowerPoint — a janelinha da câmera aparece principalmente nesse tipo de gravação
O que vem a seguir
- Um tempo mínimo de permanência. O conteúdo novo vai precisar ficar parado por alguns segundos antes de contar como slide. É isso que acaba de vez com as conversões de 900: a máscara tira a janelinha, mas uma dissolvência rápida ou um arrasto na barra de tempo continuam disparando a condição. No Video2Any já funciona. Chega ao pacote quando a interface estiver resolvida, porque um tempo de permanência não significa nada sem uma cadência de amostragem, e o pacote não sabe de propósito de quanto em quanto tempo você amostrou.
- Trocar o material sintético por uma câmera real. O material de teste atual é uma animação renderizada num canto. Uma webcam gravando uma pessoa real é um teste mais difícil e mais parecido com o que acontece, e é nele que a máscara deveria ser medida.
- A falha que nenhum dos dois métodos resolve. Quando dois slides seguidos diferem apenas por uma linha de tópico a mais, a 160×90 essa diferença fica colada no mínimo que dispara a condição, então os slides construídos em etapas se perdem inteiros. O banco de testes mostra isso até numa renderização sintética. É um problema de resolução e de critério de pontuação, e o
docs/evaluation.mdtransforma agora a forma de contar essas construções num parâmetro que precisa ser declarado e reportado, em vez de ficar a critério de cada avaliador. - Onde a calibração deveria acontecer. O Video2Any divide o vídeo entre quatro workers e cada um calibra o próprio limiar na própria fatia, de modo que o mesmo arquivo gera apresentações um pouco diferentes em máquinas com números de núcleo diferentes. Não é uma propriedade desejável. A solução óbvia seria calibrar uma vez só, globalmente. Nós construímos e medimos: numa reunião de 29 minutos em que o ffmpeg conta sete cortes reais, a calibração por fatia achou seis e a global achou apenas quatro. Calibrar globalmente não é necessariamente melhor, então o pacote não vai adotar isso como padrão enquanto não existir uma versão que seja.
Experimente
A instalação é um comando só, não tem dependências e roda em qualquer gravação que você tenha por aí:
npm i video-slide-extractorSe você construir algo com ele, ou tiver material que o faz falhar, mande para o repositório. Um vídeo que deixa o detector sem resposta vale mais para nós do que uma estrela: foi assim que o material de teste com câmera acabou existindo.