Site menu Brincando com câmeras de vigilância, parte 2

Brincando com câmeras de vigilância, parte 2

(Se você precisa de mais contexto, dê uma olhada na Parte 1.)

Gerar compactos

É humanamente impossível analisar gravação 24x7 de várias câmeras. Por outro lado, se ninguém conferir, é inútil. Mas a tecnologia pode dar uma ajuda, gerando "compactos" com as passagens relevantes.

Tenho experimentado com o DVR-Scan com resultados bastante bons para um software que não faz uso nem de ML nem de GPU.

Segue um exemplo de script "scanner". Você precisaria familiarizar-se com os parâmetros do DVR-Scan e alterar o script para sua necessidade, mas o embrião da ideia está ali.

Como disse antes, processamento de vídeo exige muita CPU, e esse uso é linearmente proporcional a resolução e FPS. Nem pense em rodar DVR-Scan num Raspberry, a não ser que você goste de rasgar dinheiro e vá usar um por câmera. Um PC velho pode dar conta de um grupo de câmeras, se a) a geração for judiciosa na resolução e no FPS, e b) o DVR-Scan for corretamente configurado.

Uso um mini-PC com Celeron J3060 dual core, desde novo foi um PC com performance sofrível, e mesmo ele consegue produzir 80-90 FPS com vídeos HD (1MP), desde que use o parâmetro -df 3 (que reduz a resolução de análise por um fator de 3). Deve ser suficiente para até 8 ou 9 câmeras.

O fato de não querer usar um PC grande, caro e barulhento só para analisar vídeo me levou a reduzir a qualidade dos streams para 1MP e 5 FPS. Se fosse só pelo bitrate, poderia ser 2MP e 10 FPS, mas no longo prazo a capacidade de fazer pós-processamento é o que importa. Para dar aquela espiadinha no movimento dos vizinhos, 1MP ou 2MP são igualmente bons.

Se você quiser ir para uma abordagem completamente oposta, comprando um PC grande e forte, usando alta resolução, alta taxa de FPS, etc. gaste um pouco de tempo em aprender como funciona a interface do DVR-Scan com o CUDA da NVidia. O uso de GPU pode multiplicar a performance por 10 ou mais.

O ajuste de sensibilidade é algo que vai lhe dar muito prazer, ou desprazer, por exigir testes e experimentos. É provável que diferentes cenas e mesmo diferentes horas do dia peçam sensibilidades diferentes. Sugiro começar pelo padrão (0.15) que é bem sensível, analisar os resultados, e ir subindo aos poucos para diminuir a taxa de falsos positivos. Em meus testes, acabei usando valores entre 0.2 e 0.4 (cada câmera tem um ponto ótimo diferente).

A forma mais cômoda de rodar o DVR-Scan é no modo -m opencv -o arquivo.avi e gravar todos os eventos num vídeo único. Esse modo tem alguns problemas: a) formato é obrigatoriamente .AVI (que não pode ser reproduzido diretamente por um browser), b) o fato do OpenCV transcodificar o vídeo para XviD (muito rápido, mas também não suportado por nenhum browser), e c) um bitrate potencialmente maior que o material original, o que desfaz parcialmente o esforço em controlar o bitrate das câmeras.

Num primeiro momento eu pegava esses compactos .AVI gerados pelo DVR-Scan e fazia transcoding para .MP4 em outro computador. Uma vez que o material sofria duas transcodificações, de vez em quando os vídeos apresentavam defeitos "misteriosos".

A solução definitiva foi usar o modo -m copy, que simplesmente copia o bitstream original, porém gera um mini-vídeo (.MP4) para cada evento. Depois podemos concatenar os eventos num vídeo mais comprido (e agradável de assistir) usando ffmpeg -c copy. Desse jeito, o resultado final é quase perfeito: mesmo bitrate do material original, pode ser aberto diretamente num browser, e evita o custo de CPU (e os eventuais artefatos) do transcoding.

Um grande defeito do modo -m copy é a "precisão" do recorte, que tem de começar num I-frame. Ao juntar os eventos num compacto, o relógio da câmera parece "andar para trás" no caso de eventos muito próximos no tempo. Mas há outro problema mais insidioso.

Por exemplo, o recorte de um evento no instante 0:45 pode ficar entre 0:42 e 0.45, para uma taxa de I-frame a cada 3s. (Da forma que é implementado, o recorte pode começar adiantado, mas nunca começa atrasado.) Mesmo com o parâmetro -tb 0, o recorte ficará, em média, 1.5s adiantado. Mas o DVR-Scan não contabiliza esse erro, então o recorte também terminará, em média, 1.5s antes do ideal. Dependendo do valor de -tp, o recorte pode simplesmente não conter o evento de interesse, principalmente se for de curta duração.

A solução é fazer -tp igual à taxa de I-frames (no meu caso, 3s) para garantir que, mesmo no pior caso, o recorte ainda contenha o evento. É claro que isso tem a desvantagem de incluir 3s a mais de vídeo inútil por evento. Uma possível mitigação é aumentar a taxa de I-frames, o que diminui a janela de erro, ao custo de um bitrate maior. Mas, se a janela de erro tem de ser rigorosamente zero, aí o jeito é usar o modo -m ffmpeg e encarar o custo do transcoding.

Mesmo com cuidadoso ajuste da sensibilidade, o grosso dos vídeos compactos será formado por falsos positivos, chatíssimos de assistir. Você vai acabar usando reprodução 2x ou 4x o tempo todo. O ffmpeg permite embutir essa aceleração no próprio vídeo, alterando a taxa de FPS sem descartar frames. Por exemplo, um vídeo de 5 FPS pode ser revisado em 20 FPS. Se alguma passagem merecer revisão, reproduza a 0.5x ou 0.25x para restaurar à velocidade original, sem perda de qualidade nem de frames.

Assistir compactos

Na minha instalação, os compactos eram publicados simplesmente jogando-os numa pasta de um site Web com auto-índice. Hoje em dia, são jogados num bucket S3 publicado como site Web estático. Uma vez que os compactos são arquivos .MP4, o browser é capaz de reproduzi-los por si mesmo. Não é preciso nem MediaMTX, nem página com elemento de mídia, nem nenhuma preparação do lado servidor.

É um esquema tosco, porém suficiente. Poderíamos publicar os compactos usando HLS, mas seriam ainda os mesmos vídeos chatos, que consumiriam a mesma banda, então não estou vendo razões para mudar.

Armazenamento de massa em servidores na nuvem é muito caro, então as opções economicamente viáveis são a) usar um servidor de borda (ou DVR) e publicar seu conteúdo na Internet usando NAT reverso ou proxy reverso, ou b) ir de "serverless" usando S3 da Amazon, que é muito barato, ainda que cobre tanto pelo armazenamento quanto pelo tráfego.

Outra opção, gratuita porém altamente "tabajara" seria fazer o upload dos compactos para o YouTube. Há diversos utilitários de linha de comando para fazer isso, o YouTube tem uma API para isso. Será que "A Grande Nuvem" poderia nos dar algo de graça, pelo menos desta vez?

O YouTube reserva-se o direito de remover vídeos (e até contas) se detectar uso abusivo do serviço. Não encontrei nenhuma afirmação categórica que fazer upload de vídeos de vigilância seria considerado abuso — mas para mim é um exemplo didático de abuso, então prefiro não fazer. Se você fizer, gostaria de ficar sabendo se deu algum problema no longo prazo (e, se o YouTube inquirir, não diga que seguiu sugestão minha...).

Inteligência artificial

O DVR-Scan deixa passar muitos falsos positivos em algumas situações. Um problema conhecido de câmeras com infravermelho é atrair enxames daqueles mosquitinhos à noite. Outra situação é noite de sereno, as microgotas de água são pequenos espelhos de infravermelho! Em noites particularmente ruins, o ganho de compactação é quase zero.

O próximo passo é portanto mudar ou aprimorar a tecnologia de geração de compactos, usando "inteligência artificial", também conhecida como machine learning ou ML. (Reluto em chamar ML de inteligência artificial, considero esta área mais como um ramo da estatística, viabilizada pelo avanço da informática.)

Tenho utilizado o Ultralytics YOLO, com resultados muito animadores. O negócio funciona mesmo! E atinge uma taxa de FPS razoável, mesmo sem GPU. Claro que é muito mais lento que o DVR-Scan, mas é uma diferença de 20x, quando o esperado era uma diferença de 200x ou 500x.

Os principais custos do ML são obter uma base de dados (no caso, um banco de imagens) para treinar a rede neural, e o próprio consumo de CPU/GPU no treinamento da rede. Mas o projeto YOLO oferece, de graça, redes pré-treinadas com algumas dezenas de objetos cotidianos (pessoas, animais domésticos, veículos, frutas, etc.). Isto é perfeitamente suficiente para uso amador.

Além disso, fornece ferramentas suficientes para você treinar sua própria rede neural, com suas imagens. E uma das virtudes do YOLO é que você precisa muito menos amostras do que em outros tipos. Um conhecido meu orientou a empregada a tirar fotos do cocô do cachorro, para num segundo momento desenvolver um robô estilo Roomba só para recolher os dejetos.

(Isso me lembra ainda outro conhecido que gastou R$ 30k, no início da década de 2010, para enviar os cachorros de mudança para a Europa. Falei para ele que enviá-los pro outro mundo custaria apenas R$ 10... Não admira tantos ex-Conectivos não me dirigem a palavra hoje em dia.)

Não é confiável o suficiente para substituir um alarme (certamente existem à venda redes neurais mais bem treinadas para este fim), a rede gratuita do YOLO às vezes tomou uma betoneira por um carneiro e um arbusto por uma pessoa (vídeo infravermelho, certamente não ajudou; brincamos aqui que o YOLO detectou uma alma penada). Mas para detectar partes relevantes de um vídeo, principalmente se pré-filtrado pelo DVR-Scan, funciona muito bem.

Informações sobre instalação e primeiras dicas de uso podem ser encontradas neste site (FreeCodeCamp).

Exemplos

O exemplo abaixo, levemente melhorado em relação aos exemplos providos pela documentação do YOLO, aceita um vídeo como parâmetro, e gera outro vídeo com os frames em que algum objeto foi detectado:

import cv2, sys
from ultralytics import YOLO

cap = cv2.VideoCapture(sys.argv[1])
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = int(cap.get(cv2.CAP_PROP_FPS))

classes = [0, 1, 2, 3, 5, 7]

out = cv2.VideoWriter(sys.argv[1] + '.webm',
        cv2.VideoWriter_fourcc('V', 'P', '8', '0'),
        fps,
        (width, height))

model = YOLO("./yolov8n.pt")

while cap.isOpened():
    success, frame = cap.read()
    if not success:
        break
    results = model.predict(frame, conf=0.45, classes=classes,
                            boxes=True, verbose=False)
    if len(results[0].boxes):
        annotated_frame = results[0].plot()
        # out.write(frame)
        out.write(annotated_frame)

out.release()

A união do YOLO com a biblioteca OpenCV é um negócio surpreendente, tanto pelo poder de fogo quanto pela facilidade de uso.

Uma melhoria que introduzi foi a lista classes, que lista os tipos de objetos em que estamos interessados. Isto diminui muito a taxa de falsos positivos "gratuitos", principalmente para classes de objetos que você nunca espera encontrar no seu ambiente (resolveu meu problema do YOLO achar elefantes e carneiros numa betoneira, por exemplo).

O exemplo a seguir faz algo diferente: simplesmente julga se um vídeo contém um objeto, numa das classes especificadas:

import cv2, sys, os
from ultralytics import YOLO

classes = [0, 1, 2, 3, 5, 7]
model = YOLO("./yolov8n.pt")

cap = cv2.VideoCapture(sys.argv[1])
fpstime = 1.0 / int(cap.get(cv2.CAP_PROP_FPS))

if not cap.isOpened():
    sys.exit(2)

detected = False
interval = 0.5
# Force analysis of first frame
elapsed_time = 999999.9

while cap.isOpened():
    success, frame = cap.read()
    if not success:
        break

    elapsed_time += fpstime
    if elapsed_time < interval:
        continue
    elapsed_time = 0.0

    results = model.predict(frame, conf=0.5, classes=classes,
                            boxes=True, verbose=False)
    if len(results[0].boxes):
        sys.exit(0)

sys.exit(1)

O script acima tem uma heurística, que pode ser adequada ou não a seu caso de uso: ele avalia apenas dois quadros por segundo, pulando frames intermediários. A ideia é economizar processamento assumindo que um objeto de interesse estará presente por diversos frames.

Pipeline de machine learning

Meu pipeline de geração de compactos, incluindo ML, é o seguinte:

A principal razão de fazer a análise em dois estágios é diminuir o esforço despendido pelo YOLO, pois ele roda num hardware relativamente limitado. A razão menor é que os compactos derivados unicamente do DVR-Scan ainda servem como arquivo morto, são muito menores que os originais.

Às vezes, a detecção "mecânica" do DVR-Scan acaba sendo mais esperta que o YOLO. Suponha a) um ladrão disfarçado de carneiro (falso negativo), ou um carro parado a noite inteira na frente da câmera (falso positivo para todo o tempo em que o carro está parado).

Eu até vejo o YOLO substituindo inteiramente o DVR-Scan no futuro, mas isso depende de duas coisas. A principal é arrumar hardware poderoso o suficiente para fazer análise em tempo real de todas as câmeras.

A outra, acessória, é desenvolver uma heurística de detecção de eventos, colocando no compacto apenas os trechos de vídeo onde um objeto é adicionado ou retirado de cena.

Em cima disso, podemos pensar em reconhecimento facial, reconhecimento de placas, uso do YOLO como sensor auxiliar de alarme, etc.

Hardware necessário para ML

Os scripts de exemplos acima trazem a seguinte linha:

model = YOLO("./yolov8n.pt")

O arquivo yolov8n.pt é um modelo pré-cozido oferecido pelo projeto YOLO. Na verdade, é o menor modelo dentre os oferecidos. Geralmente ele é o sugerido nos tutoriais pois é usável apenas com CPU. Porém, é certo que usar um modelo maior proporciona um ganho de precisão.

Com o modelo acima, que como foi dito é o nanico da ninhada, o YOLO atinge uma taxa de análise de 5FPS num Intel NUC J4125. Em resumo, é lento. (Nesse mesmo computador, o DVR-Scan atinge 110FPS.)

Para fazer análise em tempo real com YOLO, você vai ter de arrumar um servidor de borda com GPU NVidia. Para começar a brincadeira, uma GPU antiguinha como uma GTX1080 já serve. Se não for em tempo real, um PC rápido sem GPU, ou uma instância EC2 de tamanho moderado, podem dar conta do recado.