5.11. Diferenciação de quadros

A diferenciação de quadros compara cada novo quadro com um quadro de referência armazenado para encontrar as partes da cena que mudaram. Ela é o carro-chefe das aplicações de câmera que vigiam algo acontecendo – captura disparada por movimento, alertas de intrusão, “salvar um vídeo quando algo se move” – e é construída inteiramente a partir das operações pixel a pixel abordadas anteriormente: uma diferença absoluta, um limiar e uma busca por região, executados em cada quadro.

5.11.1. O pipeline básico

O primeiro estágio é adquirir uma referência. Em algum momento próximo à inicialização – idealmente quando a cena está no estado que “nenhuma mudança” significa – a aplicação captura um quadro e o guarda. O quadro se torna a linha de base com a qual toda captura subsequente será comparada.

reference = csi0.snapshot().copy()

O .copy() importa. csi0.snapshot() por si só retorna uma Image cujo buffer fica no frame buffer, onde a próxima chamada a snapshot o sobrescreverá. .copy() aloca um buffer separado para a referência e permite que os pixels deste quadro sobrevivam à próxima captura.

O segundo estágio é executado em cada quadro: capturar uma imagem nova e, em seguida, calcular a diferença absoluta entre ela e a referência. É exatamente o que difference() faz:

current = csi0.snapshot()
current.difference(reference)

Após esta chamada, current contém uma imagem cujos pixels não nulos marcam todas as posições em que a cena mudou desde que a referência foi tomada, com a magnitude de cada pixel proporcional a quanto ela mudou naquela posição.

O terceiro estágio aplica um limiar à imagem de diferença. A diferença bruta sempre contém algum ruído: pequenas variações de brilho devido ao ruído de disparo do sensor, mudanças de gradiente devido à deriva de iluminação, jitter de sub-pixel devido a leve movimento da câmera. Uma passagem de limiar – binary() com um limiar definido acima desse piso de ruído – mantém apenas as mudanças grandes o suficiente para contar como movimento real e descarta o resto, produzindo uma imagem binária cujos pixels não nulos são as posições que realmente mudaram.

O quarto estágio extrai regiões conectadas dessa máscara binária – grupos de pixels não nulos adjacentes que formam manchas contíguas. find_blobs() faz isso em uma única chamada, retornando uma lista de regiões de movimento, cada uma com uma caixa delimitadora e uma contagem de pixels, sobre a qual o restante da aplicação pode agir.

Um diagrama de pipeline horizontal. Os dois painéis mais à esquerda são um quadro de referência e um quadro atual lado a lado, com um sinal de mais entre eles. Uma seta leva do par a um terceiro painel rotulado difference, no qual algumas manchas estão brilhantes contra um fundo escuro. Uma seta leva de lá a um quarto painel mostrando uma versão binária limiarizada da diferença, com as mesmas manchas agora brancas e sólidas. Uma seta final leva a um quinto painel mostrando a máscara binária anotada com caixas delimitadoras retangulares desenhadas ao redor de cada mancha.

O pipeline de diferenciação de quadros: um quadro de referência mais um quadro atual tornam-se uma imagem de diferença; a limiarização transforma a diferença em uma máscara binária de posições alteradas; uma etapa de região conectada transforma a máscara em uma lista de regiões de movimento.

5.11.2. Referências em memória e em disco

O pipeline básico mantém o quadro de referência na RAM. Essa é a resposta correta quando a referência é capturada nesta execução do script e só precisa sobreviver enquanto o script continuar em execução.

Para uma aplicação de longa duração – uma câmera que deve retomar a detecção de mudanças após um ciclo de energia, um script intermitente que precisa detectar qualquer mudança desde algum momento anterior – o quadro de referência tem que sobreviver ao script em execução. O padrão é salvar a referência em disco:

csi0.snapshot().save("/sdcard/reference.bmp")

e carregá-la de volta no início de cada execução:

reference = image.Image("/sdcard/reference.bmp")

A lógica de diferenciação não muda; apenas onde a referência reside entre as capturas. Alguns refinamentos estendem naturalmente esta variante em disco – recaptura automática da referência por um timer, médias móveis opcionais para acompanhar a deriva lenta de iluminação – mas a substituição no centro é a mesma.

5.11.3. Isolamento de fonte de luz

O mesmo padrão de subtração aparece em um cenário ligeiramente diferente: isolar uma fonte de luz do restante da cena. O truque é capturar uma referência “com as luzes apagadas” – um quadro tomado quando o que está sendo detectado (um farol IR, um pixel de tela, um indicador de status) não está iluminado – e subtrair essa referência de cada quadro subsequente. O resultado tem brilho zero em todos os lugares onde a cena era a mesma em ambas as capturas, e brilho não nulo apenas onde a fonte de luz realmente acendeu.

5.11.4. Escolhendo difference ou sub

Uma nota prática sobre qual operação aritmética escolher. difference() retorna o valor absoluto da mudança – sem sinal – o que a torna sensível à mudança em qualquer direção (clareamento ou escurecimento) ao custo de não dizer à aplicação em qual direção a mudança ocorreu. Para detecção pura de movimento, essa é a resposta certa: qualquer coisa que se moveu é interessante, independentemente de para que lado o brilho variou.

Para detecção de fonte de luz, o pixel iluminado é sempre mais brilhante do que a referência com as luzes apagadas, então sub() (com seu corte em zero) é a escolha mais honesta. Em qualquer lugar onde o quadro atual seja mais escuro que a referência (o que seria ruído do sensor em torno do valor não iluminado), o resultado é cortado em zero em vez de relatar um sinal espúrio de “a luz estava acesa”.