5.11. Diferenciação de fotogramas¶
A diferenciação de fotogramas compara cada novo fotograma com um fotograma de referência armazenado, para encontrar as partes da cena que sofreram alterações. É o elemento central das aplicações de câmara que vigiam a ocorrência de algo – captura ativada por movimento, alertas de intrusão, «guardar 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 pesquisa de região, executadas em cada fotograma.
5.11.1. O pipeline básico¶
A primeira fase consiste em adquirir uma referência. Em algum momento perto do arranque – idealmente quando a cena se encontra no estado que representa «sem alteração» – a aplicação captura um fotograma e guarda-o. Esse fotograma torna-se a linha de base com a qual todas as capturas subsequentes serão comparadas.
reference = csi0.snapshot().copy()
O .copy() é importante. csi0.snapshot() por si só devolve uma Image cujo buffer vive no buffer de fotograma, onde a próxima chamada a snapshot o irá sobrescrever. O .copy() aloca um buffer separado para a referência e permite que os pixels deste fotograma sobrevivam para além da próxima captura.
A segunda fase é executada em cada fotograma: captura uma imagem nova e depois calcula 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 onde a cena mudou desde que a referência foi tirada, com a magnitude de cada pixel proporcional à quantidade de alteração nessa posição.
A terceira fase aplica um limiar à imagem de diferença. A diferença bruta contém sempre algum ruído: pequenas variações de brilho devidas ao ruído de disparo do sensor, alterações de gradiente por deriva de iluminação, instabilidade sub-pixel por ligeiro movimento da câmara. Uma passagem de limiarização – binary() com um limiar definido acima desse piso de ruído – mantém apenas as alterações suficientemente grandes para contar como movimento real e descarta as restantes, produzindo uma imagem binária cujos pixels não nulos são as posições efetivamente alteradas.
A quarta fase extrai regiões conectadas dessa máscara binária – grupos de pixels não nulos adjacentes que formam zonas contíguas. find_blobs() faz isso numa única chamada, devolvendo uma lista de regiões de movimento, cada uma com uma caixa delimitadora e uma contagem de pixels, sobre as quais o restante da aplicação pode atuar.
O pipeline de diferenciação de fotogramas: um fotograma de referência mais um fotograma atual produzem uma imagem de diferença; a limiarização transforma a diferença numa máscara binária das posições alteradas; uma etapa de regiões conectadas transforma a máscara numa lista de regiões de movimento.¶
5.11.2. Referências em memória e em disco¶
O pipeline básico mantém o fotograma de referência em RAM. Esta é a resposta correta quando a referência é capturada nesta execução do script e apenas precisa de sobreviver enquanto o script estiver a correr.
Para uma aplicação de longa duração – uma câmara que deve retomar a deteção de alterações após um ciclo de alimentação, um script intermitente que precisa de detetar qualquer alteração desde um momento anterior – o fotograma de referência tem de sobreviver ao script em execução. O padrão é guardar 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 o local onde a referência vive entre capturas é que muda. Alguns refinamentos estendem naturalmente esta variante em disco – recaptura automática da referência por temporizador, médias deslizantes opcionais para acompanhar a deriva lenta da 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 num contexto ligeiramente diferente: isolar uma fonte de luz do resto da cena. O truque é capturar uma referência «luzes apagadas» – um fotograma tirado quando o que está a ser detetado (um farol IR, um pixel de ecrã, um indicador de estado) não está iluminado – e subtrair essa referência de cada fotograma subsequente. O resultado tem brilho zero em todo o lado onde a cena era igual em ambas as capturas, e brilho não nulo apenas onde a fonte de luz realmente se acendeu.
5.11.4. Escolher difference ou sub¶
Uma nota prática sobre qual operação aritmética escolher. difference() devolve o valor absoluto da alteração – sem sinal – o que a torna sensível a alterações em qualquer direção (aumento ou diminuição de brilho) ao custo de não indicar à aplicação em que direção a alteração ocorreu. Para deteção pura de movimento essa é a resposta certa: tudo o que se moveu é interessante, independentemente do sentido em que o brilho variou.
Para a deteção de fontes de luz, o pixel iluminado é sempre mais brilhante do que a referência com as luzes apagadas, pelo que sub() (com o seu recorte em zero) é a escolha mais correta. Onde quer que o fotograma atual seja mais escuro do que a referência (o que seria ruído do sensor em torno do valor não iluminado), o resultado é recortado para zero em vez de reportar um sinal espúrio de «a luz estava acesa».