Marcus Vinicius Zerbini Canhaço commited on
Commit
8fb6272
·
0 Parent(s):

feat: versão inicial limpa

Browse files
Files changed (42) hide show
  1. .env.example +52 -0
  2. .env.huggingface +33 -0
  3. .gitignore +62 -0
  4. Dockerfile +64 -0
  5. README.md +111 -0
  6. app.py +31 -0
  7. pytest.ini +19 -0
  8. requirements.txt +77 -0
  9. src/__init__.py +0 -0
  10. src/application/__init__.py +0 -0
  11. src/application/dto/__init__.py +0 -0
  12. src/application/interfaces/__init__.py +0 -0
  13. src/application/use_cases/__init__.py +0 -0
  14. src/application/use_cases/process_video.py +168 -0
  15. src/domain/__init__.py +0 -0
  16. src/domain/detectors/__init__.py +0 -0
  17. src/domain/detectors/base.py +200 -0
  18. src/domain/detectors/cpu.py +587 -0
  19. src/domain/detectors/gpu.py +396 -0
  20. src/domain/entities/__init__.py +0 -0
  21. src/domain/entities/detection.py +22 -0
  22. src/domain/factories/__init__.py +0 -0
  23. src/domain/factories/detector_factory.py +296 -0
  24. src/domain/interfaces/__init__.py +0 -0
  25. src/domain/interfaces/detector.py +26 -0
  26. src/domain/interfaces/notification.py +23 -0
  27. src/domain/repositories/__init__.py +0 -0
  28. src/infrastructure/__init__.py +0 -0
  29. src/infrastructure/services/__init__.py +0 -0
  30. src/infrastructure/services/notification_services.py +116 -0
  31. src/infrastructure/services/weapon_detector.py +190 -0
  32. src/main.py +69 -0
  33. src/presentation/__init__.py +0 -0
  34. src/presentation/interfaces/__init__.py +0 -0
  35. src/presentation/web/__init__.py +0 -0
  36. src/presentation/web/gradio_interface.py +235 -0
  37. tests/__init__.py +1 -0
  38. tests/conftest.py +49 -0
  39. tests/integration/__init__.py +1 -0
  40. tests/integration/test_gradio_interface.py +1 -0
  41. tests/unit/__init__.py +1 -0
  42. tests/unit/test_weapon_detector_service.py +1 -0
.env.example ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Configurações da Aplicação
2
+ DEBUG=False
3
+ PORT=7860
4
+ HOST=0.0.0.0
5
+
6
+ # Configurações do Modelo
7
+ MODEL_THRESHOLD=0.4
8
+ TARGET_FPS=2
9
+
10
+ # Configurações de Cache
11
+ CACHE_DIR=/tmp/weapon_detection_cache
12
+ MAX_CACHE_SIZE=1000000000 # 1GB em bytes
13
+ RESULT_CACHE_SIZE=1000
14
+
15
+ # Configurações de Upload
16
+ MAX_CONTENT_LENGTH=100000000 # 100MB em bytes
17
+ ALLOWED_EXTENSIONS=mp4,avi,mov,webm
18
+
19
+ # Configurações de Processamento
20
+ MAX_VIDEO_DURATION=300 # 5 minutos em segundos
21
+ MIN_CONFIDENCE=0.3
22
+ MAX_DETECTIONS_PER_FRAME=10
23
+
24
+ # Configurações de E-mail
25
+ EMAIL_APP_PASSWORD=sua_senha_de_app_aqui
26
+ NOTIFICATION_EMAIL="" # E-mail para envio de notificações
27
+
28
+ # Configurações do Modelo
29
+ HUGGING_FACE_TOKEN="" # Token do Hugging Face para acesso aos modelos
30
+ TOKENIZERS_PARALLELISM=false
31
+ MODEL_CACHE_DIR=./.model_cache
32
+ BATCH_SIZE=16
33
+ MAX_WORKERS=2
34
+ USE_HALF_PRECISION=true
35
+ DETECTION_CONFIDENCE_THRESHOLD=0.5
36
+ MODEL_CONFIDENCE_THRESHOLD=0.5
37
+ MODEL_IOU_THRESHOLD=0.45
38
+
39
+ # Configurações do Servidor
40
+ SERVER_HOST=0.0.0.0
41
+ SERVER_PORT=7860
42
+ ENABLE_SHARING=true # true para ambiente local, false para Hugging Face
43
+
44
+ # Configurações de Vídeo
45
+ DEFAULT_FPS=2
46
+ DEFAULT_RESOLUTION=640
47
+
48
+ # Configurações de GPU (opcional - apenas se tiver GPU)
49
+ CUDA_VISIBLE_DEVICES=0
50
+ TORCH_CUDA_ARCH_LIST="7.5"
51
+ NVIDIA_VISIBLE_DEVICES=all
52
+ NVIDIA_DRIVER_CAPABILITIES=compute,utility
.env.huggingface ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Configurações do Modelo
2
+ HUGGING_FACE_TOKEN="" # Configure no Hugging Face Space
3
+ TOKENIZERS_PARALLELISM=false
4
+ MODEL_CACHE_DIR=./.model_cache
5
+ BATCH_SIZE=16
6
+ MAX_WORKERS=2
7
+ USE_HALF_PRECISION=true
8
+ DETECTION_CONFIDENCE_THRESHOLD=0.5
9
+ MODEL_CONFIDENCE_THRESHOLD=0.5
10
+ MODEL_IOU_THRESHOLD=0.45
11
+
12
+ # Configurações de Cache
13
+ CACHE_DIR=/code/.cache/weapon_detection_cache
14
+ RESULT_CACHE_SIZE=1000
15
+
16
+ # Configurações de E-mail
17
+ NOTIFICATION_EMAIL="" # Configure no Hugging Face Space
18
+ SENDGRID_API_KEY="" # Configure no Hugging Face Space
19
+
20
+ # Configurações do Servidor
21
+ SERVER_HOST=0.0.0.0
22
+ SERVER_PORT=7860
23
+ ENABLE_SHARING=false
24
+
25
+ # Configurações de Vídeo
26
+ DEFAULT_FPS=2
27
+ DEFAULT_RESOLUTION=640
28
+
29
+ # Configurações de GPU
30
+ CUDA_VISIBLE_DEVICES=0
31
+ TORCH_CUDA_ARCH_LIST="7.5"
32
+ NVIDIA_VISIBLE_DEVICES=all
33
+ NVIDIA_DRIVER_CAPABILITIES=compute,utility
.gitignore ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ambiente virtual
2
+ .venv/
3
+ venv/
4
+ ENV/
5
+ env/
6
+ .env/
7
+ .python-version
8
+
9
+ # Arquivos Python
10
+ __pycache__/
11
+ *.py[cod]
12
+ *$py.class
13
+ *.so
14
+ build/
15
+ dist/
16
+ *.egg-info/
17
+
18
+ # Arquivos de ambiente
19
+ .env
20
+ .env.*
21
+ !.env.example
22
+ !.env.huggingface
23
+
24
+ # IDE
25
+ .idea/
26
+ .vscode/
27
+ *.swp
28
+ *.swo
29
+
30
+ # Sistema
31
+ .DS_Store
32
+ Thumbs.db
33
+
34
+ # Cache e logs
35
+ *.log
36
+ .cache/
37
+ .pytest_cache/
38
+ .model_cache/
39
+
40
+ # Hugging Face
41
+ .huggingface/
42
+ .hf-cache/
43
+ .transformers/
44
+ .torch/
45
+
46
+ # Arquivos grandes
47
+ videos/
48
+ *.mp4
49
+ *.avi
50
+ *.mov
51
+ *.mkv
52
+ *.webm
53
+ *.pt
54
+ *.pth
55
+ *.onnx
56
+ *.tflite
57
+ *.h5
58
+ *.model
59
+ *.bin
60
+ *.tar
61
+ *.gz
62
+ *.zip
Dockerfile ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM nvidia/cuda:12.1.0-cudnn8-runtime-ubuntu22.04
2
+
3
+ # Configurar ambiente não interativo
4
+ ENV DEBIAN_FRONTEND=noninteractive
5
+
6
+ WORKDIR /code
7
+
8
+ # Instalar Python e dependências do sistema
9
+ RUN apt-get update && apt-get install -y \
10
+ python3.10 \
11
+ python3-pip \
12
+ python3.10-venv \
13
+ libgl1-mesa-glx \
14
+ libglib2.0-0 \
15
+ ffmpeg \
16
+ git \
17
+ git-lfs \
18
+ && apt-get clean \
19
+ && rm -rf /var/lib/apt/lists/*
20
+
21
+ # Inicializar Git LFS
22
+ RUN git lfs install
23
+
24
+ # Copiar requirements primeiro para aproveitar cache
25
+ COPY requirements.txt .
26
+
27
+ # Configurar ambiente Python
28
+ ENV VIRTUAL_ENV=/opt/venv
29
+ RUN python3.10 -m venv $VIRTUAL_ENV
30
+ ENV PATH="$VIRTUAL_ENV/bin:$PATH"
31
+
32
+ # Instalar dependências Python otimizadas para GPU
33
+ RUN pip install --no-cache-dir -U pip && \
34
+ pip install --no-cache-dir torch torchvision --extra-index-url https://download.pytorch.org/whl/cu121 && \
35
+ pip install --no-cache-dir -r requirements.txt
36
+
37
+ # Criar diretório de vídeos e cache
38
+ RUN mkdir -p /code/videos /code/.cache/huggingface /code/.cache/torch && \
39
+ chmod -R 777 /code/.cache /code/videos
40
+
41
+ # Configurar variáveis de ambiente
42
+ ENV HOST=0.0.0.0 \
43
+ PORT=7860 \
44
+ PYTHONUNBUFFERED=1 \
45
+ PYTHONPATH=/code \
46
+ TRANSFORMERS_CACHE=/code/.cache/huggingface \
47
+ TORCH_HOME=/code/.cache/torch \
48
+ GRADIO_SERVER_NAME=0.0.0.0 \
49
+ GRADIO_SERVER_PORT=7860 \
50
+ SYSTEM=spaces \
51
+ CUDA_VISIBLE_DEVICES=0 \
52
+ HUGGINGFACE_HUB_CACHE=/code/.cache/huggingface \
53
+ HF_HOME=/code/.cache/huggingface \
54
+ TORCH_CUDA_ARCH_LIST="7.5" \
55
+ MAX_WORKERS=2
56
+
57
+ # Copiar arquivos do projeto
58
+ COPY . .
59
+
60
+ # Expor porta
61
+ EXPOSE 7860
62
+
63
+ # Comando para iniciar a aplicação
64
+ CMD ["python3", "app.py"]
README.md ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Weapon Detection App
3
+ emoji: 🚨
4
+ colorFrom: red
5
+ colorTo: yellow
6
+ sdk: gradio
7
+ sdk_version: 5.15.0
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ hardware: true
12
+ resources:
13
+ accelerator: T4
14
+ gpu: true
15
+ ---
16
+
17
+ # Sistema de Detecção de Riscos em Vídeo
18
+
19
+ Este projeto implementa um sistema de detecção de riscos em vídeo utilizando YOLOv8 e Clean Architecture.
20
+
21
+ ## Pré-requisitos
22
+
23
+ - Python 3.9 ou superior
24
+ - pip (gerenciador de pacotes Python)
25
+ - Ambiente virtual Python (recomendado)
26
+
27
+ ## Configuração do Ambiente
28
+
29
+ 1. Clone o repositório:
30
+ ```bash
31
+ git clone [URL_DO_REPOSITORIO]
32
+ cd [NOME_DO_DIRETORIO]
33
+ ```
34
+
35
+ 2. Crie e ative um ambiente virtual:
36
+ ```bash
37
+ python -m venv .venv
38
+ source .venv/bin/activate # Linux/Mac
39
+ # OU
40
+ .venv\Scripts\activate # Windows
41
+ ```
42
+
43
+ 3. Instale as dependências:
44
+ ```bash
45
+ pip install -r requirements.txt
46
+ ```
47
+
48
+ 4. Configure as variáveis de ambiente:
49
+ Crie um arquivo `.env` na raiz do projeto com as seguintes variáveis:
50
+ ```
51
+ NOTIFICATION_API_KEY=sua_chave_api
52
+ ```
53
+
54
+ ## Executando o Projeto
55
+
56
+ 1. Ative o ambiente virtual (se ainda não estiver ativo)
57
+
58
+ 2. Execute o aplicativo:
59
+ ```bash
60
+ python src/main.py
61
+ ```
62
+
63
+ 3. Acesse a interface web através do navegador no endereço mostrado no terminal (geralmente http://localhost:7860)
64
+
65
+ ## Funcionalidades
66
+
67
+ - Upload de vídeos para análise
68
+ - Detecção de objetos em tempo real
69
+ - Configuração de parâmetros de detecção
70
+ - Sistema de notificações
71
+ - Monitoramento de recursos do sistema
72
+
73
+ ## Estrutura do Projeto
74
+
75
+ O projeto segue os princípios da Clean Architecture:
76
+
77
+ - `domain/`: Regras de negócio e entidades
78
+ - `application/`: Casos de uso e interfaces
79
+ - `infrastructure/`: Implementações concretas
80
+ - `presentation/`: Interface com usuário (Gradio)
81
+
82
+ ## Contribuindo
83
+
84
+ 1. Fork o projeto
85
+ 2. Crie uma branch para sua feature (`git checkout -b feature/AmazingFeature`)
86
+ 3. Commit suas mudanças (`git commit -m 'Add some AmazingFeature'`)
87
+ 4. Push para a branch (`git push origin feature/AmazingFeature`)
88
+ 5. Abra um Pull Request
89
+
90
+ ## Tecnologias
91
+
92
+ - Python 3.8+
93
+ - PyTorch com CUDA
94
+ - OWL-ViT
95
+ - Gradio
96
+ - FFmpeg
97
+
98
+ ## Requisitos de Hardware
99
+
100
+ - GPU NVIDIA T4 (fornecida pelo Hugging Face)
101
+ - 16GB de RAM
102
+ - Armazenamento para cache de modelos
103
+
104
+ ## Limitações
105
+
106
+ - Processamento pode ser lento em CPUs menos potentes
107
+ - Requer GPU para melhor performance
108
+ - Alguns falsos positivos em condições de baixa luz
109
+
110
+ ---
111
+ Desenvolvido com ❤️ para o Hackathon FIAP
app.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ from src.main import main
4
+ from dotenv import load_dotenv
5
+
6
+ # Configurar logging
7
+ logging.basicConfig(
8
+ level=logging.INFO,
9
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
10
+ )
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ if __name__ == "__main__":
15
+ try:
16
+ # Verificar se está rodando no Hugging Face
17
+ IS_HUGGINGFACE = os.getenv('SPACE_ID') is not None
18
+
19
+ # Carregar configurações do ambiente apropriado
20
+ if IS_HUGGINGFACE:
21
+ load_dotenv('.env.huggingface')
22
+ logger.info("Ambiente HuggingFace detectado")
23
+ else:
24
+ load_dotenv('.env')
25
+ logger.info("Ambiente local detectado")
26
+
27
+ # Iniciar aplicação
28
+ main()
29
+ except Exception as e:
30
+ logger.error(f"Erro ao iniciar aplicação: {str(e)}")
31
+ raise
pytest.ini ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [pytest]
2
+ python_files = test_*.py
3
+ python_classes = Test*
4
+ python_functions = test_*
5
+
6
+ addopts = -v -s --strict-markers
7
+
8
+ markers =
9
+ slow: marks tests as slow (deselect with '-m "not slow"')
10
+ integration: marks tests as integration tests
11
+ unit: marks tests as unit tests
12
+
13
+ testpaths =
14
+ tests
15
+
16
+ log_cli = true
17
+ log_cli_level = INFO
18
+ log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)
19
+ log_cli_date_format = %Y-%m-%d %H:%M:%S
requirements.txt ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ accelerate==1.3.0
2
+ aiofiles==23.2.1
3
+ annotated-types==0.7.0
4
+ anyio==4.8.0
5
+ blinker==1.9.0
6
+ certifi==2024.12.14
7
+ charset-normalizer==3.4.1
8
+ click==8.1.8
9
+ fastapi==0.115.7
10
+ ffmpeg-python==0.2.0
11
+ ffmpy==0.5.0
12
+ filelock==3.17.0
13
+ Flask==3.1.0
14
+ fsspec==2024.12.0
15
+ future==1.0.0
16
+ gradio==5.15.0
17
+ gradio_client==1.7.0
18
+ h11==0.14.0
19
+ httpcore==1.0.7
20
+ httpx==0.28.1
21
+ huggingface-hub==0.28.1
22
+ idna==3.10
23
+ itsdangerous==2.2.0
24
+ Jinja2==3.1.5
25
+ markdown-it-py==3.0.0
26
+ MarkupSafe==2.1.5
27
+ mdurl==0.1.2
28
+ mpmath==1.3.0
29
+ networkx==3.4.2
30
+ numpy==2.2.2
31
+ nvidia-ml-py3==7.352.0
32
+ opencv-python==4.11.0.86
33
+ opencv-python-headless==4.11.0.86
34
+ orjson==3.10.15
35
+ packaging==24.2
36
+ pandas==2.2.3
37
+ pillow==11.1.0
38
+ psutil==5.9.5
39
+ pydantic==2.10.6
40
+ pydantic_core==2.27.2
41
+ pydub==0.25.1
42
+ Pygments==2.19.1
43
+ python-dateutil==2.9.0.post0
44
+ python-dotenv==1.0.1
45
+ python-http-client==3.3.7
46
+ python-multipart==0.0.20
47
+ pytz==2024.2
48
+ PyYAML==6.0.2
49
+ regex==2024.11.6
50
+ requests==2.32.3
51
+ rich==13.9.4
52
+ ruff==0.9.3
53
+ safehttpx==0.1.6
54
+ safetensors==0.5.2
55
+ scipy==1.15.1
56
+ semantic-version==2.10.0
57
+ sendgrid==6.11.0
58
+ setuptools==75.8.0
59
+ shellingham==1.5.4
60
+ six==1.17.0
61
+ sniffio==1.3.1
62
+ starkbank-ecdsa==2.2.0
63
+ starlette==0.45.2
64
+ sympy==1.13.1
65
+ tokenizers==0.18.0
66
+ tomlkit==0.13.2
67
+ torch==2.5.1
68
+ torchvision==0.20.1
69
+ tqdm==4.67.1
70
+ transformers==4.36.2
71
+ typer==0.15.1
72
+ typing_extensions==4.12.2
73
+ tzdata==2025.1
74
+ urllib3==2.3.0
75
+ uvicorn==0.34.0
76
+ websockets==14.2
77
+ Werkzeug==3.1.3
src/__init__.py ADDED
File without changes
src/application/__init__.py ADDED
File without changes
src/application/dto/__init__.py ADDED
File without changes
src/application/interfaces/__init__.py ADDED
File without changes
src/application/use_cases/__init__.py ADDED
File without changes
src/application/use_cases/process_video.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from typing import Optional, Dict, Any
3
+ from ...domain.interfaces.detector import DetectorInterface
4
+ from ...domain.interfaces.notification import NotificationFactory
5
+ from ...domain.entities.detection import DetectionResult
6
+ import logging
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ @dataclass
11
+ class ProcessVideoRequest:
12
+ """DTO para requisição de processamento de vídeo."""
13
+ video_path: str
14
+ threshold: float = 0.5
15
+ fps: Optional[int] = None
16
+ resolution: Optional[int] = None
17
+ notification_type: Optional[str] = None
18
+ notification_target: Optional[str] = None
19
+
20
+ @dataclass
21
+ class ProcessVideoResponse:
22
+ """DTO para resposta do processamento de vídeo."""
23
+ status_message: str
24
+ detection_result: DetectionResult
25
+ memory_info: str
26
+ device_info: str
27
+ cache_stats: Optional[Dict[str, Any]] = None
28
+
29
+ class ProcessVideoUseCase:
30
+ """Caso de uso para processamento de vídeo e notificação."""
31
+
32
+ def __init__(
33
+ self,
34
+ detector: DetectorInterface,
35
+ notification_factory: NotificationFactory,
36
+ default_fps: int,
37
+ default_resolution: int
38
+ ):
39
+ self.detector = detector
40
+ self.notification_factory = notification_factory
41
+ self.default_fps = default_fps
42
+ self.default_resolution = default_resolution
43
+
44
+ def execute(self, request: ProcessVideoRequest) -> ProcessVideoResponse:
45
+ """Executa o processamento do vídeo e envia notificações se necessário."""
46
+ try:
47
+ # Usar valores padrão se não especificados
48
+ fps = request.fps or self.default_fps
49
+ resolution = request.resolution or self.default_resolution
50
+
51
+ # Processar vídeo
52
+ output_path, result = self.detector.process_video(
53
+ request.video_path,
54
+ fps=fps,
55
+ threshold=request.threshold,
56
+ resolution=resolution
57
+ )
58
+
59
+ # Enviar notificação se houver detecções e destino configurado
60
+ if result.detections and request.notification_type and request.notification_target:
61
+ notification_service = self.notification_factory.create_service(request.notification_type)
62
+ if notification_service:
63
+ detection_data = {
64
+ 'detections': [
65
+ {
66
+ 'label': det.label,
67
+ 'confidence': det.confidence,
68
+ 'box': det.box,
69
+ 'timestamp': det.timestamp
70
+ } for det in result.detections
71
+ ],
72
+ 'technical': {
73
+ 'threshold': request.threshold,
74
+ 'fps': fps,
75
+ 'resolution': resolution
76
+ }
77
+ }
78
+ notification_service.send_notification(detection_data, request.notification_target)
79
+
80
+ # Formatar mensagem de status
81
+ status_msg = self._format_status_message(result)
82
+
83
+ # Obter informações do sistema de forma segura
84
+ try:
85
+ device_info = self.detector.get_device_info() if hasattr(self.detector, 'get_device_info') else {}
86
+ except Exception as e:
87
+ logger.error(f"Erro ao obter informações do dispositivo: {str(e)}")
88
+ device_info = {}
89
+
90
+ try:
91
+ cache_stats = self.detector.get_cache_stats() if hasattr(self.detector, 'get_cache_stats') else {}
92
+ except Exception as e:
93
+ logger.error(f"Erro ao obter estatísticas do cache: {str(e)}")
94
+ cache_stats = {}
95
+
96
+ # Limpar memória
97
+ try:
98
+ self.detector.clean_memory()
99
+ except Exception as e:
100
+ logger.error(f"Erro ao limpar memória: {str(e)}")
101
+
102
+ return ProcessVideoResponse(
103
+ status_message=status_msg,
104
+ detection_result=result,
105
+ memory_info=self._format_memory_info(device_info),
106
+ device_info=self._format_device_info(device_info),
107
+ cache_stats=cache_stats
108
+ )
109
+
110
+ except Exception as e:
111
+ logger.error(f"Erro ao executar caso de uso: {str(e)}")
112
+ # Criar um resultado vazio em caso de erro
113
+ empty_result = DetectionResult(
114
+ video_path=request.video_path,
115
+ detections=[],
116
+ frames_analyzed=0,
117
+ total_time=0.0,
118
+ device_type="Unknown",
119
+ frame_extraction_time=0.0,
120
+ analysis_time=0.0
121
+ )
122
+ return ProcessVideoResponse(
123
+ status_message="Erro ao processar o vídeo. Por favor, tente novamente.",
124
+ detection_result=empty_result,
125
+ memory_info="N/A",
126
+ device_info="N/A",
127
+ cache_stats={}
128
+ )
129
+
130
+ def _format_status_message(self, result: DetectionResult) -> str:
131
+ """Formata a mensagem de status do processamento."""
132
+ try:
133
+ status = "⚠️ RISCO DETECTADO" if result.detections else "✅ SEGURO"
134
+
135
+ message = f"""Processamento concluído! ({result.device_type})
136
+
137
+ Status: {status}
138
+ Detecções: {len(result.detections)}
139
+ Frames analisados: {result.frames_analyzed}
140
+ Tempo total: {result.total_time:.2f}s
141
+ Tempo de extração: {result.frame_extraction_time:.2f}s
142
+ Tempo de análise: {result.analysis_time:.2f}s"""
143
+
144
+ # Adicionar detalhes das detecções se houver
145
+ if result.detections:
146
+ message += "\n\nDetecções encontradas:"
147
+ for i, det in enumerate(result.detections[:3], 1): # Mostrar até 3 detecções
148
+ message += f"\n{i}. {det.label} (Confiança: {det.confidence:.1%}, Frame: {det.frame})"
149
+ if len(result.detections) > 3:
150
+ message += f"\n... e mais {len(result.detections) - 3} detecção(ões)"
151
+
152
+ return message
153
+
154
+ except Exception as e:
155
+ logger.error(f"Erro ao formatar mensagem de status: {str(e)}")
156
+ return "Erro ao processar o vídeo. Por favor, tente novamente."
157
+
158
+ def _format_memory_info(self, device_info: Dict[str, Any]) -> str:
159
+ if device_info.get('type') == 'GPU':
160
+ return f"GPU: {device_info.get('memory_used', 0) / 1024**2:.1f}MB / {device_info.get('memory_total', 0) / 1024**2:.1f}MB"
161
+ else:
162
+ return f"RAM: {device_info.get('memory_used', 0) / 1024**2:.1f}MB / {device_info.get('memory_total', 0) / 1024**2:.1f}MB"
163
+
164
+ def _format_device_info(self, device_info: Dict[str, Any]) -> str:
165
+ if device_info.get('type') == 'GPU':
166
+ return f"GPU: {device_info.get('name', 'Unknown')}"
167
+ else:
168
+ return f"CPU Threads: {device_info.get('threads', 'N/A')}"
src/domain/__init__.py ADDED
File without changes
src/domain/detectors/__init__.py ADDED
File without changes
src/domain/detectors/base.py ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC, abstractmethod
2
+ import gc
3
+ import torch
4
+ import logging
5
+ from typing import Dict, Any, Optional, List, Tuple
6
+ import os
7
+ import cv2
8
+ from PIL import Image
9
+ import time
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class BaseCache:
14
+ """Cache base para armazenar resultados de detecção."""
15
+ def __init__(self, max_size: int = 1000):
16
+ self.cache = {}
17
+ self.max_size = max_size
18
+ self.hits = 0
19
+ self.misses = 0
20
+ self.last_access = {}
21
+
22
+ def get(self, key: str) -> Optional[Dict]:
23
+ try:
24
+ if key in self.cache:
25
+ self.hits += 1
26
+ self.last_access[key] = time.time()
27
+ return self.cache[key]
28
+ self.misses += 1
29
+ return None
30
+ except Exception as e:
31
+ logger.error(f"Erro ao recuperar do cache: {str(e)}")
32
+ return None
33
+
34
+ def put(self, key: str, results: Dict):
35
+ try:
36
+ if len(self.cache) >= self.max_size:
37
+ oldest_key = min(self.last_access.items(), key=lambda x: x[1])[0]
38
+ del self.cache[oldest_key]
39
+ del self.last_access[oldest_key]
40
+ self.cache[key] = results
41
+ self.last_access[key] = time.time()
42
+ except Exception as e:
43
+ logger.error(f"Erro ao armazenar no cache: {str(e)}")
44
+
45
+ def clear(self):
46
+ """Limpa o cache e libera memória."""
47
+ self.cache.clear()
48
+ self.last_access.clear()
49
+ gc.collect()
50
+
51
+ def get_stats(self) -> dict:
52
+ total = self.hits + self.misses
53
+ hit_rate = (self.hits / total) * 100 if total > 0 else 0
54
+ return {
55
+ "cache_size": len(self.cache),
56
+ "max_size": self.max_size,
57
+ "hits": self.hits,
58
+ "misses": self.misses,
59
+ "hit_rate": f"{hit_rate:.2f}%",
60
+ "memory_usage": sum(sys.getsizeof(v) for v in self.cache.values())
61
+ }
62
+
63
+ class BaseDetector(ABC):
64
+ """Classe base abstrata para detectores de objetos perigosos."""
65
+ def __init__(self):
66
+ self._initialized = False
67
+ self.device = None
68
+ self.owlv2_model = None
69
+ self.owlv2_processor = None
70
+ self.text_queries = None
71
+ self.processed_text = None
72
+ self.threshold = 0.3
73
+ self.result_cache = None
74
+
75
+ @abstractmethod
76
+ def _initialize(self):
77
+ """Inicializa o modelo e o processador."""
78
+ pass
79
+
80
+ @abstractmethod
81
+ def _get_best_device(self):
82
+ """Retorna o melhor dispositivo disponível."""
83
+ pass
84
+
85
+ def initialize(self):
86
+ """Inicializa o detector se ainda não estiver inicializado."""
87
+ if not self._initialized:
88
+ self._initialize()
89
+
90
+ def extract_frames(self, video_path: str, fps: int = None, resolution: int = 640) -> List:
91
+ """Extrai frames do vídeo com taxa e resolução especificadas."""
92
+ try:
93
+ if not os.path.exists(video_path):
94
+ logger.error(f"Arquivo de vídeo não encontrado: {video_path}")
95
+ return []
96
+
97
+ cap = cv2.VideoCapture(video_path)
98
+ if not cap.isOpened():
99
+ logger.error("Erro ao abrir o vídeo")
100
+ return []
101
+
102
+ original_fps = cap.get(cv2.CAP_PROP_FPS)
103
+ target_fps = fps if fps else min(2, original_fps)
104
+ frame_interval = int(original_fps / target_fps)
105
+
106
+ frames = []
107
+ frame_count = 0
108
+
109
+ while True:
110
+ ret, frame = cap.read()
111
+ if not ret:
112
+ break
113
+
114
+ if frame_count % frame_interval == 0:
115
+ if resolution:
116
+ height, width = frame.shape[:2]
117
+ scale = resolution / max(height, width)
118
+ if scale < 1:
119
+ new_width = int(width * scale)
120
+ new_height = int(height * scale)
121
+ frame = cv2.resize(frame, (new_width, new_height))
122
+ frames.append(frame)
123
+
124
+ frame_count += 1
125
+
126
+ cap.release()
127
+ return frames
128
+
129
+ except Exception as e:
130
+ logger.error(f"Erro ao extrair frames: {str(e)}")
131
+ return []
132
+
133
+ @abstractmethod
134
+ def detect_objects(self, image: Image.Image, threshold: float = 0.3) -> List[Dict]:
135
+ """Detecta objetos em uma imagem."""
136
+ pass
137
+
138
+ @abstractmethod
139
+ def process_video(self, video_path: str, fps: int = None, threshold: float = 0.3, resolution: int = 640) -> Tuple[str, Dict]:
140
+ """Processa um vídeo para detecção de objetos."""
141
+ pass
142
+
143
+ def clean_memory(self):
144
+ """Limpa memória não utilizada."""
145
+ try:
146
+ if torch.cuda.is_available():
147
+ torch.cuda.empty_cache()
148
+ logger.debug("Cache GPU limpo")
149
+ gc.collect()
150
+ logger.debug("Garbage collector executado")
151
+ except Exception as e:
152
+ logger.error(f"Erro ao limpar memória: {str(e)}")
153
+
154
+ def _get_detection_queries(self) -> List[str]:
155
+ """Retorna as queries otimizadas para detecção de objetos perigosos."""
156
+ firearms = ["handgun", "rifle", "shotgun", "machine gun", "firearm"]
157
+ edged_weapons = ["knife", "dagger", "machete", "box cutter", "sword"]
158
+ ranged_weapons = ["crossbow", "bow"]
159
+ sharp_objects = ["blade", "razor", "glass shard", "screwdriver", "metallic pointed object"]
160
+
161
+ firearm_contexts = ["close-up", "clear view", "detailed"]
162
+ edged_contexts = ["close-up", "clear view", "detailed", "metallic", "sharp"]
163
+ ranged_contexts = ["close-up", "clear view", "detailed"]
164
+ sharp_contexts = ["close-up", "clear view", "detailed", "sharp"]
165
+
166
+ queries = []
167
+
168
+ for weapon in firearms:
169
+ queries.append(f"a photo of a {weapon}")
170
+ for context in firearm_contexts:
171
+ queries.append(f"a photo of a {context} {weapon}")
172
+
173
+ for weapon in edged_weapons:
174
+ queries.append(f"a photo of a {weapon}")
175
+ for context in edged_contexts:
176
+ queries.append(f"a photo of a {context} {weapon}")
177
+
178
+ for weapon in ranged_weapons:
179
+ queries.append(f"a photo of a {weapon}")
180
+ for context in ranged_contexts:
181
+ queries.append(f"a photo of a {context} {weapon}")
182
+
183
+ for weapon in sharp_objects:
184
+ queries.append(f"a photo of a {weapon}")
185
+ for context in sharp_contexts:
186
+ queries.append(f"a photo of a {context} {weapon}")
187
+
188
+ queries = sorted(list(set(queries)))
189
+ logger.info(f"Total de queries otimizadas geradas: {len(queries)}")
190
+ return queries
191
+
192
+ @abstractmethod
193
+ def _apply_nms(self, detections: List[Dict], iou_threshold: float = 0.5) -> List[Dict]:
194
+ """Aplica Non-Maximum Suppression nas detecções."""
195
+ pass
196
+
197
+ @abstractmethod
198
+ def _preprocess_image(self, image: Any) -> Any:
199
+ """Pré-processa a imagem para o formato adequado."""
200
+ pass
src/domain/detectors/cpu.py ADDED
@@ -0,0 +1,587 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from transformers import Owlv2Processor, Owlv2ForObjectDetection
3
+ from PIL import Image
4
+ import numpy as np
5
+ import cv2
6
+ import time
7
+ from typing import List, Dict, Tuple, Optional, Union
8
+ import os
9
+ from tqdm import tqdm
10
+ import json
11
+ from pathlib import Path
12
+ from contextlib import nullcontext
13
+ import threading
14
+ import hashlib
15
+ import pickle
16
+ from datetime import datetime
17
+ from dotenv import load_dotenv
18
+ import tempfile
19
+ import subprocess
20
+ import shutil
21
+ import traceback
22
+ import psutil
23
+ import logging
24
+ import gc
25
+ import sys
26
+ from concurrent.futures import ThreadPoolExecutor
27
+ import torch.nn.functional as F
28
+ from .base import BaseDetector, BaseCache
29
+
30
+
31
+ # Configurar logging
32
+ logging.basicConfig(level=logging.INFO)
33
+ logger = logging.getLogger(__name__)
34
+
35
+ # Carregar variáveis de ambiente
36
+ load_dotenv()
37
+
38
+ class CPUCache(BaseCache):
39
+ """Cache otimizado para CPU."""
40
+ def __init__(self, max_size: int = 1000):
41
+ super().__init__(max_size)
42
+ self.device = torch.device('cpu')
43
+
44
+ class WeaponDetectorCPU(BaseDetector):
45
+ """Implementação CPU do detector de armas."""
46
+ def __init__(self):
47
+ """Inicializa variáveis básicas."""
48
+ super().__init__()
49
+ self.default_resolution = 640
50
+ self.device = torch.device('cpu')
51
+
52
+ def _get_best_device(self):
53
+ return torch.device('cpu')
54
+
55
+ def _initialize(self):
56
+ """Inicializa o modelo e o processador para execução em CPU."""
57
+ try:
58
+ # Configurações otimizadas para CPU
59
+ torch.set_num_threads(min(8, os.cpu_count()))
60
+ torch.set_num_interop_threads(min(8, os.cpu_count()))
61
+
62
+ # Carregar modelo com configurações otimizadas
63
+ cache_dir = os.getenv('CACHE_DIR', '/tmp/weapon_detection_cache')
64
+ os.makedirs(cache_dir, exist_ok=True)
65
+
66
+ model_name = "google/owlv2-base-patch16"
67
+ logger.info("Carregando modelo e processador...")
68
+
69
+ self.owlv2_processor = Owlv2Processor.from_pretrained(
70
+ model_name,
71
+ cache_dir=cache_dir
72
+ )
73
+
74
+ self.owlv2_model = Owlv2ForObjectDetection.from_pretrained(
75
+ model_name,
76
+ cache_dir=cache_dir,
77
+ torch_dtype=torch.float32,
78
+ low_cpu_mem_usage=True
79
+ ).to(self.device)
80
+
81
+ self.owlv2_model.eval()
82
+
83
+ # Usar queries do método base
84
+ self.text_queries = self._get_detection_queries()
85
+ logger.info(f"Total de queries carregadas: {len(self.text_queries)}")
86
+
87
+ # Processar queries uma única vez
88
+ logger.info("Processando queries...")
89
+ self.processed_text = self.owlv2_processor(
90
+ text=self.text_queries,
91
+ return_tensors="pt",
92
+ padding=True
93
+ ).to(self.device)
94
+
95
+ # Inicializar cache
96
+ cache_size = int(os.getenv('RESULT_CACHE_SIZE', '1000'))
97
+ self.result_cache = CPUCache(max_size=cache_size)
98
+
99
+ logger.info("Inicialização CPU completa!")
100
+ self._initialized = True
101
+
102
+ except Exception as e:
103
+ logger.error(f"Erro na inicialização CPU: {str(e)}")
104
+ raise
105
+
106
+ def _apply_nms(self, detections: list, iou_threshold: float = 0.5) -> list:
107
+ """Aplica NMS usando operações em CPU."""
108
+ try:
109
+ if not detections:
110
+ return []
111
+
112
+ boxes = torch.tensor([[d["box"][0], d["box"][1], d["box"][2], d["box"][3]] for d in detections])
113
+ scores = torch.tensor([d["confidence"] for d in detections])
114
+ labels = [d["label"] for d in detections]
115
+
116
+ area = (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1])
117
+ _, order = scores.sort(descending=True)
118
+
119
+ keep = []
120
+ while order.numel() > 0:
121
+ if order.numel() == 1:
122
+ keep.append(order.item())
123
+ break
124
+ i = order[0]
125
+ keep.append(i.item())
126
+
127
+ xx1 = torch.max(boxes[i, 0], boxes[order[1:], 0])
128
+ yy1 = torch.max(boxes[i, 1], boxes[order[1:], 1])
129
+ xx2 = torch.min(boxes[i, 2], boxes[order[1:], 2])
130
+ yy2 = torch.min(boxes[i, 3], boxes[order[1:], 3])
131
+
132
+ w = torch.clamp(xx2 - xx1, min=0)
133
+ h = torch.clamp(yy2 - yy1, min=0)
134
+ inter = w * h
135
+
136
+ ovr = inter / (area[i] + area[order[1:]] - inter)
137
+ ids = (ovr <= iou_threshold).nonzero().squeeze()
138
+ if ids.numel() == 0:
139
+ break
140
+ order = order[ids + 1]
141
+
142
+ filtered_detections = []
143
+ for idx in keep:
144
+ filtered_detections.append({
145
+ "confidence": scores[idx].item(),
146
+ "box": boxes[idx].tolist(),
147
+ "label": labels[idx]
148
+ })
149
+ return filtered_detections
150
+
151
+ except Exception as e:
152
+ logger.error(f"Erro ao aplicar NMS: {str(e)}")
153
+ return []
154
+
155
+ def _preprocess_image(self, image: Image.Image) -> Image.Image:
156
+ """Pré-processa a imagem para o tamanho 640x640 e garante RGB."""
157
+ try:
158
+ target_size = (640, 640)
159
+ if image.mode != 'RGB':
160
+ image = image.convert('RGB')
161
+ if image.size != target_size:
162
+ ratio = min(target_size[0] / image.size[0], target_size[1] / image.size[1])
163
+ new_size = tuple(int(dim * ratio) for dim in image.size)
164
+ image = image.resize(new_size, Image.LANCZOS)
165
+ if new_size != target_size:
166
+ new_image = Image.new('RGB', target_size, (0, 0, 0))
167
+ paste_x = (target_size[0] - new_size[0]) // 2
168
+ paste_y = (target_size[1] - new_size[1]) // 2
169
+ new_image.paste(image, (paste_x, paste_y))
170
+ image = new_image
171
+ return image
172
+ except Exception as e:
173
+ logger.error(f"Erro no pré-processamento: {str(e)}")
174
+ return image
175
+
176
+ def detect_objects(self, image: Image.Image, threshold: float = 0.3) -> list:
177
+ """Detecta objetos em uma imagem utilizando CPU."""
178
+ try:
179
+ image = self._preprocess_image(image)
180
+ with torch.no_grad():
181
+ image_inputs = self.owlv2_processor(
182
+ images=image,
183
+ return_tensors="pt"
184
+ ).to(self.device)
185
+ inputs = {**image_inputs, **self.processed_text}
186
+ outputs = self.owlv2_model(**inputs)
187
+
188
+ target_sizes = torch.tensor([image.size[::-1]])
189
+ results = self.owlv2_processor.post_process_grounded_object_detection(
190
+ outputs=outputs,
191
+ target_sizes=target_sizes,
192
+ threshold=threshold
193
+ )[0]
194
+
195
+ detections = []
196
+ for score, box, label in zip(results["scores"], results["boxes"], results["labels"]):
197
+ x1, y1, x2, y2 = box.tolist()
198
+ detections.append({
199
+ "confidence": score.item(),
200
+ "box": [int(x1), int(y1), int(x2), int(y2)],
201
+ "label": self.text_queries[label]
202
+ })
203
+ return self._apply_nms(detections)
204
+
205
+ except Exception as e:
206
+ logger.error(f"Erro em detect_objects: {str(e)}")
207
+ return []
208
+
209
+ def process_video(self, video_path: str, fps: int = None, threshold: float = 0.3, resolution: int = 640) -> tuple:
210
+ """Processa um vídeo utilizando CPU. Para na primeira detecção encontrada."""
211
+ try:
212
+ metrics = {
213
+ "total_time": 0,
214
+ "frame_extraction_time": 0,
215
+ "analysis_time": 0,
216
+ "frames_analyzed": 0,
217
+ "video_duration": 0,
218
+ "device_type": self.device.type,
219
+ "detections": [],
220
+ "technical": {
221
+ "model": "owlv2-base-patch16-ensemble",
222
+ "input_size": f"{resolution}x{resolution}",
223
+ "nms_threshold": 0.5,
224
+ "preprocessing": "basic",
225
+ "early_stop": True
226
+ },
227
+ }
228
+
229
+ start_time = time.time()
230
+ t0 = time.time()
231
+ frames = self.extract_frames(video_path, fps, resolution)
232
+ metrics["frame_extraction_time"] = time.time() - t0
233
+ metrics["frames_analyzed"] = len(frames)
234
+
235
+ if not frames:
236
+ logger.warning("Nenhum frame extraído do vídeo")
237
+ return video_path, metrics
238
+
239
+ metrics["video_duration"] = len(frames) / (fps or 2)
240
+ t0 = time.time()
241
+ detections = []
242
+ frames_processed = 0
243
+
244
+ # Processar um frame por vez para otimizar memória e permitir parada precoce
245
+ for frame_idx, frame in enumerate(frames):
246
+ frames_processed += 1
247
+
248
+ # Converter frame para RGB e pré-processar
249
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
250
+ image = Image.fromarray(frame_rgb)
251
+ image = self._preprocess_image(image)
252
+
253
+ # Detectar objetos com threshold direto
254
+ with torch.no_grad():
255
+ image_inputs = self.owlv2_processor(
256
+ images=image,
257
+ return_tensors="pt"
258
+ ).to(self.device)
259
+ inputs = {**image_inputs, **self.processed_text}
260
+ outputs = self.owlv2_model(**inputs)
261
+
262
+ target_sizes = torch.tensor([image.size[::-1]])
263
+ results = self.owlv2_processor.post_process_grounded_object_detection(
264
+ outputs=outputs,
265
+ target_sizes=target_sizes,
266
+ threshold=threshold # Aplicar threshold diretamente
267
+ )[0]
268
+
269
+ # Se encontrou alguma detecção acima do threshold
270
+ if len(results["scores"]) > 0:
271
+ # Pegar a detecção com maior confiança
272
+ max_score_idx = torch.argmax(results["scores"])
273
+ score = results["scores"][max_score_idx].item()
274
+ box = results["boxes"][max_score_idx].tolist()
275
+ label = results["labels"][max_score_idx].item()
276
+
277
+ detections.append({
278
+ "frame": frame_idx,
279
+ "confidence": score,
280
+ "box": [int(x) for x in box],
281
+ "label": self.text_queries[label]
282
+ })
283
+
284
+ # Atualizar métricas e parar o processamento
285
+ metrics["frames_processed_until_detection"] = frames_processed
286
+ metrics["analysis_time"] = time.time() - t0
287
+ metrics["total_time"] = time.time() - start_time
288
+ metrics["detections"] = detections
289
+ logger.info(f"Detecção encontrada após processar {frames_processed} frames")
290
+ return video_path, metrics
291
+
292
+ # Liberar memória a cada 10 frames
293
+ if frames_processed % 10 == 0:
294
+ gc.collect()
295
+
296
+ # Se chegou aqui, não encontrou nenhuma detecção
297
+ metrics["analysis_time"] = time.time() - t0
298
+ metrics["total_time"] = time.time() - start_time
299
+ metrics["frames_processed_until_detection"] = frames_processed
300
+ metrics["detections"] = detections
301
+ return video_path, metrics
302
+
303
+ except Exception as e:
304
+ logger.error(f"Erro ao processar vídeo: {str(e)}")
305
+ return video_path, {}
306
+
307
+ def extract_frames(self, video_path: str, fps: int = 2, resolution: int = 480) -> list:
308
+ """Extrai frames de um vídeo utilizando ffmpeg."""
309
+ frames = []
310
+ temp_dir = Path(tempfile.mkdtemp())
311
+ try:
312
+ threads = min(os.cpu_count(), 4) # Menor número de threads para CPU
313
+ cmd = [
314
+ 'ffmpeg', '-i', video_path,
315
+ '-threads', str(threads),
316
+ '-vf', (f'fps={fps},'
317
+ f'scale={resolution}:{resolution}:force_original_aspect_ratio=decrease:flags=lanczos,'
318
+ f'pad={resolution}:{resolution}:(ow-iw)/2:(oh-ih)/2'),
319
+ '-frame_pts', '1',
320
+ f'{temp_dir}/%d.jpg'
321
+ ]
322
+ subprocess.run(cmd, check=True, capture_output=True)
323
+ frame_files = sorted(temp_dir.glob('*.jpg'), key=lambda x: int(x.stem))
324
+ chunk_size = 50 # Menor chunk size para CPU
325
+ with ThreadPoolExecutor(max_workers=threads) as executor:
326
+ for i in range(0, len(frame_files), chunk_size):
327
+ chunk = frame_files[i:i + chunk_size]
328
+ chunk_frames = list(tqdm(
329
+ executor.map(lambda f: cv2.imread(str(f)), chunk),
330
+ desc=f"Carregando frames {i+1}-{min(i+chunk_size, len(frame_files))}",
331
+ total=len(chunk)
332
+ ))
333
+ frames.extend(chunk_frames)
334
+ if i % (chunk_size * 5) == 0:
335
+ gc.collect()
336
+ finally:
337
+ shutil.rmtree(temp_dir)
338
+ return frames
339
+
340
+ def clear_cache(self):
341
+ """Limpa o cache de resultados e libera memória."""
342
+ try:
343
+ if hasattr(self, 'result_cache'):
344
+ self.result_cache.clear()
345
+ gc.collect()
346
+ logger.info("Cache CPU limpo com sucesso")
347
+ except Exception as e:
348
+ logger.error(f"Erro ao limpar cache CPU: {str(e)}")
349
+
350
+ def _apply_nms(self, detections: list, iou_threshold: float = 0.5) -> list:
351
+ """Aplica NMS usando operações em CPU."""
352
+ try:
353
+ if not detections:
354
+ return []
355
+
356
+ boxes = torch.tensor([[d["box"][0], d["box"][1], d["box"][2], d["box"][3]] for d in detections])
357
+ scores = torch.tensor([d["confidence"] for d in detections])
358
+ labels = [d["label"] for d in detections]
359
+
360
+ area = (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1])
361
+ _, order = scores.sort(descending=True)
362
+
363
+ keep = []
364
+ while order.numel() > 0:
365
+ if order.numel() == 1:
366
+ keep.append(order.item())
367
+ break
368
+ i = order[0]
369
+ keep.append(i.item())
370
+
371
+ xx1 = torch.max(boxes[i, 0], boxes[order[1:], 0])
372
+ yy1 = torch.max(boxes[i, 1], boxes[order[1:], 1])
373
+ xx2 = torch.min(boxes[i, 2], boxes[order[1:], 2])
374
+ yy2 = torch.min(boxes[i, 3], boxes[order[1:], 3])
375
+
376
+ w = torch.clamp(xx2 - xx1, min=0)
377
+ h = torch.clamp(yy2 - yy1, min=0)
378
+ inter = w * h
379
+
380
+ ovr = inter / (area[i] + area[order[1:]] - inter)
381
+ ids = (ovr <= iou_threshold).nonzero().squeeze()
382
+ if ids.numel() == 0:
383
+ break
384
+ order = order[ids + 1]
385
+
386
+ filtered_detections = []
387
+ for idx in keep:
388
+ filtered_detections.append({
389
+ "confidence": scores[idx].item(),
390
+ "box": boxes[idx].tolist(),
391
+ "label": labels[idx]
392
+ })
393
+ return filtered_detections
394
+
395
+ except Exception as e:
396
+ logger.error(f"Erro ao aplicar NMS: {str(e)}")
397
+ return []
398
+
399
+ def _preprocess_image(self, image: Image.Image) -> Image.Image:
400
+ """Pré-processa a imagem para o tamanho 640x640 e garante RGB."""
401
+ try:
402
+ target_size = (640, 640)
403
+ if image.mode != 'RGB':
404
+ image = image.convert('RGB')
405
+ if image.size != target_size:
406
+ ratio = min(target_size[0] / image.size[0], target_size[1] / image.size[1])
407
+ new_size = tuple(int(dim * ratio) for dim in image.size)
408
+ image = image.resize(new_size, Image.LANCZOS)
409
+ if new_size != target_size:
410
+ new_image = Image.new('RGB', target_size, (0, 0, 0))
411
+ paste_x = (target_size[0] - new_size[0]) // 2
412
+ paste_y = (target_size[1] - new_size[1]) // 2
413
+ new_image.paste(image, (paste_x, paste_y))
414
+ image = new_image
415
+ return image
416
+ except Exception as e:
417
+ logger.error(f"Erro no pré-processamento: {str(e)}")
418
+ return image
419
+
420
+ def detect_objects(self, image: Image.Image, threshold: float = 0.3) -> list:
421
+ """Detecta objetos em uma imagem utilizando CPU."""
422
+ try:
423
+ image = self._preprocess_image(image)
424
+ with torch.no_grad():
425
+ image_inputs = self.owlv2_processor(
426
+ images=image,
427
+ return_tensors="pt"
428
+ ).to(self.device)
429
+ inputs = {**image_inputs, **self.processed_text}
430
+ outputs = self.owlv2_model(**inputs)
431
+
432
+ target_sizes = torch.tensor([image.size[::-1]])
433
+ results = self.owlv2_processor.post_process_grounded_object_detection(
434
+ outputs=outputs,
435
+ target_sizes=target_sizes,
436
+ threshold=threshold
437
+ )[0]
438
+
439
+ detections = []
440
+ for score, box, label in zip(results["scores"], results["boxes"], results["labels"]):
441
+ x1, y1, x2, y2 = box.tolist()
442
+ detections.append({
443
+ "confidence": score.item(),
444
+ "box": [int(x1), int(y1), int(x2), int(y2)],
445
+ "label": self.text_queries[label]
446
+ })
447
+ return self._apply_nms(detections)
448
+
449
+ except Exception as e:
450
+ logger.error(f"Erro em detect_objects: {str(e)}")
451
+ return []
452
+
453
+ def process_video(self, video_path: str, fps: int = None, threshold: float = 0.3, resolution: int = 640) -> tuple:
454
+ """Processa um vídeo utilizando CPU. Para na primeira detecção encontrada."""
455
+ try:
456
+ metrics = {
457
+ "total_time": 0,
458
+ "frame_extraction_time": 0,
459
+ "analysis_time": 0,
460
+ "frames_analyzed": 0,
461
+ "video_duration": 0,
462
+ "device_type": self.device.type,
463
+ "detections": [],
464
+ "technical": {
465
+ "model": "owlv2-base-patch16-ensemble",
466
+ "input_size": f"{resolution}x{resolution}",
467
+ "nms_threshold": 0.5,
468
+ "preprocessing": "basic",
469
+ "early_stop": True
470
+ },
471
+ }
472
+
473
+ start_time = time.time()
474
+ t0 = time.time()
475
+ frames = self.extract_frames(video_path, fps, resolution)
476
+ metrics["frame_extraction_time"] = time.time() - t0
477
+ metrics["frames_analyzed"] = len(frames)
478
+
479
+ if not frames:
480
+ logger.warning("Nenhum frame extraído do vídeo")
481
+ return video_path, metrics
482
+
483
+ metrics["video_duration"] = len(frames) / (fps or 2)
484
+ t0 = time.time()
485
+ detections = []
486
+ frames_processed = 0
487
+
488
+ # Processar um frame por vez para otimizar memória e permitir parada precoce
489
+ for frame_idx, frame in enumerate(frames):
490
+ frames_processed += 1
491
+
492
+ # Converter frame para RGB e pré-processar
493
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
494
+ image = Image.fromarray(frame_rgb)
495
+ image = self._preprocess_image(image)
496
+
497
+ # Detectar objetos com threshold direto
498
+ with torch.no_grad():
499
+ image_inputs = self.owlv2_processor(
500
+ images=image,
501
+ return_tensors="pt"
502
+ ).to(self.device)
503
+ inputs = {**image_inputs, **self.processed_text}
504
+ outputs = self.owlv2_model(**inputs)
505
+
506
+ target_sizes = torch.tensor([image.size[::-1]])
507
+ results = self.owlv2_processor.post_process_grounded_object_detection(
508
+ outputs=outputs,
509
+ target_sizes=target_sizes,
510
+ threshold=threshold # Aplicar threshold diretamente
511
+ )[0]
512
+
513
+ # Se encontrou alguma detecção acima do threshold
514
+ if len(results["scores"]) > 0:
515
+ # Pegar a detecção com maior confiança
516
+ max_score_idx = torch.argmax(results["scores"])
517
+ score = results["scores"][max_score_idx].item()
518
+ box = results["boxes"][max_score_idx].tolist()
519
+ label = results["labels"][max_score_idx].item()
520
+
521
+ detections.append({
522
+ "frame": frame_idx,
523
+ "confidence": score,
524
+ "box": [int(x) for x in box],
525
+ "label": self.text_queries[label]
526
+ })
527
+
528
+ # Atualizar métricas e parar o processamento
529
+ metrics["frames_processed_until_detection"] = frames_processed
530
+ metrics["analysis_time"] = time.time() - t0
531
+ metrics["total_time"] = time.time() - start_time
532
+ metrics["detections"] = detections
533
+ logger.info(f"Detecção encontrada após processar {frames_processed} frames")
534
+ return video_path, metrics
535
+
536
+ # Liberar memória a cada 10 frames
537
+ if frames_processed % 10 == 0:
538
+ gc.collect()
539
+
540
+ # Se chegou aqui, não encontrou nenhuma detecção
541
+ metrics["analysis_time"] = time.time() - t0
542
+ metrics["total_time"] = time.time() - start_time
543
+ metrics["frames_processed_until_detection"] = frames_processed
544
+ metrics["detections"] = detections
545
+ return video_path, metrics
546
+
547
+ except Exception as e:
548
+ logger.error(f"Erro ao processar vídeo: {str(e)}")
549
+ return video_path, {}
550
+
551
+ def extract_frames(self, video_path: str, fps: int = 2, resolution: int = 480) -> list:
552
+ """Extrai frames de um vídeo utilizando ffmpeg."""
553
+ frames = []
554
+ temp_dir = Path(tempfile.mkdtemp())
555
+ try:
556
+ threads = min(os.cpu_count(), 4) # Menor número de threads para CPU
557
+ cmd = [
558
+ 'ffmpeg', '-i', video_path,
559
+ '-threads', str(threads),
560
+ '-vf', (f'fps={fps},'
561
+ f'scale={resolution}:{resolution}:force_original_aspect_ratio=decrease:flags=lanczos,'
562
+ f'pad={resolution}:{resolution}:(ow-iw)/2:(oh-ih)/2'),
563
+ '-frame_pts', '1',
564
+ f'{temp_dir}/%d.jpg'
565
+ ]
566
+ subprocess.run(cmd, check=True, capture_output=True)
567
+ frame_files = sorted(temp_dir.glob('*.jpg'), key=lambda x: int(x.stem))
568
+ chunk_size = 50 # Menor chunk size para CPU
569
+ with ThreadPoolExecutor(max_workers=threads) as executor:
570
+ for i in range(0, len(frame_files), chunk_size):
571
+ chunk = frame_files[i:i + chunk_size]
572
+ chunk_frames = list(tqdm(
573
+ executor.map(lambda f: cv2.imread(str(f)), chunk),
574
+ desc=f"Carregando frames {i+1}-{min(i+chunk_size, len(frame_files))}",
575
+ total=len(chunk)
576
+ ))
577
+ frames.extend(chunk_frames)
578
+ if i % (chunk_size * 5) == 0:
579
+ gc.collect()
580
+ finally:
581
+ shutil.rmtree(temp_dir)
582
+ return frames
583
+
584
+ def clear_cache(self):
585
+ """Limpa cache e libera memória."""
586
+ self.result_cache.clear()
587
+ gc.collect()
src/domain/detectors/gpu.py ADDED
@@ -0,0 +1,396 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn.functional as F
3
+ import torch._dynamo
4
+ import logging
5
+ import os
6
+ import time
7
+ import gc
8
+ import numpy as np
9
+ import cv2
10
+ from PIL import Image
11
+ from transformers import Owlv2Processor, Owlv2ForObjectDetection
12
+ from .base import BaseDetector, BaseCache
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Configurações globais do PyTorch para otimização em GPU
17
+ torch.backends.cuda.matmul.allow_tf32 = True
18
+ torch.backends.cudnn.allow_tf32 = True
19
+ torch.backends.cudnn.benchmark = True
20
+ torch.backends.cuda.matmul.allow_fp16_reduced_precision_reduction = True
21
+ torch._dynamo.config.suppress_errors = True
22
+
23
+
24
+ class GPUCache(BaseCache):
25
+ """Cache otimizado para GPU."""
26
+ def __init__(self, max_size: int = 1000):
27
+ super().__init__(max_size)
28
+ self.device = torch.device('cuda')
29
+
30
+
31
+ class WeaponDetectorGPU(BaseDetector):
32
+ """Implementação GPU do detector de armas com otimizações para a última versão do OWLv2."""
33
+
34
+ def __init__(self):
35
+ """Inicializa variáveis básicas."""
36
+ super().__init__()
37
+ self.default_resolution = 640
38
+ self.amp_dtype = torch.float16
39
+ self.preprocess_stream = torch.cuda.Stream()
40
+ self.max_batch_size = 16 # Aumentado para 16
41
+ self.current_batch_size = 8 # Aumentado para 8
42
+ self.min_batch_size = 2
43
+
44
+ def _initialize(self):
45
+ """Inicializa o modelo e o processador para execução exclusiva em GPU."""
46
+ try:
47
+ # Configurar device
48
+ self.device = self._get_best_device()
49
+
50
+ # Diretório de cache para o modelo
51
+ cache_dir = os.getenv('CACHE_DIR', '/tmp/weapon_detection_cache')
52
+ os.makedirs(cache_dir, exist_ok=True)
53
+
54
+ # Limpar memória GPU
55
+ self._clear_gpu_memory()
56
+
57
+ logger.info("Carregando modelo e processador...")
58
+
59
+ # Carregar processador e modelo com otimizações
60
+ model_name = "google/owlv2-base-patch16"
61
+ self.owlv2_processor = Owlv2Processor.from_pretrained(
62
+ model_name,
63
+ cache_dir=cache_dir
64
+ )
65
+
66
+ # Configurações otimizadas para T4
67
+ self.owlv2_model = Owlv2ForObjectDetection.from_pretrained(
68
+ model_name,
69
+ cache_dir=cache_dir,
70
+ torch_dtype=self.amp_dtype,
71
+ device_map="auto",
72
+ low_cpu_mem_usage=True
73
+ ).to(self.device)
74
+
75
+ # Otimizar modelo para inferência
76
+ self.owlv2_model.eval()
77
+ torch.compile(self.owlv2_model) # Usar torch.compile para otimização
78
+
79
+ # Usar queries do método base
80
+ self.text_queries = self._get_detection_queries()
81
+ logger.info(f"Total de queries carregadas: {len(self.text_queries)}")
82
+
83
+ # Processar queries uma única vez com otimização de memória
84
+ with torch.cuda.amp.autocast(dtype=self.amp_dtype):
85
+ self.processed_text = self.owlv2_processor(
86
+ text=self.text_queries,
87
+ return_tensors="pt",
88
+ padding=True
89
+ )
90
+
91
+ self.processed_text = {
92
+ key: val.to(self.device, non_blocking=True)
93
+ for key, val in self.processed_text.items()
94
+ }
95
+
96
+ # Ajustar batch size baseado na memória disponível
97
+ self._adjust_batch_size()
98
+
99
+ logger.info(f"Inicialização GPU completa! Batch size inicial: {self.current_batch_size}")
100
+ self._initialized = True
101
+
102
+ except Exception as e:
103
+ logger.error(f"Erro na inicialização GPU: {str(e)}")
104
+ raise
105
+
106
+ def _adjust_batch_size(self):
107
+ """Ajusta o batch size baseado na memória disponível."""
108
+ try:
109
+ gpu_mem = torch.cuda.get_device_properties(0).total_memory
110
+ free_mem = torch.cuda.memory_reserved() - torch.cuda.memory_allocated()
111
+ mem_ratio = free_mem / gpu_mem
112
+
113
+ if mem_ratio < 0.2: # Menos de 20% livre
114
+ self.current_batch_size = max(self.min_batch_size, self.current_batch_size // 2)
115
+ elif mem_ratio > 0.4: # Mais de 40% livre
116
+ self.current_batch_size = min(self.max_batch_size, self.current_batch_size * 2)
117
+
118
+ logger.debug(f"Batch size ajustado para {self.current_batch_size} (Memória livre: {mem_ratio:.1%})")
119
+ except Exception as e:
120
+ logger.warning(f"Erro ao ajustar batch size: {str(e)}")
121
+ self.current_batch_size = self.min_batch_size
122
+
123
+ def detect_objects(self, image: Image.Image, threshold: float = 0.3) -> list:
124
+ """Detecta objetos em uma imagem utilizando a última versão do OWLv2."""
125
+ try:
126
+ self.threshold = threshold
127
+
128
+ # Pré-processar imagem
129
+ if image.mode != 'RGB':
130
+ image = image.convert('RGB')
131
+
132
+ # Processar imagem
133
+ image_inputs = self.owlv2_processor(
134
+ images=image,
135
+ return_tensors="pt"
136
+ )
137
+
138
+ image_inputs = {
139
+ key: val.to(self.device)
140
+ for key, val in image_inputs.items()
141
+ }
142
+
143
+ # Inferência
144
+ with torch.no_grad():
145
+ inputs = {**image_inputs, **self.processed_text}
146
+ outputs = self.owlv2_model(**inputs)
147
+
148
+ target_sizes = torch.tensor([image.size[::-1]], device=self.device)
149
+ results = self.owlv2_processor.post_process_grounded_object_detection(
150
+ outputs=outputs,
151
+ target_sizes=target_sizes,
152
+ threshold=threshold
153
+ )[0]
154
+
155
+ # Processar detecções
156
+ detections = []
157
+ if len(results["scores"]) > 0:
158
+ scores = results["scores"]
159
+ boxes = results["boxes"]
160
+ labels = results["labels"]
161
+
162
+ for score, box, label in zip(scores, boxes, labels):
163
+ if score.item() >= threshold:
164
+ detections.append({
165
+ "confidence": score.item(),
166
+ "box": [int(x) for x in box.tolist()],
167
+ "label": self.text_queries[label]
168
+ })
169
+
170
+ return detections
171
+
172
+ except Exception as e:
173
+ logger.error(f"Erro em detect_objects: {str(e)}")
174
+ return []
175
+
176
+ def process_video(self, video_path: str, fps: int = None, threshold: float = 0.3, resolution: int = 640) -> tuple:
177
+ """Processa um vídeo utilizando GPU com processamento em lote e otimizações para T4."""
178
+ try:
179
+ metrics = {
180
+ "total_time": 0,
181
+ "frame_extraction_time": 0,
182
+ "analysis_time": 0,
183
+ "frames_analyzed": 0,
184
+ "video_duration": 0,
185
+ "device_type": self.device.type,
186
+ "detections": [],
187
+ "technical": {
188
+ "model": "owlv2-base-patch16",
189
+ "input_size": f"{resolution}x{resolution}",
190
+ "threshold": threshold,
191
+ "batch_size": self.current_batch_size,
192
+ "gpu_memory": f"{torch.cuda.get_device_properties(0).total_memory / 1024**3:.1f}GB"
193
+ }
194
+ }
195
+
196
+ start_time = time.time()
197
+ frames = self.extract_frames(video_path, fps, resolution)
198
+ metrics["frame_extraction_time"] = time.time() - start_time
199
+ metrics["frames_analyzed"] = len(frames)
200
+
201
+ if not frames:
202
+ logger.warning("Nenhum frame extraído do vídeo")
203
+ return video_path, metrics
204
+
205
+ metrics["video_duration"] = len(frames) / (fps or 2)
206
+ analysis_start = time.time()
207
+
208
+ # Processar frames em lotes com ajuste dinâmico de batch size
209
+ for i in range(0, len(frames), self.current_batch_size):
210
+ try:
211
+ batch_frames = frames[i:i + self.current_batch_size]
212
+
213
+ # Pré-processamento assíncrono
214
+ with torch.cuda.stream(self.preprocess_stream):
215
+ batch_images = [
216
+ Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
217
+ for frame in batch_frames
218
+ ]
219
+
220
+ batch_inputs = self.owlv2_processor(
221
+ images=batch_images,
222
+ return_tensors="pt"
223
+ )
224
+
225
+ batch_inputs = {
226
+ key: val.to(self.device, non_blocking=True)
227
+ for key, val in batch_inputs.items()
228
+ }
229
+
230
+ # Expandir texto processado para o batch
231
+ batch_text = {
232
+ key: val.repeat(len(batch_images), 1)
233
+ for key, val in self.processed_text.items()
234
+ }
235
+
236
+ inputs = {**batch_inputs, **batch_text}
237
+
238
+ # Inferência com mixed precision
239
+ with torch.cuda.amp.autocast(dtype=self.amp_dtype):
240
+ with torch.no_grad():
241
+ outputs = self.owlv2_model(**inputs)
242
+
243
+ # Processar resultados
244
+ target_sizes = torch.tensor([[img.size[::-1] for img in batch_images]], device=self.device)
245
+ results = self.owlv2_processor.post_process_grounded_object_detection(
246
+ outputs=outputs,
247
+ target_sizes=target_sizes[0],
248
+ threshold=threshold
249
+ )
250
+
251
+ # Verificar detecções
252
+ for batch_idx, result in enumerate(results):
253
+ if len(result["scores"]) > 0:
254
+ frame_idx = i + batch_idx
255
+ max_score_idx = torch.argmax(result["scores"])
256
+ score = result["scores"][max_score_idx]
257
+
258
+ if score.item() >= threshold:
259
+ detection = {
260
+ "frame": frame_idx,
261
+ "confidence": score.item(),
262
+ "box": [int(x) for x in result["boxes"][max_score_idx].tolist()],
263
+ "label": self.text_queries[result["labels"][max_score_idx]]
264
+ }
265
+ metrics["detections"].append(detection)
266
+ metrics["analysis_time"] = time.time() - analysis_start
267
+ metrics["total_time"] = time.time() - start_time
268
+ return video_path, metrics
269
+
270
+ # Limpar memória e ajustar batch size periodicamente
271
+ if (i // self.current_batch_size) % 5 == 0:
272
+ self._clear_gpu_memory()
273
+ self._adjust_batch_size()
274
+
275
+ except RuntimeError as e:
276
+ if "out of memory" in str(e):
277
+ logger.warning("OOM detectado, reduzindo batch size")
278
+ self._clear_gpu_memory()
279
+ self.current_batch_size = max(self.min_batch_size, self.current_batch_size // 2)
280
+ continue
281
+ raise
282
+
283
+ metrics["analysis_time"] = time.time() - analysis_start
284
+ metrics["total_time"] = time.time() - start_time
285
+ return video_path, metrics
286
+
287
+ except Exception as e:
288
+ logger.error(f"Erro ao processar vídeo: {str(e)}")
289
+ return video_path, metrics
290
+
291
+ def _clear_gpu_memory(self):
292
+ """Limpa memória GPU de forma agressiva."""
293
+ try:
294
+ torch.cuda.empty_cache()
295
+ torch.cuda.synchronize()
296
+ gc.collect()
297
+ except Exception as e:
298
+ logger.error(f"Erro ao limpar memória GPU: {str(e)}")
299
+
300
+ def _get_best_device(self):
301
+ if not torch.cuda.is_available():
302
+ raise RuntimeError("CUDA não está disponível!")
303
+ return torch.device('cuda')
304
+
305
+ def _preprocess_image(self, image: Image.Image) -> Image.Image:
306
+ """Pré-processa a imagem com otimizações para GPU."""
307
+ try:
308
+ target_size = (self.default_resolution, self.default_resolution)
309
+ if image.mode != 'RGB':
310
+ image = image.convert('RGB')
311
+
312
+ if image.size != target_size:
313
+ ratio = min(target_size[0] / image.size[0], target_size[1] / image.size[1])
314
+ new_size = (int(image.size[0] * ratio), int(image.size[1] * ratio))
315
+
316
+ with torch.cuda.stream(self.preprocess_stream), torch.amp.autocast(device_type='cuda', dtype=self.amp_dtype):
317
+ img_tensor = torch.from_numpy(np.array(image)).permute(2, 0, 1).unsqueeze(0)
318
+ img_tensor = img_tensor.to(self.device, dtype=self.amp_dtype, non_blocking=True)
319
+ img_tensor = img_tensor / 255.0
320
+
321
+ mode = 'bilinear' if ratio < 1 else 'nearest'
322
+ img_tensor = F.interpolate(
323
+ img_tensor,
324
+ size=new_size,
325
+ mode=mode,
326
+ align_corners=False if mode == 'bilinear' else None
327
+ )
328
+
329
+ if new_size != target_size:
330
+ final_tensor = torch.zeros(
331
+ (1, 3, target_size[1], target_size[0]),
332
+ device=self.device,
333
+ dtype=self.amp_dtype
334
+ )
335
+ pad_left = (target_size[0] - new_size[0]) // 2
336
+ pad_top = (target_size[1] - new_size[1]) // 2
337
+ final_tensor[
338
+ :,
339
+ :,
340
+ pad_top:pad_top + new_size[1],
341
+ pad_left:pad_left + new_size[0]
342
+ ] = img_tensor
343
+
344
+ img_tensor = final_tensor
345
+
346
+ img_tensor = img_tensor.squeeze(0).permute(1, 2, 0).cpu()
347
+ image = Image.fromarray((img_tensor.numpy() * 255).astype(np.uint8))
348
+
349
+ return image
350
+
351
+ except Exception as e:
352
+ logger.error(f"Erro no pré-processamento: {str(e)}")
353
+ return image
354
+
355
+ def _get_memory_usage(self):
356
+ """Retorna o uso atual de memória GPU em porcentagem."""
357
+ try:
358
+ allocated = torch.cuda.memory_allocated()
359
+ reserved = torch.cuda.memory_reserved()
360
+ total = torch.cuda.get_device_properties(0).total_memory
361
+ return (allocated + reserved) / total * 100
362
+ except Exception as e:
363
+ logger.error(f"Erro ao obter uso de memória GPU: {str(e)}")
364
+ return 0
365
+
366
+ def _should_clear_cache(self):
367
+ """Determina se o cache deve ser limpo baseado no uso de memória."""
368
+ try:
369
+ memory_usage = self._get_memory_usage()
370
+ if memory_usage > 90:
371
+ return True
372
+ if memory_usage > 75 and not hasattr(self, '_last_cache_clear'):
373
+ return True
374
+ if hasattr(self, '_last_cache_clear'):
375
+ time_since_last_clear = time.time() - self._last_cache_clear
376
+ if memory_usage > 80 and time_since_last_clear > 300:
377
+ return True
378
+ return False
379
+ except Exception as e:
380
+ logger.error(f"Erro ao verificar necessidade de limpeza: {str(e)}")
381
+ return False
382
+
383
+ def clear_cache(self):
384
+ """Limpa o cache de resultados e libera memória quando necessário."""
385
+ try:
386
+ if self._should_clear_cache():
387
+ if hasattr(self, 'result_cache'):
388
+ self.result_cache.clear()
389
+ torch.cuda.empty_cache()
390
+ gc.collect()
391
+ self._last_cache_clear = time.time()
392
+ logger.info(f"Cache GPU limpo com sucesso. Uso de memória: {self._get_memory_usage():.1f}%")
393
+ else:
394
+ logger.debug("Limpeza de cache não necessária no momento")
395
+ except Exception as e:
396
+ logger.error(f"Erro ao limpar cache GPU: {str(e)}")
src/domain/entities/__init__.py ADDED
File without changes
src/domain/entities/detection.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from typing import List, Tuple, Optional
3
+
4
+ @dataclass
5
+ class Detection:
6
+ """Representa uma detecção de objeto perigoso."""
7
+ frame: int
8
+ confidence: float
9
+ label: str
10
+ box: List[int] # [x1, y1, x2, y2]
11
+ timestamp: float = 0.0
12
+
13
+ @dataclass
14
+ class DetectionResult:
15
+ """Resultado completo do processamento de vídeo."""
16
+ video_path: str
17
+ detections: List[Detection]
18
+ frames_analyzed: int
19
+ total_time: float
20
+ device_type: str
21
+ frame_extraction_time: float
22
+ analysis_time: float
src/domain/factories/__init__.py ADDED
File without changes
src/domain/factories/detector_factory.py ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import gc
4
+ import cv2
5
+ import json
6
+ import torch
7
+ import psutil
8
+ import shutil
9
+ import pickle
10
+ import hashlib
11
+ import tempfile
12
+ import logging
13
+ import subprocess
14
+ import numpy as np
15
+ import sys
16
+ from tqdm import tqdm
17
+ from pathlib import Path
18
+ from datetime import datetime
19
+ from concurrent.futures import ThreadPoolExecutor
20
+ from PIL import Image
21
+ from dotenv import load_dotenv
22
+ from transformers import Owlv2Processor, Owlv2ForObjectDetection
23
+ from typing import Optional
24
+ from src.domain.detectors.base import BaseDetector, BaseCache
25
+ from src.domain.detectors.gpu import WeaponDetectorGPU
26
+ from src.domain.detectors.cpu import WeaponDetectorCPU
27
+
28
+ # Carregar variáveis de ambiente
29
+ load_dotenv()
30
+
31
+ logging.basicConfig(level=logging.INFO)
32
+ logger = logging.getLogger(__name__)
33
+
34
+ class BaseCache:
35
+ """Cache base para armazenar resultados de detecção."""
36
+ def __init__(self, max_size: int = 1000):
37
+ self.cache = {}
38
+ self.max_size = max_size
39
+ self.hits = 0
40
+ self.misses = 0
41
+ self.last_access = {}
42
+
43
+ def get(self, image: np.ndarray) -> list:
44
+ try:
45
+ key = hashlib.blake2b(image.tobytes(), digest_size=16).hexdigest()
46
+ if key in self.cache:
47
+ self.hits += 1
48
+ self.last_access[key] = time.time()
49
+ return self.cache[key]
50
+ self.misses += 1
51
+ return None
52
+ except Exception as e:
53
+ logger.error(f"Erro ao recuperar do cache: {str(e)}")
54
+ return None
55
+
56
+ def put(self, image: np.ndarray, results: list):
57
+ try:
58
+ key = hashlib.blake2b(image.tobytes(), digest_size=16).hexdigest()
59
+ if len(self.cache) >= self.max_size:
60
+ oldest_key = min(self.last_access.items(), key=lambda x: x[1])[0]
61
+ del self.cache[oldest_key]
62
+ del self.last_access[oldest_key]
63
+ self.cache[key] = results
64
+ self.last_access[key] = time.time()
65
+ except Exception as e:
66
+ logger.error(f"Erro ao armazenar no cache: {str(e)}")
67
+
68
+ def clear(self):
69
+ """Limpa o cache e libera memória."""
70
+ self.cache.clear()
71
+ self.last_access.clear()
72
+ gc.collect()
73
+
74
+ def get_stats(self) -> dict:
75
+ total = self.hits + self.misses
76
+ hit_rate = (self.hits / total) * 100 if total > 0 else 0
77
+ return {
78
+ "cache_size": len(self.cache),
79
+ "max_size": self.max_size,
80
+ "hits": self.hits,
81
+ "misses": self.misses,
82
+ "hit_rate": f"{hit_rate:.2f}%",
83
+ "memory_usage": sum(sys.getsizeof(v) for v in self.cache.values())
84
+ }
85
+
86
+ class BaseWeaponDetector:
87
+ """Classe base abstrata para detecção de armas."""
88
+ def __init__(self):
89
+ """Inicialização básica comum a todos os detectores."""
90
+ self._initialized = False
91
+ self.device = self._get_best_device()
92
+ self._initialize()
93
+
94
+ def _check_initialized(self):
95
+ """Verifica se o detector está inicializado."""
96
+ if not self._initialized:
97
+ raise RuntimeError("Detector não está inicializado")
98
+
99
+ def clean_memory(self):
100
+ """Limpa memória não utilizada."""
101
+ try:
102
+ if torch.cuda.is_available():
103
+ torch.cuda.empty_cache()
104
+ logger.debug("Cache GPU limpo")
105
+ gc.collect()
106
+ logger.debug("Garbage collector executado")
107
+ except Exception as e:
108
+ logger.error(f"Erro ao limpar memória: {str(e)}")
109
+
110
+ def _get_best_device(self):
111
+ """Deve ser implementado nas classes filhas."""
112
+ raise NotImplementedError
113
+
114
+ def _initialize(self):
115
+ """Deve ser implementado nas classes filhas."""
116
+ raise NotImplementedError
117
+
118
+ def detect_objects(self, image, threshold=0.3):
119
+ """Deve ser implementado nas classes filhas."""
120
+ raise NotImplementedError
121
+
122
+ def process_video(self, video_path, fps=None, threshold=0.3, resolution=640):
123
+ """Deve ser implementado nas classes filhas."""
124
+ raise NotImplementedError
125
+
126
+ def _apply_nms(self, detections: list, iou_threshold: float = 0.5) -> list:
127
+ """Deve ser implementado nas classes filhas."""
128
+ raise NotImplementedError
129
+
130
+ def extract_frames(self, video_path: str, fps: int = 2, resolution: int = 640) -> list:
131
+ """Extrai frames de um vídeo utilizando ffmpeg."""
132
+ frames = []
133
+ temp_dir = Path(tempfile.mkdtemp())
134
+ try:
135
+ threads = min(os.cpu_count(), 8)
136
+ cmd = [
137
+ 'ffmpeg', '-i', video_path,
138
+ '-threads', str(threads),
139
+ '-vf', (f'fps={fps},'
140
+ f'scale={resolution}:{resolution}:force_original_aspect_ratio=decrease:flags=lanczos,'
141
+ f'pad={resolution}:{resolution}:(ow-iw)/2:(oh-ih)/2'),
142
+ '-frame_pts', '1',
143
+ f'{temp_dir}/%d.jpg'
144
+ ]
145
+ subprocess.run(cmd, check=True, capture_output=True)
146
+ frame_files = sorted(temp_dir.glob('*.jpg'), key=lambda x: int(x.stem))
147
+ chunk_size = 100
148
+ with ThreadPoolExecutor(max_workers=threads) as executor:
149
+ for i in range(0, len(frame_files), chunk_size):
150
+ chunk = frame_files[i:i + chunk_size]
151
+ chunk_frames = list(tqdm(
152
+ executor.map(lambda f: cv2.imread(str(f)), chunk),
153
+ desc=f"Carregando frames {i+1}-{min(i+chunk_size, len(frame_files))}",
154
+ total=len(chunk)
155
+ ))
156
+ frames.extend(chunk_frames)
157
+ if i % (chunk_size * 5) == 0:
158
+ gc.collect()
159
+ finally:
160
+ shutil.rmtree(temp_dir)
161
+ return frames
162
+
163
+ def _preprocess_image(self, image: Image.Image) -> Image.Image:
164
+ """Deve ser implementado nas classes filhas."""
165
+ raise NotImplementedError
166
+
167
+ def _update_frame_metrics(self, detections: list, frame_idx: int, metrics: dict):
168
+ """Atualiza as métricas para um conjunto de detecções em um frame."""
169
+ try:
170
+ for detection in detections:
171
+ self._update_detection_metrics(detection, metrics)
172
+ if isinstance(detection, dict):
173
+ metrics.setdefault("detections", []).append({
174
+ "frame": frame_idx,
175
+ "box": detection.get("box", []),
176
+ "confidence": detection.get("confidence", 0),
177
+ "label": detection.get("label", "unknown")
178
+ })
179
+ except Exception as e:
180
+ logger.error(f"Erro ao atualizar métricas do frame: {str(e)}")
181
+
182
+ def _update_detection_metrics(self, detection: dict, metrics: dict):
183
+ """Atualiza as métricas de detecção."""
184
+ try:
185
+ if not isinstance(detection, dict):
186
+ logger.warning(f"Detection não é um dicionário: {detection}")
187
+ return
188
+ confidence = detection.get("confidence", 0)
189
+ if not confidence:
190
+ return
191
+ if "detection_stats" not in metrics:
192
+ metrics["detection_stats"] = {
193
+ "total_detections": 0,
194
+ "avg_confidence": 0,
195
+ "confidence_distribution": {
196
+ "low": 0,
197
+ "medium": 0,
198
+ "high": 0
199
+ }
200
+ }
201
+ stats = metrics["detection_stats"]
202
+ stats["total_detections"] += 1
203
+ if confidence < 0.5:
204
+ stats["confidence_distribution"]["low"] += 1
205
+ elif confidence < 0.7:
206
+ stats["confidence_distribution"]["medium"] += 1
207
+ else:
208
+ stats["confidence_distribution"]["high"] += 1
209
+ n = stats["total_detections"]
210
+ old_avg = stats["avg_confidence"]
211
+ stats["avg_confidence"] = (old_avg * (n - 1) + confidence) / n
212
+ except Exception as e:
213
+ logger.error(f"Error updating metrics: {str(e)}")
214
+
215
+ def clear_cache(self):
216
+ """Deve ser implementado nas classes filhas."""
217
+ raise NotImplementedError
218
+
219
+ class ResultCache(BaseCache):
220
+ """
221
+ Cache otimizado para armazenar resultados de detecção.
222
+ """
223
+ def __init__(self, max_size: int = 1000):
224
+ super().__init__(max_size)
225
+
226
+ class WeaponDetector:
227
+ """Implementação do Factory Pattern para criar a instância apropriada do detector."""
228
+ _instance = None
229
+
230
+ def __new__(cls):
231
+ try:
232
+ if cls._instance is None:
233
+ if torch.cuda.is_available():
234
+ cls._instance = WeaponDetectorGPU()
235
+ logger.info("Detector GPU criado")
236
+ else:
237
+ cls._instance = WeaponDetectorCPU()
238
+ logger.info("Detector CPU criado")
239
+
240
+ # Garantir que o detector foi inicializado corretamente
241
+ if not cls._instance:
242
+ raise RuntimeError("Falha ao criar instância do detector")
243
+
244
+ # Inicializar o detector
245
+ if hasattr(cls._instance, 'initialize'):
246
+ cls._instance.initialize()
247
+
248
+ # Verificar se os métodos necessários existem
249
+ required_methods = ['process_video', 'clean_memory', 'detect_objects']
250
+ for method in required_methods:
251
+ if not hasattr(cls._instance, method):
252
+ raise RuntimeError(f"Detector não possui método obrigatório: {method}")
253
+
254
+ return cls._instance
255
+
256
+ except Exception as e:
257
+ logger.error(f"Erro ao criar detector: {str(e)}")
258
+ raise
259
+
260
+ @classmethod
261
+ def get_instance(cls):
262
+ """Retorna a instância existente ou cria uma nova."""
263
+ if cls._instance is None:
264
+ return cls()
265
+ return cls._instance
266
+
267
+ def detect_objects(self, image: Image.Image, threshold: float = 0.3) -> list:
268
+ """Detecta objetos em uma imagem."""
269
+ if not self._instance:
270
+ raise RuntimeError("Detector não inicializado")
271
+ return self._instance.detect_objects(image, threshold)
272
+
273
+ def extract_frames(self, video_path: str, fps: int = 2, resolution: int = 640) -> list:
274
+ """Extrai frames de um vídeo."""
275
+ if not self._instance:
276
+ raise RuntimeError("Detector não inicializado")
277
+ return self._instance.extract_frames(video_path, fps, resolution)
278
+
279
+ def process_video(self, video_path: str, fps: int = None, threshold: float = 0.3, resolution: int = 640) -> tuple:
280
+ """Processa o vídeo e retorna os detalhes técnicos e as detecções."""
281
+ if not self._instance:
282
+ raise RuntimeError("Detector não inicializado")
283
+ return self._instance.process_video(video_path, fps, threshold, resolution)
284
+
285
+ def clean_memory(self):
286
+ """Limpa todo o cache do sistema."""
287
+ if not self._instance:
288
+ return
289
+ if hasattr(self._instance, 'clear_cache'):
290
+ self._instance.clear_cache()
291
+ if hasattr(self._instance, 'clean_memory'):
292
+ self._instance.clean_memory()
293
+ # Forçar limpeza de memória
294
+ gc.collect()
295
+ if torch.cuda.is_available():
296
+ torch.cuda.empty_cache()
src/domain/interfaces/__init__.py ADDED
File without changes
src/domain/interfaces/detector.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC, abstractmethod
2
+ from typing import List, Tuple, Dict, Any
3
+ from ..entities.detection import DetectionResult
4
+
5
+ class DetectorInterface(ABC):
6
+ """Interface base para detectores de objetos perigosos."""
7
+
8
+ @abstractmethod
9
+ def process_video(self, video_path: str, fps: int, threshold: float, resolution: int) -> Tuple[str, DetectionResult]:
10
+ """Processa um vídeo e retorna as detecções encontradas."""
11
+ pass
12
+
13
+ @abstractmethod
14
+ def clean_memory(self) -> None:
15
+ """Limpa a memória utilizada pelo detector."""
16
+ pass
17
+
18
+ @abstractmethod
19
+ def get_device_info(self) -> Dict[str, Any]:
20
+ """Retorna informações sobre o dispositivo em uso."""
21
+ pass
22
+
23
+ @abstractmethod
24
+ def get_cache_stats(self) -> Dict[str, Any]:
25
+ """Retorna estatísticas do cache."""
26
+ pass
src/domain/interfaces/notification.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC, abstractmethod
2
+ from typing import Dict, Any, List
3
+
4
+ class NotificationService(ABC):
5
+ """Interface base para serviços de notificação."""
6
+
7
+ @abstractmethod
8
+ def send_notification(self, detection_data: Dict[str, Any], recipient: str) -> bool:
9
+ """Envia notificação usando o serviço específico."""
10
+ pass
11
+
12
+ class NotificationFactory(ABC):
13
+ """Interface para fábrica de serviços de notificação."""
14
+
15
+ @abstractmethod
16
+ def create_service(self, service_type: str) -> NotificationService:
17
+ """Cria uma instância do serviço de notificação especificado."""
18
+ pass
19
+
20
+ @abstractmethod
21
+ def get_available_services(self) -> List[str]:
22
+ """Retorna lista de serviços de notificação disponíveis."""
23
+ pass
src/domain/repositories/__init__.py ADDED
File without changes
src/infrastructure/__init__.py ADDED
File without changes
src/infrastructure/services/__init__.py ADDED
File without changes
src/infrastructure/services/notification_services.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ from typing import Dict, Any
4
+ from sendgrid import SendGridAPIClient
5
+ from sendgrid.helpers.mail import Mail, Email, To, Content
6
+ from src.domain.interfaces.notification import NotificationService, NotificationFactory
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ class EmailNotification(NotificationService):
11
+ def send_notification(self, detection_data: Dict[str, Any], recipient: str) -> bool:
12
+ try:
13
+ # Verificar se há detecções
14
+ if not detection_data.get("detections"):
15
+ logger.info("Nenhuma detecção para notificar")
16
+ return True # Retorna True pois não é um erro
17
+
18
+ sender_email = os.getenv('NOTIFICATION_EMAIL')
19
+ sendgrid_api_key = os.getenv('SENDGRID_API_KEY')
20
+
21
+ if not sender_email:
22
+ logger.error("NOTIFICATION_EMAIL não configurado")
23
+ return False
24
+
25
+ if not sendgrid_api_key:
26
+ logger.error("SENDGRID_API_KEY não configurada")
27
+ return False
28
+
29
+ if not recipient:
30
+ logger.error("Destinatário de e-mail não fornecido")
31
+ return False
32
+
33
+ body = self._format_email_body(detection_data)
34
+
35
+ message = Mail(
36
+ from_email=sender_email,
37
+ to_emails=recipient,
38
+ subject='🚨 ALERTA DE SEGURANÇA - Detecção de Risco',
39
+ html_content=f'<pre style="font-family: monospace;">{body}</pre>'
40
+ )
41
+
42
+ try:
43
+ sg = SendGridAPIClient(sendgrid_api_key)
44
+ response = sg.send(message)
45
+ success = response.status_code == 202
46
+
47
+ if success:
48
+ logger.info(f"E-mail enviado com sucesso para {recipient}")
49
+ logger.debug(f"Status: {response.status_code}")
50
+ logger.debug(f"Body: {response.body}")
51
+ logger.debug(f"Headers: {response.headers}")
52
+ else:
53
+ logger.error(f"Erro ao enviar e-mail. Status code: {response.status_code}")
54
+
55
+ return success
56
+
57
+ except Exception as e:
58
+ logger.error(f"Erro ao enviar e-mail via SendGrid: {str(e)}")
59
+ return False
60
+
61
+ except Exception as e:
62
+ logger.error(f"Erro no serviço de e-mail: {str(e)}")
63
+ return False
64
+
65
+ def _format_email_body(self, detection_data: Dict[str, Any]) -> str:
66
+ """Formata o corpo do e-mail com os dados da detecção."""
67
+ try:
68
+ detections = detection_data.get("detections", [])
69
+ if not detections:
70
+ return "Nenhuma detecção encontrada no vídeo."
71
+
72
+ body = """
73
+ ⚠️ ALERTA DE SEGURANÇA ⚠️
74
+
75
+ Uma detecção de risco foi identificada:
76
+
77
+ """
78
+ # Adicionar informações da primeira detecção
79
+ first_detection = detections[0]
80
+ body += f"""📹 Detecção:
81
+ - Objeto: {first_detection.get('label', 'Desconhecido')}
82
+ - Confiança: {first_detection.get('confidence', 0):.2%}
83
+ - Timestamp: {first_detection.get('timestamp', 0):.2f}s
84
+
85
+ """
86
+
87
+ # Adicionar informações técnicas
88
+ if "technical" in detection_data:
89
+ tech = detection_data["technical"]
90
+ body += f"""Informações Técnicas:
91
+ - Threshold: {tech.get('threshold', 'N/A')}
92
+ - FPS: {tech.get('fps', 'N/A')}
93
+ - Resolução: {tech.get('resolution', 'N/A')}
94
+ """
95
+
96
+ body += """
97
+ --
98
+ Este é um e-mail automático enviado pelo Sistema de Detecção de Riscos.
99
+ Não responda este e-mail.
100
+ """
101
+
102
+ return body
103
+
104
+ except Exception as e:
105
+ logger.error(f"Erro ao formatar e-mail: {str(e)}")
106
+ return "Erro ao formatar dados da detecção."
107
+
108
+ class NotificationServiceFactory(NotificationFactory):
109
+ def __init__(self):
110
+ self._services = {'email': EmailNotification()}
111
+
112
+ def create_service(self, service_type: str) -> NotificationService:
113
+ return self._services.get(service_type)
114
+
115
+ def get_available_services(self) -> list:
116
+ return list(self._services.keys())
src/infrastructure/services/weapon_detector.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from typing import Tuple
3
+ from src.domain.interfaces.detector import DetectorInterface
4
+ from src.domain.entities.detection import Detection, DetectionResult
5
+ from src.domain.factories.detector_factory import WeaponDetector
6
+ from src.domain.detectors.gpu import WeaponDetectorGPU
7
+ from src.domain.detectors.cpu import WeaponDetectorCPU
8
+ import logging
9
+ import gc
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class WeaponDetectorService(DetectorInterface):
14
+ """Adaptador que conecta os detectores do domínio com a infraestrutura externa."""
15
+
16
+ def __init__(self):
17
+ try:
18
+ # Usar o Factory Pattern do domínio para criar o detector apropriado
19
+ self.detector = WeaponDetector.get_instance() # Usar get_instance ao invés do construtor direto
20
+ if not self.detector:
21
+ raise RuntimeError("Falha ao criar o detector")
22
+
23
+ self.device_type = "GPU" if torch.cuda.is_available() else "CPU"
24
+ logger.info(f"Detector inicializado em modo {self.device_type}")
25
+
26
+ # Manter referência à implementação específica para otimizações
27
+ if hasattr(self.detector, '_instance') and self.detector._instance is not None:
28
+ self._specific_detector = self.detector._instance
29
+ else:
30
+ self._specific_detector = self.detector
31
+
32
+ # Verificar se o detector foi inicializado corretamente
33
+ if not hasattr(self._specific_detector, 'process_video'):
34
+ raise RuntimeError("Detector não possui método process_video")
35
+
36
+ # Garantir que o detector está inicializado
37
+ if hasattr(self._specific_detector, 'initialize'):
38
+ self._specific_detector.initialize()
39
+
40
+ except Exception as e:
41
+ logger.error(f"Erro ao inicializar WeaponDetectorService: {str(e)}")
42
+ raise RuntimeError(f"Falha na inicialização do detector: {str(e)}")
43
+
44
+ def process_video(
45
+ self,
46
+ video_path: str,
47
+ fps: int,
48
+ threshold: float,
49
+ resolution: int
50
+ ) -> Tuple[str, DetectionResult]:
51
+ """Processa o vídeo usando o detector apropriado."""
52
+ try:
53
+ if not self._specific_detector:
54
+ raise RuntimeError("Detector não inicializado")
55
+
56
+ # Garantir que o detector está inicializado
57
+ if hasattr(self._specific_detector, 'initialize'):
58
+ self._specific_detector.initialize()
59
+
60
+ output_path, metrics = self._specific_detector.process_video(
61
+ video_path,
62
+ fps=fps,
63
+ threshold=threshold,
64
+ resolution=resolution
65
+ )
66
+
67
+ if not metrics:
68
+ logger.warning("Nenhuma métrica retornada pelo detector")
69
+ metrics = {}
70
+
71
+ # Converter detecções para entidades do domínio
72
+ detections = []
73
+ for d in metrics.get('detections', []):
74
+ try:
75
+ detections.append(Detection(
76
+ frame=d.get('frame', 0),
77
+ confidence=d.get('confidence', 0.0),
78
+ label=d.get('label', 'unknown'),
79
+ box=d.get('box', [0, 0, 0, 0]),
80
+ timestamp=d.get('frame', 0) / fps if fps else 0
81
+ ))
82
+ except Exception as e:
83
+ logger.error(f"Erro ao processar detecção: {str(e)}")
84
+
85
+ result = DetectionResult(
86
+ video_path=output_path or video_path,
87
+ detections=detections,
88
+ frames_analyzed=metrics.get('frames_analyzed', 0),
89
+ total_time=metrics.get('total_time', 0.0),
90
+ device_type=self.device_type,
91
+ frame_extraction_time=metrics.get('frame_extraction_time', 0.0),
92
+ analysis_time=metrics.get('analysis_time', 0.0)
93
+ )
94
+
95
+ return output_path or video_path, result
96
+
97
+ except Exception as e:
98
+ logger.error(f"Erro ao processar vídeo: {str(e)}")
99
+ empty_result = DetectionResult(
100
+ video_path=video_path,
101
+ detections=[],
102
+ frames_analyzed=0,
103
+ total_time=0.0,
104
+ device_type=self.device_type,
105
+ frame_extraction_time=0.0,
106
+ analysis_time=0.0
107
+ )
108
+ return video_path, empty_result
109
+
110
+ def clean_memory(self) -> None:
111
+ """Limpa a memória do detector."""
112
+ try:
113
+ if not self._specific_detector:
114
+ logger.warning("Nenhum detector específico para limpar memória")
115
+ return
116
+
117
+ if hasattr(self._specific_detector, 'clear_cache'):
118
+ self._specific_detector.clear_cache()
119
+
120
+ if hasattr(self._specific_detector, 'clean_memory'):
121
+ self._specific_detector.clean_memory()
122
+
123
+ # Forçar coleta de lixo
124
+ gc.collect()
125
+ if torch.cuda.is_available():
126
+ torch.cuda.empty_cache()
127
+
128
+ except Exception as e:
129
+ logger.error(f"Erro ao limpar memória: {str(e)}")
130
+
131
+ def get_device_info(self) -> dict:
132
+ """Retorna informações detalhadas sobre o dispositivo em uso."""
133
+ try:
134
+ if not self._specific_detector:
135
+ return {
136
+ "type": self.device_type,
137
+ "memory_total": 0,
138
+ "memory_used": 0
139
+ }
140
+
141
+ if isinstance(self._specific_detector, WeaponDetectorGPU):
142
+ return {
143
+ "type": "GPU",
144
+ "name": torch.cuda.get_device_name(0),
145
+ "memory_total": torch.cuda.get_device_properties(0).total_memory,
146
+ "memory_used": torch.cuda.memory_allocated(),
147
+ "memory_cached": torch.cuda.memory_reserved()
148
+ }
149
+ else:
150
+ import psutil
151
+ return {
152
+ "type": "CPU",
153
+ "threads": psutil.cpu_count(),
154
+ "memory_total": psutil.virtual_memory().total,
155
+ "memory_used": psutil.virtual_memory().used
156
+ }
157
+ except Exception as e:
158
+ logger.error(f"Erro ao obter informações do dispositivo: {str(e)}")
159
+ return {
160
+ "type": self.device_type,
161
+ "memory_total": 0,
162
+ "memory_used": 0
163
+ }
164
+
165
+ def get_cache_stats(self) -> dict:
166
+ """Retorna estatísticas do cache se disponível."""
167
+ try:
168
+ if not self._specific_detector:
169
+ return self._get_empty_cache_stats()
170
+
171
+ if (hasattr(self._specific_detector, 'result_cache') and
172
+ self._specific_detector.result_cache is not None):
173
+ return self._specific_detector.result_cache.get_stats()
174
+
175
+ return self._get_empty_cache_stats()
176
+
177
+ except Exception as e:
178
+ logger.error(f"Erro ao obter estatísticas do cache: {str(e)}")
179
+ return self._get_empty_cache_stats()
180
+
181
+ def _get_empty_cache_stats(self) -> dict:
182
+ """Retorna estatísticas vazias do cache."""
183
+ return {
184
+ "cache_size": 0,
185
+ "max_size": 0,
186
+ "hits": 0,
187
+ "misses": 0,
188
+ "hit_rate": "0.00%",
189
+ "memory_usage": 0
190
+ }
src/main.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+ from src.presentation.web.gradio_interface import GradioInterface
4
+ import logging
5
+ import torch
6
+
7
+ # Configurar logging
8
+ logging.basicConfig(
9
+ level=logging.INFO,
10
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
11
+ )
12
+ logger = logging.getLogger(__name__)
13
+
14
+ def main():
15
+ """Função principal que inicia a aplicação."""
16
+ try:
17
+ # Verificar se está rodando no Hugging Face
18
+ IS_HUGGINGFACE = os.getenv('SPACE_ID') is not None
19
+
20
+ # Carregar configurações do ambiente apropriado
21
+ if IS_HUGGINGFACE:
22
+ load_dotenv('.env.huggingface')
23
+ logger.info("Ambiente HuggingFace detectado")
24
+ else:
25
+ load_dotenv('.env')
26
+ logger.info("Ambiente local detectado")
27
+
28
+ # Criar e configurar interface
29
+ interface = GradioInterface()
30
+ demo = interface.create_interface()
31
+
32
+ if IS_HUGGINGFACE:
33
+ # Calcular número ideal de workers baseado na GPU
34
+ if torch.cuda.is_available():
35
+ gpu_mem = torch.cuda.get_device_properties(0).total_memory / (1024**3) # em GB
36
+ max_concurrent = min(2, int(gpu_mem / 8)) # 8GB por worker
37
+ logger.info(f"GPU Memory: {gpu_mem:.1f}GB, Max Concurrent: {max_concurrent}")
38
+ else:
39
+ max_concurrent = 1
40
+
41
+ # Primeiro configurar a fila
42
+ demo = demo.queue(
43
+ max_size=16, # Aumentado para corresponder ao max_batch_size
44
+ concurrency_count=max_concurrent, # Baseado na memória GPU
45
+ status_update_rate=10, # Atualizações mais frequentes
46
+ api_open=False,
47
+ max_batch_size=16 # Aumentado para corresponder ao detector
48
+ )
49
+ # Depois fazer o launch
50
+ demo.launch(
51
+ server_name="0.0.0.0",
52
+ server_port=7860,
53
+ share=False,
54
+ max_threads=4 # Limitar threads da CPU
55
+ )
56
+ else:
57
+ # Ambiente local - apenas launch direto
58
+ demo.launch(
59
+ server_name="0.0.0.0",
60
+ server_port=7860,
61
+ share=True
62
+ )
63
+
64
+ except Exception as e:
65
+ logger.error(f"Erro ao iniciar aplicação: {str(e)}")
66
+ raise
67
+
68
+ if __name__ == "__main__":
69
+ main()
src/presentation/__init__.py ADDED
File without changes
src/presentation/interfaces/__init__.py ADDED
File without changes
src/presentation/web/__init__.py ADDED
File without changes
src/presentation/web/gradio_interface.py ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import os
3
+ from typing import Tuple, Any
4
+ from pathlib import Path
5
+ from src.application.use_cases.process_video import ProcessVideoUseCase, ProcessVideoRequest
6
+ from src.infrastructure.services.weapon_detector import WeaponDetectorService
7
+ from src.infrastructure.services.notification_services import NotificationServiceFactory
8
+
9
+ class GradioInterface:
10
+ """Interface Gradio usando Clean Architecture."""
11
+
12
+ def __init__(self):
13
+ self.detector = WeaponDetectorService()
14
+ self.notification_factory = NotificationServiceFactory()
15
+ self.default_fps = 2 if self.detector.device_type == "GPU" else 1
16
+ self.default_resolution = "640" if self.detector.device_type == "GPU" else "480"
17
+
18
+ self.use_case = ProcessVideoUseCase(
19
+ detector=self.detector,
20
+ notification_factory=self.notification_factory,
21
+ default_fps=self.default_fps,
22
+ default_resolution=int(self.default_resolution)
23
+ )
24
+
25
+ def list_sample_videos(self) -> list:
26
+ """Lista os vídeos de exemplo na pasta videos."""
27
+ video_dir = Path("videos")
28
+ if not video_dir.exists():
29
+ os.makedirs(video_dir)
30
+ return []
31
+
32
+ video_extensions = ['.mp4', '.avi', '.mov', '.mkv']
33
+ videos = []
34
+
35
+ # Procurar em subdiretórios específicos
36
+ for status_dir in ['seguro', 'risco_detectado']:
37
+ dir_path = video_dir / status_dir
38
+ if dir_path.exists():
39
+ for ext in video_extensions:
40
+ for video_path in dir_path.glob(f'*{ext}'):
41
+ videos.append({
42
+ 'path': str(video_path),
43
+ 'name': video_path.name,
44
+ 'ground_truth': '✅ SEGURO (Ground Truth)' if status_dir == 'seguro' else '⚠️ RISCO DETECTADO (Ground Truth)'
45
+ })
46
+
47
+ return videos
48
+
49
+ def load_sample_video(self, video_path: str) -> str:
50
+ """Carrega um vídeo de exemplo."""
51
+ try:
52
+ return video_path
53
+ except Exception as e:
54
+ return None
55
+
56
+ def create_interface(self) -> gr.Blocks:
57
+ """Cria a interface Gradio."""
58
+ title = "Detector de Riscos em Vídeos"
59
+ sample_videos = self.list_sample_videos()
60
+
61
+ with gr.Blocks(
62
+ title=title,
63
+ theme=gr.themes.Ocean(),
64
+ css="footer {display: none !important}"
65
+ ) as demo:
66
+ gr.Markdown(f"""# 🚨 {title}
67
+
68
+ Faça upload de um vídeo para detectar objetos perigosos.
69
+ Opcionalmente, configure notificações para receber alertas em caso de detecções.
70
+
71
+ **Importante para melhor performance:**
72
+ - Vídeos de até 60 segundos
73
+ - FPS entre 1-2 para análise com maior performance
74
+ - FPS maior que 2 para análise com maior precisão
75
+ """)
76
+ with gr.Group():
77
+ gr.Markdown("""### Configuração de Processamento""")
78
+ with gr.Row():
79
+ threshold = gr.Slider(
80
+ minimum=0.1,
81
+ maximum=1.0,
82
+ value=0.5,
83
+ step=0.1,
84
+ label="Limiar de Detecção",
85
+ )
86
+ fps = gr.Slider(
87
+ minimum=1,
88
+ maximum=5,
89
+ value=self.default_fps,
90
+ step=1,
91
+ label="Frames por Segundo",
92
+ )
93
+ resolution = gr.Radio(
94
+ choices=["480", "640", "768"],
95
+ value=self.default_resolution,
96
+ label="Resolução de Processamento",
97
+ )
98
+ with gr.Group():
99
+ gr.Markdown("""### Configuração de Notificações de Detecção (Opcional)""")
100
+ with gr.Row():
101
+ notification_type = gr.Radio(
102
+ choices=self.notification_factory.get_available_services(),
103
+ value="email",
104
+ label="Tipo de Notificação",
105
+ interactive=True,
106
+ )
107
+ notification_target = gr.Textbox(
108
+ label="Destino da Notificação (E-mail)",
109
+ placeholder="[email protected]",
110
+ )
111
+ with gr.Row():
112
+ with gr.Column(scale=2):
113
+ input_video = gr.Video(
114
+ label="Vídeo de Entrada",
115
+ format="mp4",
116
+ interactive=True,
117
+ height=400
118
+ )
119
+
120
+ submit_btn = gr.Button(
121
+ "Analisar Vídeo",
122
+ variant="primary",
123
+ scale=2
124
+ )
125
+ with gr.Column(scale=1):
126
+ status = gr.Textbox(
127
+ label="Status da Detecção",
128
+ lines=4,
129
+ show_copy_button=True
130
+ )
131
+ with gr.Accordion("Detalhes Técnicos", open=False):
132
+ json_output = gr.JSON(
133
+ label="Detalhes Técnicos",
134
+ )
135
+
136
+ # Informações adicionais
137
+ with gr.Accordion("Informações Adicionais", open=False):
138
+ gr.Markdown("""
139
+ ### Sobre o Detector
140
+ Este sistema utiliza um modelo de IA avançado para detectar objetos perigosos em vídeos.
141
+
142
+ ### Tipos de Objetos Detectados
143
+ - Armas de fogo (pistolas, rifles, etc.)
144
+ - Armas brancas (facas, canivetes, etc.)
145
+ - Objetos perigosos (bastões, objetos pontiagudos, etc.)
146
+
147
+ ### Recomendações
148
+ - Use vídeos com boa iluminação
149
+ - Evite vídeos muito longos
150
+ - Mantenha os objetos visíveis e em foco
151
+ """)
152
+ # Vídeos de exemplo
153
+ if sample_videos:
154
+ with gr.Group():
155
+ gr.Markdown("### Vídeos de Exemplo")
156
+ with gr.Row():
157
+ with gr.Column(scale=3):
158
+ gr.Markdown("#### Vídeo")
159
+ with gr.Column(scale=2):
160
+ gr.Markdown("#### Status Real")
161
+ with gr.Column(scale=1):
162
+ gr.Markdown("#### Ação")
163
+
164
+ for video in sample_videos:
165
+ with gr.Row():
166
+ with gr.Column(scale=3):
167
+ gr.Video(
168
+ value=video['path'],
169
+ format="mp4",
170
+ height=150,
171
+ interactive=False,
172
+ show_label=False
173
+ )
174
+ with gr.Column(scale=2, min_width=200):
175
+ gr.Markdown(video['ground_truth'])
176
+ with gr.Column(scale=1, min_width=100):
177
+ gr.Button(
178
+ "📥 Carregar",
179
+ size="sm"
180
+ ).click(
181
+ fn=self.load_sample_video,
182
+ inputs=[gr.State(video['path'])],
183
+ outputs=[input_video]
184
+ )
185
+
186
+
187
+
188
+ # Configurar callback do botão
189
+ submit_btn.click(
190
+ fn=lambda *args: self._process_video(*args),
191
+ inputs=[
192
+ input_video,
193
+ threshold,
194
+ fps,
195
+ resolution,
196
+ notification_type,
197
+ notification_target
198
+ ],
199
+ outputs=[status, json_output]
200
+ )
201
+
202
+ return demo
203
+
204
+ def _process_video(
205
+ self,
206
+ video_path: str,
207
+ threshold: float = 0.5,
208
+ fps: int = None,
209
+ resolution: str = None,
210
+ notification_type: str = None,
211
+ notification_target: str = None
212
+ ) -> Tuple[str, Any]:
213
+ """Processa o vídeo usando o caso de uso."""
214
+ if not video_path:
215
+ return "Erro: Nenhum vídeo fornecido", {}
216
+
217
+ # Usar valores padrão se não especificados
218
+ fps = fps or self.default_fps
219
+ resolution = resolution or self.default_resolution
220
+
221
+ request = ProcessVideoRequest(
222
+ video_path=video_path,
223
+ threshold=threshold,
224
+ fps=fps,
225
+ resolution=int(resolution),
226
+ notification_type=notification_type,
227
+ notification_target=notification_target
228
+ )
229
+
230
+ response = self.use_case.execute(request)
231
+
232
+ return (
233
+ response.status_message,
234
+ response.detection_result.__dict__
235
+ )
tests/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
tests/conftest.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ import os
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ # Adiciona o diretório src ao PYTHONPATH
7
+ sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
8
+
9
+ @pytest.fixture
10
+ def sample_video_path():
11
+ """Retorna o caminho para um vídeo de teste"""
12
+ return str(Path(__file__).parent / "fixtures" / "sample_video.mp4")
13
+
14
+ @pytest.fixture
15
+ def mock_weapon_detector_service():
16
+ """Mock do serviço de detecção de armas"""
17
+ class MockWeaponDetectorService:
18
+ def detect(self, video_path, threshold=0.5):
19
+ return {
20
+ "detections": [
21
+ {"label": "weapon", "confidence": 0.8, "bbox": [10, 10, 100, 100]},
22
+ ],
23
+ "frame_count": 30,
24
+ "processing_time": 1.5
25
+ }
26
+
27
+ return MockWeaponDetectorService()
28
+
29
+ @pytest.fixture
30
+ def mock_notification_service():
31
+ """Mock do serviço de notificação"""
32
+ class MockNotificationService:
33
+ def send_notification(self, message, level="info"):
34
+ return {"status": "success", "message": message}
35
+
36
+ return MockNotificationService()
37
+
38
+ @pytest.fixture
39
+ def mock_system_monitor():
40
+ """Mock do monitor de sistema"""
41
+ class MockSystemMonitor:
42
+ def get_system_info(self):
43
+ return {
44
+ "cpu_percent": 50.0,
45
+ "memory_percent": 60.0,
46
+ "gpu_info": {"name": "Test GPU", "memory_used": 1000}
47
+ }
48
+
49
+ return MockSystemMonitor()
tests/integration/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
tests/integration/test_gradio_interface.py ADDED
@@ -0,0 +1 @@
 
 
1
+
tests/unit/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
tests/unit/test_weapon_detector_service.py ADDED
@@ -0,0 +1 @@
 
 
1
+