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.
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”.