Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- .gitattributes +2 -0
- .gitignore +129 -214
- Dockerfile +22 -22
- LICENSE +21 -21
- Procfile +1 -0
- README.md +350 -9
- api/auth.py +118 -0
- api/main.py +271 -216
- app.py +1206 -521
- config.py +35 -35
- core/analytics.py +240 -240
- core/detection.py +172 -172
- core/emotion.py +95 -0
- core/nlquery.py +79 -0
- core/osint.py +271 -271
- core/reid.py +76 -0
- core/reporter.py +170 -0
- core/tracker.py +249 -249
- core/weapon.py +72 -0
- requirements.txt +20 -18
- test_reid.py +37 -0
- test_weapon.py +26 -0
- test_weapon2.py +21 -0
.gitattributes
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
.gitignore
CHANGED
|
@@ -1,214 +1,129 @@
|
|
| 1 |
-
# Byte-compiled / optimized / DLL files
|
| 2 |
-
__pycache__/
|
| 3 |
-
*.py[codz]
|
| 4 |
-
*$py.class
|
| 5 |
-
|
| 6 |
-
# C extensions
|
| 7 |
-
*.so
|
| 8 |
-
|
| 9 |
-
# Distribution / packaging
|
| 10 |
-
.Python
|
| 11 |
-
build/
|
| 12 |
-
develop-eggs/
|
| 13 |
-
dist/
|
| 14 |
-
downloads/
|
| 15 |
-
eggs/
|
| 16 |
-
.eggs/
|
| 17 |
-
lib/
|
| 18 |
-
lib64/
|
| 19 |
-
parts/
|
| 20 |
-
sdist/
|
| 21 |
-
var/
|
| 22 |
-
wheels/
|
| 23 |
-
share/python-wheels/
|
| 24 |
-
*.egg-info/
|
| 25 |
-
.installed.cfg
|
| 26 |
-
*.egg
|
| 27 |
-
MANIFEST
|
| 28 |
-
|
| 29 |
-
# PyInstaller
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
.
|
| 42 |
-
.
|
| 43 |
-
.
|
| 44 |
-
.
|
| 45 |
-
.
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
#
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
.
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
# Celery stuff
|
| 131 |
-
celerybeat-schedule
|
| 132 |
-
celerybeat.pid
|
| 133 |
-
|
| 134 |
-
# SageMath parsed files
|
| 135 |
-
*.sage.py
|
| 136 |
-
|
| 137 |
-
# Environments
|
| 138 |
-
.env
|
| 139 |
-
.envrc
|
| 140 |
-
.venv
|
| 141 |
-
env/
|
| 142 |
-
venv/
|
| 143 |
-
ENV/
|
| 144 |
-
env.bak/
|
| 145 |
-
venv.bak/
|
| 146 |
-
|
| 147 |
-
# Spyder project settings
|
| 148 |
-
.spyderproject
|
| 149 |
-
.spyproject
|
| 150 |
-
|
| 151 |
-
# Rope project settings
|
| 152 |
-
.ropeproject
|
| 153 |
-
|
| 154 |
-
# mkdocs documentation
|
| 155 |
-
/site
|
| 156 |
-
|
| 157 |
-
# mypy
|
| 158 |
-
.mypy_cache/
|
| 159 |
-
.dmypy.json
|
| 160 |
-
dmypy.json
|
| 161 |
-
|
| 162 |
-
# Pyre type checker
|
| 163 |
-
.pyre/
|
| 164 |
-
|
| 165 |
-
# pytype static type analyzer
|
| 166 |
-
.pytype/
|
| 167 |
-
|
| 168 |
-
# Cython debug symbols
|
| 169 |
-
cython_debug/
|
| 170 |
-
|
| 171 |
-
# PyCharm
|
| 172 |
-
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
| 173 |
-
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
| 174 |
-
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
| 175 |
-
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
| 176 |
-
#.idea/
|
| 177 |
-
|
| 178 |
-
# Abstra
|
| 179 |
-
# Abstra is an AI-powered process automation framework.
|
| 180 |
-
# Ignore directories containing user credentials, local state, and settings.
|
| 181 |
-
# Learn more at https://abstra.io/docs
|
| 182 |
-
.abstra/
|
| 183 |
-
|
| 184 |
-
# Visual Studio Code
|
| 185 |
-
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
| 186 |
-
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
| 187 |
-
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
| 188 |
-
# you could uncomment the following to ignore the entire vscode folder
|
| 189 |
-
# .vscode/
|
| 190 |
-
|
| 191 |
-
# Ruff stuff:
|
| 192 |
-
.ruff_cache/
|
| 193 |
-
|
| 194 |
-
# PyPI configuration file
|
| 195 |
-
.pypirc
|
| 196 |
-
|
| 197 |
-
# Cursor
|
| 198 |
-
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
| 199 |
-
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
| 200 |
-
# refer to https://docs.cursor.com/context/ignore-files
|
| 201 |
-
.cursorignore
|
| 202 |
-
.cursorindexingignore
|
| 203 |
-
|
| 204 |
-
# Marimo
|
| 205 |
-
marimo/_static/
|
| 206 |
-
marimo/_lsp/
|
| 207 |
-
__marimo__/
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
*.pt
|
| 211 |
-
*.pth
|
| 212 |
-
data/gallery/
|
| 213 |
-
data/videos/
|
| 214 |
-
outputs/
|
|
|
|
| 1 |
+
# Byte-compiled / optimized / DLL files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[codz]
|
| 4 |
+
*$py.class
|
| 5 |
+
|
| 6 |
+
# C extensions
|
| 7 |
+
*.so
|
| 8 |
+
|
| 9 |
+
# Distribution / packaging
|
| 10 |
+
.Python
|
| 11 |
+
build/
|
| 12 |
+
develop-eggs/
|
| 13 |
+
dist/
|
| 14 |
+
downloads/
|
| 15 |
+
eggs/
|
| 16 |
+
.eggs/
|
| 17 |
+
lib/
|
| 18 |
+
lib64/
|
| 19 |
+
parts/
|
| 20 |
+
sdist/
|
| 21 |
+
var/
|
| 22 |
+
wheels/
|
| 23 |
+
share/python-wheels/
|
| 24 |
+
*.egg-info/
|
| 25 |
+
.installed.cfg
|
| 26 |
+
*.egg
|
| 27 |
+
MANIFEST
|
| 28 |
+
|
| 29 |
+
# PyInstaller
|
| 30 |
+
*.manifest
|
| 31 |
+
*.spec
|
| 32 |
+
|
| 33 |
+
# Installer logs
|
| 34 |
+
pip-log.txt
|
| 35 |
+
pip-delete-this-directory.txt
|
| 36 |
+
|
| 37 |
+
# Unit test / coverage reports
|
| 38 |
+
htmlcov/
|
| 39 |
+
.tox/
|
| 40 |
+
.nox/
|
| 41 |
+
.coverage
|
| 42 |
+
.coverage.*
|
| 43 |
+
.cache
|
| 44 |
+
nosetests.xml
|
| 45 |
+
coverage.xml
|
| 46 |
+
*.cover
|
| 47 |
+
*.py.cover
|
| 48 |
+
.hypothesis/
|
| 49 |
+
.pytest_cache/
|
| 50 |
+
cover/
|
| 51 |
+
|
| 52 |
+
# Translations
|
| 53 |
+
*.mo
|
| 54 |
+
*.pot
|
| 55 |
+
|
| 56 |
+
# Django stuff:
|
| 57 |
+
*.log
|
| 58 |
+
local_settings.py
|
| 59 |
+
db.sqlite3
|
| 60 |
+
db.sqlite3-journal
|
| 61 |
+
|
| 62 |
+
# Flask stuff:
|
| 63 |
+
instance/
|
| 64 |
+
.webassets-cache
|
| 65 |
+
|
| 66 |
+
# Scrapy stuff:
|
| 67 |
+
.scrapy
|
| 68 |
+
|
| 69 |
+
# Sphinx documentation
|
| 70 |
+
docs/_build/
|
| 71 |
+
|
| 72 |
+
# PyBuilder
|
| 73 |
+
.pybuilder/
|
| 74 |
+
target/
|
| 75 |
+
|
| 76 |
+
# Jupyter Notebook
|
| 77 |
+
.ipynb_checkpoints
|
| 78 |
+
|
| 79 |
+
# IPython
|
| 80 |
+
profile_default/
|
| 81 |
+
ipython_config.py
|
| 82 |
+
|
| 83 |
+
.pdm-python
|
| 84 |
+
.pdm-build/
|
| 85 |
+
.pixi
|
| 86 |
+
__pypackages__/
|
| 87 |
+
|
| 88 |
+
celerybeat-schedule
|
| 89 |
+
celerybeat.pid
|
| 90 |
+
|
| 91 |
+
*.sage.py
|
| 92 |
+
|
| 93 |
+
# Environments
|
| 94 |
+
.env
|
| 95 |
+
.envrc
|
| 96 |
+
.venv
|
| 97 |
+
env/
|
| 98 |
+
venv/
|
| 99 |
+
ENV/
|
| 100 |
+
env.bak/
|
| 101 |
+
venv.bak/
|
| 102 |
+
|
| 103 |
+
.spyderproject
|
| 104 |
+
.spyproject
|
| 105 |
+
.ropeproject
|
| 106 |
+
/site
|
| 107 |
+
.mypy_cache/
|
| 108 |
+
.dmypy.json
|
| 109 |
+
dmypy.json
|
| 110 |
+
.pyre/
|
| 111 |
+
.pytype/
|
| 112 |
+
cython_debug/
|
| 113 |
+
.abstra/
|
| 114 |
+
.ruff_cache/
|
| 115 |
+
.pypirc
|
| 116 |
+
.cursorignore
|
| 117 |
+
.cursorindexingignore
|
| 118 |
+
|
| 119 |
+
marimo/_static/
|
| 120 |
+
marimo/_lsp/
|
| 121 |
+
__marimo__/
|
| 122 |
+
|
| 123 |
+
data/gallery/
|
| 124 |
+
data/videos/
|
| 125 |
+
outputs/
|
| 126 |
+
|
| 127 |
+
fix_*.py
|
| 128 |
+
test_nl.py
|
| 129 |
+
test_picture/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Dockerfile
CHANGED
|
@@ -1,22 +1,22 @@
|
|
| 1 |
-
FROM python:3.10-slim
|
| 2 |
-
|
| 3 |
-
RUN apt-get update && apt-get install -y \
|
| 4 |
-
libgl1 \
|
| 5 |
-
libglib2.0-0 \
|
| 6 |
-
libsm6 \
|
| 7 |
-
libxext6 \
|
| 8 |
-
libxrender1 \
|
| 9 |
-
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
-
|
| 11 |
-
WORKDIR /app
|
| 12 |
-
|
| 13 |
-
COPY requirements.txt .
|
| 14 |
-
RUN pip install --no-cache-dir -r requirements.txt
|
| 15 |
-
|
| 16 |
-
COPY . .
|
| 17 |
-
|
| 18 |
-
RUN mkdir -p data/gallery data/videos outputs models
|
| 19 |
-
|
| 20 |
-
EXPOSE 7860
|
| 21 |
-
|
| 22 |
-
CMD ["
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
RUN apt-get update && apt-get install -y \
|
| 4 |
+
libgl1 \
|
| 5 |
+
libglib2.0-0 \
|
| 6 |
+
libsm6 \
|
| 7 |
+
libxext6 \
|
| 8 |
+
libxrender1 \
|
| 9 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
+
|
| 11 |
+
WORKDIR /app
|
| 12 |
+
|
| 13 |
+
COPY requirements.txt .
|
| 14 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 15 |
+
|
| 16 |
+
COPY . .
|
| 17 |
+
|
| 18 |
+
RUN mkdir -p data/gallery data/videos outputs models
|
| 19 |
+
|
| 20 |
+
EXPOSE 7860
|
| 21 |
+
|
| 22 |
+
CMD ["python", "api/main.py"]
|
LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
| 1 |
-
MIT License
|
| 2 |
-
|
| 3 |
-
Copyright (c) 2026 Abu-Sameer
|
| 4 |
-
|
| 5 |
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
-
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
-
in the Software without restriction, including without limitation the rights
|
| 8 |
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
-
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
-
furnished to do so, subject to the following conditions:
|
| 11 |
-
|
| 12 |
-
The above copyright notice and this permission notice shall be included in all
|
| 13 |
-
copies or substantial portions of the Software.
|
| 14 |
-
|
| 15 |
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
-
SOFTWARE.
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 Abu-Sameer
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
Procfile
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
web: uvicorn api.main:app --host 0.0.0.0 --port $PORT
|
README.md
CHANGED
|
@@ -1,9 +1,350 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div align="center">
|
| 2 |
+
|
| 3 |
+
<img src="https://capsule-render.vercel.app/api?type=waving&color=0:0a0a0a,50:1a0040,100:0d1f0d&height=300§ion=header&text=π·%20PhantomEye&fontSize=75&fontColor=00ff88&animation=fadeIn&fontAlignY=38&desc=AI-Powered%20Surveillance%20Intelligence%20System&descSize=18&descAlignY=58&descAlign=50&descColor=00aa55" width="100%"/>
|
| 4 |
+
|
| 5 |
+
<br/>
|
| 6 |
+
|
| 7 |
+
<img src="https://readme-typing-svg.herokuapp.com?font=JetBrains+Mono&weight=700&size=22&pause=1000&color=00FF88¢er=true&vCenter=true&width=900&lines=Person+Detection+%7C+ByteTrack+Multi-Object+Tracking;Deep+ReID+OSNet+β+Rank-1+81.7%25+mAP+58.5%25;Emotion+Intelligence+β+Age+%7C+Gender+%7C+Emotion;Weapon+Detection+β+9+Classes+mAP50+53.2%25;Natural+Language+Queries+β+English+%2B+Roman+Urdu;OSINT+Privacy+Audit+β+Exposure+Score+0%E2%80%93100" />
|
| 8 |
+
|
| 9 |
+
<br/><br/>
|
| 10 |
+
|
| 11 |
+
<a href="https://abu-sameer-66-phantomeye.hf.space">
|
| 12 |
+
<img src="https://img.shields.io/badge/%F0%9F%9F%A2%20LIVE%20DEMO-HuggingFace%20Spaces-00ff88?style=for-the-badge&labelColor=0d1f0d"/>
|
| 13 |
+
</a>
|
| 14 |
+
|
| 15 |
+
<a href="https://phantomeye-production.up.railway.app/docs">
|
| 16 |
+
<img src="https://img.shields.io/badge/%F0%9F%93%A1%20API%20DOCS-Railway-00aa55?style=for-the-badge&labelColor=003322"/>
|
| 17 |
+
</a>
|
| 18 |
+
|
| 19 |
+
<a href="https://medium.com/@sameerdataanalyst66/i-built-an-ai-that-watches-tracks-and-audits-phantomeye-is-live-afe2f62bcb7b">
|
| 20 |
+
<img src="https://img.shields.io/badge/%F0%9F%93%96%20MEDIUM-Full%20Article-00ff88?style=for-the-badge&labelColor=0d1f0d"/>
|
| 21 |
+
</a>
|
| 22 |
+
|
| 23 |
+
<a href="https://github.com/Abu-Sameer-66/PhantomEye/blob/main/LICENSE">
|
| 24 |
+
<img src="https://img.shields.io/badge/License-MIT-003322?style=for-the-badge&labelColor=0d1f0d"/>
|
| 25 |
+
</a>
|
| 26 |
+
|
| 27 |
+
<br/><br/>
|
| 28 |
+
|
| 29 |
+
<img src="https://img.shields.io/badge/YOLOv8-Detection-00ff88?style=flat-square&logo=pytorch&logoColor=black&labelColor=0d1f0d"/>
|
| 30 |
+
<img src="https://img.shields.io/badge/ByteTrack-Multi--Object%20Tracking-00aa55?style=flat-square&labelColor=003322"/>
|
| 31 |
+
<img src="https://img.shields.io/badge/OSNet-Deep%20ReID%20Rank--1%2081.7%25-00ff88?style=flat-square&logo=pytorch&logoColor=black&labelColor=0d1f0d"/>
|
| 32 |
+
<img src="https://img.shields.io/badge/DeepFace-Emotion%20Intelligence-00aa55?style=flat-square&labelColor=003322"/>
|
| 33 |
+
<img src="https://img.shields.io/badge/Groq%20LLaMA3-NL%20Query%20Engine-00ff88?style=flat-square&labelColor=0d1f0d"/>
|
| 34 |
+
<img src="https://img.shields.io/badge/YOLOv8%20Custom-Weapon%20Detection-00aa55?style=flat-square&labelColor=003322"/>
|
| 35 |
+
<img src="https://img.shields.io/badge/FastAPI-8%20Endpoints-00ff88?style=flat-square&logo=fastapi&logoColor=white&labelColor=0d1f0d"/>
|
| 36 |
+
<img src="https://img.shields.io/badge/Docker-Containerized-00aa55?style=flat-square&logo=docker&logoColor=white&labelColor=003322"/>
|
| 37 |
+
<img src="https://img.shields.io/badge/Python-3.10-00ff88?style=flat-square&logo=python&logoColor=black&labelColor=0d1f0d"/>
|
| 38 |
+
|
| 39 |
+
</div>
|
| 40 |
+
|
| 41 |
+
---
|
| 42 |
+
|
| 43 |
+
## What is PhantomEye?
|
| 44 |
+
|
| 45 |
+
Most computer vision projects stop at detection. They draw a box around a person and call it done. The box appears. The box disappears. Nothing is remembered. Nothing is understood.
|
| 46 |
+
|
| 47 |
+
**PhantomEye goes further.**
|
| 48 |
+
|
| 49 |
+
It is a full-stack AI surveillance intelligence platform that transforms passive camera feeds into a live reasoning engine β detecting, tracking, analyzing behavior, auditing identity, recognizing emotion, querying in natural language, and detecting weapons, all in one unified system.
|
| 50 |
+
|
| 51 |
+
Built entirely from scratch. Trained on real datasets. Deployed live. Zero pre-loaded data.
|
| 52 |
+
|
| 53 |
+
---
|
| 54 |
+
|
| 55 |
+
## Intelligence Modules
|
| 56 |
+
|
| 57 |
+
<table>
|
| 58 |
+
<tr>
|
| 59 |
+
<td width="50%">
|
| 60 |
+
|
| 61 |
+
### π· Person Detection
|
| 62 |
+
YOLOv8-nano configured for class-0 only. Returns bounding boxes and confidence scores on standard CPU in milliseconds. No GPU required at inference time.
|
| 63 |
+
|
| 64 |
+
### π― Multi-Object Tracking
|
| 65 |
+
Custom ByteTrack with IOU matching. Each person receives a persistent color-coded ID with trajectory trail β across frames, through occlusion, across re-entries.
|
| 66 |
+
|
| 67 |
+
### π₯ Behavioral Heatmap
|
| 68 |
+
NumPy position accumulation builds a live heatmap of human movement. High-activity zones appear red. Dwell time tracked per person in seconds. Loitering alerts fire automatically.
|
| 69 |
+
|
| 70 |
+
### π§ Deep Person Re-ID
|
| 71 |
+
OSNet x0.25 trained from scratch on Market-1501 (12,936 images, 751 identities). **Rank-1: 81.7% β mAP: 58.5%.** Identifies the same person across camera networks using body appearance alone β no face required.
|
| 72 |
+
|
| 73 |
+
</td>
|
| 74 |
+
<td width="50%">
|
| 75 |
+
|
| 76 |
+
### πΆ Emotion Intelligence
|
| 77 |
+
DeepFace pipeline β detects age, gender, and dominant emotion per face. Powered by TensorFlow with OpenCV face detector backend. Optimized for CPU deployment.
|
| 78 |
+
|
| 79 |
+
### π¬ Natural Language Query Engine
|
| 80 |
+
Groq LLaMA 3 powered query parser. Ask questions in plain English or Roman Urdu β the system extracts structured filters automatically. First open-source surveillance system with multilingual NL query support.
|
| 81 |
+
|
| 82 |
+
### π« Weapon Detection
|
| 83 |
+
YOLOv8 custom trained on 9 weapon classes β Handgun, Knife, Shotgun, Sniper, Automatic Rifle, SMG, Sword, Bazooka, Grenade Launcher. **mAP50: 53.2% β Handgun: 89.5% β Shotgun: 96.3% β SMG: 98.6%.** Real-time threat alert on detection.
|
| 84 |
+
|
| 85 |
+
### π OSINT Privacy Audit
|
| 86 |
+
Upload a face β get a Privacy Exposure Score from 0 to 100. LBPH embedding search against a reference gallery. Risk classification: LOW / MEDIUM / HIGH.
|
| 87 |
+
|
| 88 |
+
</td>
|
| 89 |
+
</tr>
|
| 90 |
+
</table>
|
| 91 |
+
|
| 92 |
+
---
|
| 93 |
+
|
| 94 |
+
## Benchmark Results
|
| 95 |
+
|
| 96 |
+
| Module | Model | Metric | Score |
|
| 97 |
+
|:---|:---|:---|:---:|
|
| 98 |
+
| Person Detection | YOLOv8-nano | Confidence | >85% avg |
|
| 99 |
+
| Multi-Object Tracking | ByteTrack | ID Persistence | Across occlusion |
|
| 100 |
+
| Deep Re-ID | OSNet x0.25 | **Rank-1** | **81.7%** |
|
| 101 |
+
| Deep Re-ID | OSNet x0.25 | **mAP** | **58.5%** |
|
| 102 |
+
| Emotion Recognition | DeepFace | Face Detection | OpenCV backend |
|
| 103 |
+
| Weapon Detection | YOLOv8n custom | **mAP50** | **53.2%** |
|
| 104 |
+
| Weapon Detection | YOLOv8n custom | Handgun AP | 89.5% |
|
| 105 |
+
| Weapon Detection | YOLOv8n custom | Shotgun AP | 96.3% |
|
| 106 |
+
| Weapon Detection | YOLOv8n custom | SMG AP | 98.6% |
|
| 107 |
+
| NL Query Engine | Groq LLaMA 3 | Languages | English + Roman Urdu |
|
| 108 |
+
|
| 109 |
+
---
|
| 110 |
+
|
| 111 |
+
## Why PhantomEye is Different
|
| 112 |
+
|
| 113 |
+
| Capability | Typical CV Project | PhantomEye |
|
| 114 |
+
|:---|:---:|:---:|
|
| 115 |
+
| Person detection | β
| β
YOLOv8-nano |
|
| 116 |
+
| Persistent ID tracking | β | β
ByteTrack |
|
| 117 |
+
| Behavioral heatmap | β | β
NumPy accumulation |
|
| 118 |
+
| Dwell time analytics | β | β
Per-person seconds |
|
| 119 |
+
| Loitering alert | β | β
Threshold-based |
|
| 120 |
+
| Deep Re-ID (no face) | β | β
OSNet Rank-1 81.7% |
|
| 121 |
+
| Emotion recognition | β | β
DeepFace β Age + Gender + Emotion |
|
| 122 |
+
| Weapon detection | β | β
9-class YOLOv8 custom |
|
| 123 |
+
| NL query interface | β | β
English + Roman Urdu |
|
| 124 |
+
| OSINT privacy audit | β | β
Score 0β100 |
|
| 125 |
+
| Production REST API | β | β
8 endpoints, OAS 3.1 |
|
| 126 |
+
| Live 24/7 deployment | β | β
HuggingFace + Railway |
|
| 127 |
+
| Zero pre-loaded data | β | β
Privacy-first |
|
| 128 |
+
|
| 129 |
+
---
|
| 130 |
+
|
| 131 |
+
## System Architecture
|
| 132 |
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 133 |
+
β INPUT LAYER β
|
| 134 |
+
β Image Upload / Video File / RTSP Feed β
|
| 135 |
+
ββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββ
|
| 136 |
+
β
|
| 137 |
+
ββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββ
|
| 138 |
+
β VISION PIPELINE β
|
| 139 |
+
β β
|
| 140 |
+
β YOLOv8-nano βββββββ Person Detection β
|
| 141 |
+
β β bbox + confidence β
|
| 142 |
+
β β β
|
| 143 |
+
β ByteTrack ββββββββ Persistent ID Assignment β
|
| 144 |
+
β β Color trails + occlusion β
|
| 145 |
+
β β β
|
| 146 |
+
β OSNet x0.25 ββββββ Deep Person Re-ID β
|
| 147 |
+
β Rank-1 81.7% on Market-1501 β
|
| 148 |
+
ββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββ
|
| 149 |
+
β
|
| 150 |
+
ββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββ
|
| 151 |
+
β INTELLIGENCE LAYER β
|
| 152 |
+
β β
|
| 153 |
+
β Behavioral Analytics β
|
| 154 |
+
β βββ NumPy heatmap accumulation β
|
| 155 |
+
β βββ Per-person dwell time (seconds) β
|
| 156 |
+
β βββ Automated loitering alerts β
|
| 157 |
+
β β
|
| 158 |
+
β Emotion Intelligence β
|
| 159 |
+
β βββ DeepFace β Age + Gender + Emotion β
|
| 160 |
+
β βββ OpenCV detector backend (CPU optimized) β
|
| 161 |
+
β β
|
| 162 |
+
β Weapon Detection β
|
| 163 |
+
β βββ YOLOv8 custom β 9 weapon classes β
|
| 164 |
+
β βββ Real-time threat alert on detection β
|
| 165 |
+
β β
|
| 166 |
+
β NL Query Engine β
|
| 167 |
+
β βββ Groq LLaMA 3 β query parser β
|
| 168 |
+
β βββ English + Roman Urdu β structured filters β
|
| 169 |
+
β β
|
| 170 |
+
β OSINT Audit Engine β
|
| 171 |
+
β βββ LBPH face embedding extraction β
|
| 172 |
+
β βββ Gallery similarity search β
|
| 173 |
+
β βββ Exposure score (0β100) + risk level β
|
| 174 |
+
ββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββ
|
| 175 |
+
β
|
| 176 |
+
ββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββ
|
| 177 |
+
β OUTPUT LAYER β
|
| 178 |
+
β β
|
| 179 |
+
β FastAPI REST API ββ 8 endpoints, OAS 3.1 β
|
| 180 |
+
β Streamlit Dashboard ββ Cyberpunk UI β
|
| 181 |
+
β JSON Reports ββ Exportable audit logs β
|
| 182 |
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 183 |
+
|
| 184 |
+
---
|
| 185 |
+
|
| 186 |
+
## Live Deployment
|
| 187 |
+
|
| 188 |
+
| Service | Platform | Status |
|
| 189 |
+
|:---|:---|:---:|
|
| 190 |
+
| [Interactive Dashboard](https://abu-sameer-66-phantomeye.hf.space) | HuggingFace Spaces | π’ Live |
|
| 191 |
+
| [REST API](https://phantomeye-production.up.railway.app) | Railway | π’ Live |
|
| 192 |
+
| [API Documentation](https://phantomeye-production.up.railway.app/docs) | Railway | π’ Live |
|
| 193 |
+
|
| 194 |
+
---
|
| 195 |
+
|
| 196 |
+
## API Reference
|
| 197 |
+
|
| 198 |
+
**Base URL:** `https://phantomeye-production.up.railway.app`
|
| 199 |
+
|
| 200 |
+
| Method | Endpoint | Description |
|
| 201 |
+
|:---|:---|:---|
|
| 202 |
+
| `GET` | `/` | System info + version |
|
| 203 |
+
| `GET` | `/health` | Live health check |
|
| 204 |
+
| `POST` | `/detect` | Person detection on image |
|
| 205 |
+
| `POST` | `/track/video` | Multi-object tracking on video |
|
| 206 |
+
| `POST` | `/osint/audit` | Privacy exposure audit |
|
| 207 |
+
| `POST` | `/osint/add-to-gallery` | Register person to gallery |
|
| 208 |
+
| `GET` | `/osint/gallery` | List gallery persons |
|
| 209 |
+
| `GET` | `/outputs` | List output files |
|
| 210 |
+
|
| 211 |
+
**Quick test:**
|
| 212 |
+
```bash
|
| 213 |
+
curl -X POST "https://phantomeye-production.up.railway.app/detect" \
|
| 214 |
+
-F "file=@crowd.jpg"
|
| 215 |
+
```
|
| 216 |
+
```json
|
| 217 |
+
{
|
| 218 |
+
"status": "success",
|
| 219 |
+
"total_persons": 8,
|
| 220 |
+
"detections": [
|
| 221 |
+
{ "id": 1, "bbox": [120, 80, 310, 420], "confidence": 0.87 },
|
| 222 |
+
{ "id": 2, "bbox": [450, 95, 620, 430], "confidence": 0.74 }
|
| 223 |
+
]
|
| 224 |
+
}
|
| 225 |
+
```
|
| 226 |
+
|
| 227 |
+
---
|
| 228 |
+
|
| 229 |
+
## Local Setup
|
| 230 |
+
|
| 231 |
+
```bash
|
| 232 |
+
git clone https://github.com/Abu-Sameer-66/PhantomEye.git
|
| 233 |
+
cd PhantomEye
|
| 234 |
+
|
| 235 |
+
conda create -n phantomeye python=3.10 -y
|
| 236 |
+
conda activate phantomeye
|
| 237 |
+
pip install -r requirements.txt
|
| 238 |
+
```
|
| 239 |
+
|
| 240 |
+
```bash
|
| 241 |
+
# Streamlit dashboard
|
| 242 |
+
streamlit run app.py
|
| 243 |
+
|
| 244 |
+
# FastAPI backend
|
| 245 |
+
python api/main.py
|
| 246 |
+
|
| 247 |
+
# Detection on image or video
|
| 248 |
+
python core/detection.py
|
| 249 |
+
|
| 250 |
+
# OSINT audit
|
| 251 |
+
python core/osint.py
|
| 252 |
+
|
| 253 |
+
# Weapon detection
|
| 254 |
+
python core/weapon.py
|
| 255 |
+
|
| 256 |
+
# Emotion analysis
|
| 257 |
+
python core/emotion.py
|
| 258 |
+
```
|
| 259 |
+
|
| 260 |
+
---
|
| 261 |
+
|
| 262 |
+
## Repository Structure
|
| 263 |
+
PhantomEye/
|
| 264 |
+
βββ core/
|
| 265 |
+
β βββ detection.py YOLOv8 person detector
|
| 266 |
+
β βββ tracker.py ByteTrack multi-object tracker
|
| 267 |
+
β βββ analytics.py Heatmap + dwell time + loitering alerts
|
| 268 |
+
β βββ osint.py OSINT privacy audit engine
|
| 269 |
+
β βββ emotion.py DeepFace emotion + age + gender
|
| 270 |
+
β βββ reid.py OSNet deep Re-ID module
|
| 271 |
+
β βββ weapon.py YOLOv8 weapon detection
|
| 272 |
+
β βββ nlquery.py Groq NL query parser
|
| 273 |
+
βββ models/
|
| 274 |
+
β βββ osnet_phantomeye_reid.pth Trained Re-ID weights
|
| 275 |
+
β βββ weapon_detector.pt Trained weapon detector
|
| 276 |
+
βββ api/
|
| 277 |
+
β βββ main.py FastAPI backend β 8 endpoints
|
| 278 |
+
β βββ routes/ Modular route handlers
|
| 279 |
+
βββ app.py Streamlit dashboard β 7 modules
|
| 280 |
+
βββ config.py Global configuration
|
| 281 |
+
βββ Dockerfile Container deployment
|
| 282 |
+
βββ requirements.txt Dependencies
|
| 283 |
+
|
| 284 |
+
---
|
| 285 |
+
|
| 286 |
+
## Real-World Applications
|
| 287 |
+
|
| 288 |
+
| Domain | Use Case |
|
| 289 |
+
|:---|:---|
|
| 290 |
+
| Law Enforcement | Cross-camera suspect tracking, weapon threat detection, automated evidence extraction |
|
| 291 |
+
| Retail Intelligence | Customer heatmaps, queue monitoring, suspicious behavior detection |
|
| 292 |
+
| Campus Security | Unauthorized access detection, behavioral anomaly alerts |
|
| 293 |
+
| Healthcare | Patient wandering alerts, fall detection, ICU monitoring |
|
| 294 |
+
| Border Security | Weapon screening, person Re-ID across checkpoints |
|
| 295 |
+
| Privacy Research | Digital footprint auditing, OSINT defense tools |
|
| 296 |
+
|
| 297 |
+
---
|
| 298 |
+
|
| 299 |
+
## Privacy-First Design
|
| 300 |
+
|
| 301 |
+
- **Zero pre-loaded data** β no faces, videos, or images in the repository
|
| 302 |
+
- **In-session processing** β uploaded files processed in RAM only, never stored
|
| 303 |
+
- **User-controlled gallery** β only data you explicitly upload is referenced
|
| 304 |
+
- **Ethical OSINT framing** β audit module built for privacy defense, not offense
|
| 305 |
+
- **Fully open source** β every processing step is transparent and auditable
|
| 306 |
+
|
| 307 |
+
---
|
| 308 |
+
|
| 309 |
+
## Roadmap
|
| 310 |
+
|
| 311 |
+
- [x] YOLOv8 person detection β CPU optimized
|
| 312 |
+
- [x] ByteTrack multi-object tracking
|
| 313 |
+
- [x] Behavioral heatmap + loitering alerts
|
| 314 |
+
- [x] OSINT privacy audit engine
|
| 315 |
+
- [x] FastAPI production backend β 8 endpoints
|
| 316 |
+
- [x] Cyberpunk Streamlit dashboard
|
| 317 |
+
- [x] HuggingFace + Railway live deployment
|
| 318 |
+
- [x] DeepFace emotion intelligence module
|
| 319 |
+
- [x] Groq NL query engine β English + Roman Urdu
|
| 320 |
+
- [x] OSNet Deep Re-ID β Rank-1 81.7% on Market-1501
|
| 321 |
+
- [x] YOLOv8 weapon detection β 9 classes mAP50 53.2%
|
| 322 |
+
- [ ] PDF intelligence report generator
|
| 323 |
+
- [ ] JWT authentication + API key management
|
| 324 |
+
- [ ] RTSP live stream support
|
| 325 |
+
- [ ] Anonymization mode β face blur + full analytics
|
| 326 |
+
- [ ] Edge deployment β Raspberry Pi + Jetson Nano
|
| 327 |
+
|
| 328 |
+
---
|
| 329 |
+
|
| 330 |
+
## Author
|
| 331 |
+
|
| 332 |
+
<div align="center">
|
| 333 |
+
|
| 334 |
+
**Abu Sameer** β AI/ML Engineer Β· Computer Vision Researcher Β· GSoC 2026 Contributor
|
| 335 |
+
|
| 336 |
+
<br/>
|
| 337 |
+
|
| 338 |
+
<a href="https://sameer-nadeem-portfolio.vercel.app"><img src="https://img.shields.io/badge/Portfolio-sameer--nadeem--portfolio-00ff88?style=for-the-badge&labelColor=0d1f0d"/></a>
|
| 339 |
+
<a href="https://github.com/Abu-Sameer-66"><img src="https://img.shields.io/badge/GitHub-Abu--Sameer--66-00aa55?style=for-the-badge&logo=github&labelColor=003322"/></a>
|
| 340 |
+
<a href="https://www.linkedin.com/in/sameer-nadeem-66339a357/"><img src="https://img.shields.io/badge/LinkedIn-Sameer%20Nadeem-00ff88?style=for-the-badge&logo=linkedin&labelColor=0d1f0d"/></a>
|
| 341 |
+
<a href="https://www.kaggle.com/sameernadeem66"><img src="https://img.shields.io/badge/Kaggle-sameernadeem66-00aa55?style=for-the-badge&logo=kaggle&labelColor=003322"/></a>
|
| 342 |
+
<a href="https://medium.com/@sameerdataanalyst66/i-built-an-ai-that-watches-tracks-and-audits-phantomeye-is-live-afe2f62bcb7b"><img src="https://img.shields.io/badge/Medium-Full%20Article-00ff88?style=for-the-badge&logo=medium&labelColor=0d1f0d"/></a>
|
| 343 |
+
|
| 344 |
+
</div>
|
| 345 |
+
|
| 346 |
+
---
|
| 347 |
+
|
| 348 |
+
<div align="center">
|
| 349 |
+
<img src="https://capsule-render.vercel.app/api?type=waving&color=0:0d1f0d,50:003322,100:0a0a0a&height=120§ion=footer" width="100%"/>
|
| 350 |
+
</div>
|
api/auth.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import uuid
|
| 3 |
+
import hashlib
|
| 4 |
+
import hmac
|
| 5 |
+
from datetime import datetime, timedelta
|
| 6 |
+
from typing import Optional
|
| 7 |
+
from jose import JWTError, jwt
|
| 8 |
+
from passlib.context import CryptContext
|
| 9 |
+
from fastapi import HTTPException, Security, status
|
| 10 |
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
| 11 |
+
|
| 12 |
+
# Config
|
| 13 |
+
SECRET_KEY = os.getenv("SECRET_KEY", "phantomeye-secret-key-change-in-production-2026")
|
| 14 |
+
ALGORITHM = "HS256"
|
| 15 |
+
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 # 24 hours
|
| 16 |
+
|
| 17 |
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
| 18 |
+
bearer_scheme = HTTPBearer()
|
| 19 |
+
|
| 20 |
+
# In-memory API key store β replace with PostgreSQL in production
|
| 21 |
+
API_KEYS_DB: dict[str, dict] = {}
|
| 22 |
+
|
| 23 |
+
# Pre-registered demo keys for testing
|
| 24 |
+
DEMO_KEYS = {
|
| 25 |
+
"PE-DEMO-KEY-2026-FREE": {
|
| 26 |
+
"client": "demo_user",
|
| 27 |
+
"tier": "free",
|
| 28 |
+
"rate_limit": 100,
|
| 29 |
+
"calls_today": 0,
|
| 30 |
+
"created_at": datetime.utcnow().isoformat(),
|
| 31 |
+
"active": True,
|
| 32 |
+
},
|
| 33 |
+
"PE-PROD-KEY-2026-PRO": {
|
| 34 |
+
"client": "pro_user",
|
| 35 |
+
"tier": "pro",
|
| 36 |
+
"rate_limit": 10000,
|
| 37 |
+
"calls_today": 0,
|
| 38 |
+
"created_at": datetime.utcnow().isoformat(),
|
| 39 |
+
"active": True,
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
API_KEYS_DB.update(DEMO_KEYS)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def generate_api_key(client_name: str, tier: str = "free") -> str:
|
| 46 |
+
"""Generate a unique PhantomEye API key."""
|
| 47 |
+
unique = str(uuid.uuid4()).replace("-", "").upper()[:16]
|
| 48 |
+
key = f"PE-{tier.upper()}-{unique}"
|
| 49 |
+
API_KEYS_DB[key] = {
|
| 50 |
+
"client": client_name,
|
| 51 |
+
"tier": tier,
|
| 52 |
+
"rate_limit": 100 if tier == "free" else 10000,
|
| 53 |
+
"calls_today": 0,
|
| 54 |
+
"created_at": datetime.utcnow().isoformat(),
|
| 55 |
+
"active": True,
|
| 56 |
+
}
|
| 57 |
+
return key
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def validate_api_key(api_key: str) -> dict:
|
| 61 |
+
"""Validate API key and check rate limit."""
|
| 62 |
+
if api_key not in API_KEYS_DB:
|
| 63 |
+
raise HTTPException(
|
| 64 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 65 |
+
detail="Invalid API key"
|
| 66 |
+
)
|
| 67 |
+
key_data = API_KEYS_DB[api_key]
|
| 68 |
+
if not key_data["active"]:
|
| 69 |
+
raise HTTPException(
|
| 70 |
+
status_code=status.HTTP_403_FORBIDDEN,
|
| 71 |
+
detail="API key is disabled"
|
| 72 |
+
)
|
| 73 |
+
if key_data["calls_today"] >= key_data["rate_limit"]:
|
| 74 |
+
raise HTTPException(
|
| 75 |
+
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
| 76 |
+
detail=f"Rate limit exceeded β {key_data['rate_limit']} calls/day for {key_data['tier']} tier"
|
| 77 |
+
)
|
| 78 |
+
API_KEYS_DB[api_key]["calls_today"] += 1
|
| 79 |
+
return key_data
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
|
| 83 |
+
"""Create JWT access token."""
|
| 84 |
+
to_encode = data.copy()
|
| 85 |
+
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
|
| 86 |
+
to_encode.update({"exp": expire})
|
| 87 |
+
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def verify_token(credentials: HTTPAuthorizationCredentials = Security(bearer_scheme)) -> dict:
|
| 91 |
+
"""Verify JWT token from Authorization header."""
|
| 92 |
+
try:
|
| 93 |
+
payload = jwt.decode(credentials.credentials, SECRET_KEY, algorithms=[ALGORITHM])
|
| 94 |
+
client = payload.get("sub")
|
| 95 |
+
if client is None:
|
| 96 |
+
raise HTTPException(status_code=401, detail="Invalid token")
|
| 97 |
+
return payload
|
| 98 |
+
except JWTError:
|
| 99 |
+
raise HTTPException(status_code=401, detail="Invalid or expired token")
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def get_api_key_stats() -> dict:
|
| 103 |
+
"""Return stats about all registered API keys."""
|
| 104 |
+
return {
|
| 105 |
+
"total_keys": len(API_KEYS_DB),
|
| 106 |
+
"active_keys": sum(1 for k in API_KEYS_DB.values() if k["active"]),
|
| 107 |
+
"keys": [
|
| 108 |
+
{
|
| 109 |
+
"key": k[:12] + "****",
|
| 110 |
+
"client": v["client"],
|
| 111 |
+
"tier": v["tier"],
|
| 112 |
+
"calls_today": v["calls_today"],
|
| 113 |
+
"rate_limit": v["rate_limit"],
|
| 114 |
+
"active": v["active"],
|
| 115 |
+
}
|
| 116 |
+
for k, v in API_KEYS_DB.items()
|
| 117 |
+
]
|
| 118 |
+
}
|
api/main.py
CHANGED
|
@@ -1,217 +1,272 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import time
|
| 3 |
-
import shutil
|
| 4 |
-
import uvicorn
|
| 5 |
-
from pathlib import Path
|
| 6 |
-
from fastapi import FastAPI, File, UploadFile, HTTPException
|
| 7 |
-
from fastapi.middleware.cors import CORSMiddleware
|
| 8 |
-
from fastapi.responses import JSONResponse, FileResponse
|
| 9 |
-
|
| 10 |
-
import sys
|
| 11 |
-
sys.path.append(str(Path(__file__).resolve().parent.parent))
|
| 12 |
-
|
| 13 |
-
from core.detection import PersonDetector
|
| 14 |
-
from core.tracker import ByteTracker
|
| 15 |
-
from core.osint import OSINTAudit
|
| 16 |
-
from config import OUTPUTS_DIR, GALLERY_DIR, API_HOST, API_PORT
|
| 17 |
-
|
| 18 |
-
app = FastAPI(
|
| 19 |
-
title="PhantomEye API",
|
| 20 |
-
description="AI-powered surveillance intelligence β Person Re-ID, Behavioral Analytics, OSINT Defense",
|
| 21 |
-
version="1.0.0",
|
| 22 |
-
)
|
| 23 |
-
|
| 24 |
-
app.add_middleware(
|
| 25 |
-
CORSMiddleware,
|
| 26 |
-
allow_origins=["*"],
|
| 27 |
-
allow_methods=["*"],
|
| 28 |
-
allow_headers=["*"],
|
| 29 |
-
)
|
| 30 |
-
|
| 31 |
-
detector = PersonDetector()
|
| 32 |
-
osint = OSINTAudit()
|
| 33 |
-
|
| 34 |
-
import cv2
|
| 35 |
-
import numpy as np
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
@app.get("/")
|
| 39 |
-
def root():
|
| 40 |
-
return {
|
| 41 |
-
"system" : "PhantomEye",
|
| 42 |
-
"version" : "1.0.0",
|
| 43 |
-
"status" : "online",
|
| 44 |
-
"modules" : ["detection", "tracking", "analytics", "osint"],
|
| 45 |
-
"author" : "Abu-Sameer-66",
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
@app.get("/health")
|
| 50 |
-
def health():
|
| 51 |
-
return {
|
| 52 |
-
"status" : "healthy",
|
| 53 |
-
"gallery_size" : len(osint.gallery),
|
| 54 |
-
"timestamp" : time.strftime("%Y-%m-%d %H:%M:%S"),
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
@app.post("/detect")
|
| 59 |
-
async def detect_persons(file: UploadFile = File(...)):
|
| 60 |
-
if not file.filename.lower().endswith((".jpg", ".jpeg", ".png")):
|
| 61 |
-
raise HTTPException(400, "Only JPG/PNG images supported.")
|
| 62 |
-
|
| 63 |
-
data = await file.read()
|
| 64 |
-
arr = np.frombuffer(data, np.uint8)
|
| 65 |
-
image = cv2.imdecode(arr, cv2.IMREAD_COLOR)
|
| 66 |
-
|
| 67 |
-
if image is None:
|
| 68 |
-
raise HTTPException(400, "Cannot decode image.")
|
| 69 |
-
|
| 70 |
-
detections = detector.detect(image)
|
| 71 |
-
|
| 72 |
-
return JSONResponse({
|
| 73 |
-
"status" : "success",
|
| 74 |
-
"filename" : file.filename,
|
| 75 |
-
"total_persons" : len(detections),
|
| 76 |
-
"detections" : [
|
| 77 |
-
{
|
| 78 |
-
"id" : i + 1,
|
| 79 |
-
"bbox" : list(d["bbox"]),
|
| 80 |
-
"confidence": d["confidence"],
|
| 81 |
-
}
|
| 82 |
-
for i, d in enumerate(detections)
|
| 83 |
-
],
|
| 84 |
-
})
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
@app.post("/osint/audit")
|
| 88 |
-
async def osint_audit(file: UploadFile = File(...)):
|
| 89 |
-
if not file.filename.lower().endswith((".jpg", ".jpeg", ".png")):
|
| 90 |
-
raise HTTPException(400, "Only JPG/PNG images supported.")
|
| 91 |
-
|
| 92 |
-
data = await file.read()
|
| 93 |
-
arr = np.frombuffer(data, np.uint8)
|
| 94 |
-
image = cv2.imdecode(arr, cv2.IMREAD_COLOR)
|
| 95 |
-
|
| 96 |
-
if image is None:
|
| 97 |
-
raise HTTPException(400, "Cannot decode image.")
|
| 98 |
-
|
| 99 |
-
query_id = Path(file.filename).stem
|
| 100 |
-
result = osint.audit(image, query_id=query_id)
|
| 101 |
-
osint.save_report(result)
|
| 102 |
-
|
| 103 |
-
return JSONResponse({
|
| 104 |
-
"status": "success",
|
| 105 |
-
"audit" : result,
|
| 106 |
-
})
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
@app.post("/osint/add-to-gallery")
|
| 110 |
-
async def add_to_gallery(
|
| 111 |
-
file : UploadFile = File(...),
|
| 112 |
-
person_id: str = "unknown",
|
| 113 |
-
):
|
| 114 |
-
data = await file.read()
|
| 115 |
-
arr = np.frombuffer(data, np.uint8)
|
| 116 |
-
image = cv2.imdecode(arr, cv2.IMREAD_COLOR)
|
| 117 |
-
|
| 118 |
-
if image is None:
|
| 119 |
-
raise HTTPException(400, "Cannot decode image.")
|
| 120 |
-
|
| 121 |
-
success = osint.add_to_gallery(image, person_id)
|
| 122 |
-
|
| 123 |
-
if not success:
|
| 124 |
-
raise HTTPException(400, "No face detected in uploaded image.")
|
| 125 |
-
|
| 126 |
-
return JSONResponse({
|
| 127 |
-
"status" : "success",
|
| 128 |
-
"person_id" : person_id,
|
| 129 |
-
"gallery_size": len(osint.gallery),
|
| 130 |
-
"message" : f"Person '{person_id}' added to gallery.",
|
| 131 |
-
})
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
@app.get("/osint/gallery")
|
| 135 |
-
def get_gallery():
|
| 136 |
-
return JSONResponse({
|
| 137 |
-
"status" : "success",
|
| 138 |
-
"gallery_size": len(osint.gallery),
|
| 139 |
-
"persons" : list(osint.gallery.keys()),
|
| 140 |
-
})
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
@app.post("/track/video")
|
| 144 |
-
async def track_video(file: UploadFile = File(...)):
|
| 145 |
-
if not file.filename.lower().endswith((".mp4", ".avi", ".mov")):
|
| 146 |
-
raise HTTPException(400, "Only MP4/AVI/MOV videos supported.")
|
| 147 |
-
|
| 148 |
-
tmp_path = OUTPUTS_DIR / f"tmp_{int(time.time())}_{file.filename}"
|
| 149 |
-
OUTPUTS_DIR.mkdir(parents=True, exist_ok=True)
|
| 150 |
-
|
| 151 |
-
with open(tmp_path, "wb") as f:
|
| 152 |
-
shutil.copyfileobj(file.file, f)
|
| 153 |
-
|
| 154 |
-
cap = cv2.VideoCapture(str(tmp_path))
|
| 155 |
-
fps = int(cap.get(cv2.CAP_PROP_FPS)) or 25
|
| 156 |
-
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 157 |
-
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
| 158 |
-
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
| 159 |
-
cap.release()
|
| 160 |
-
|
| 161 |
-
tracker = ByteTracker()
|
| 162 |
-
cap = cv2.VideoCapture(str(tmp_path))
|
| 163 |
-
frame_data = []
|
| 164 |
-
frame_idx = 0
|
| 165 |
-
|
| 166 |
-
while frame_idx < min(total, fps * 10):
|
| 167 |
-
ret, frame = cap.read()
|
| 168 |
-
if not ret:
|
| 169 |
-
break
|
| 170 |
-
dets = detector.detect(frame)
|
| 171 |
-
active = tracker.update(dets)
|
| 172 |
-
frame_data.append({
|
| 173 |
-
"frame" : frame_idx,
|
| 174 |
-
"active_persons" : len(active),
|
| 175 |
-
"track_ids" : [t.track_id for t in active],
|
| 176 |
-
})
|
| 177 |
-
frame_idx += 1
|
| 178 |
-
|
| 179 |
-
cap.release()
|
| 180 |
-
tmp_path.unlink(missing_ok=True)
|
| 181 |
-
|
| 182 |
-
return JSONResponse({
|
| 183 |
-
"status" : "success",
|
| 184 |
-
"filename" : file.filename,
|
| 185 |
-
"resolution" : f"{w}x{h}",
|
| 186 |
-
"fps" : fps,
|
| 187 |
-
"frames_analyzed" : frame_idx,
|
| 188 |
-
"unique_persons" : tracker.next_id - 1,
|
| 189 |
-
"frame_summary" : frame_data[:10],
|
| 190 |
-
})
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
@app.get("/outputs")
|
| 194 |
-
def list_outputs():
|
| 195 |
-
OUTPUTS_DIR.mkdir(parents=True, exist_ok=True)
|
| 196 |
-
files = [
|
| 197 |
-
{
|
| 198 |
-
"name": f.name,
|
| 199 |
-
"size": f"{f.stat().st_size // 1024} KB",
|
| 200 |
-
}
|
| 201 |
-
for f in OUTPUTS_DIR.iterdir()
|
| 202 |
-
if f.is_file() and not f.name.startswith("tmp_")
|
| 203 |
-
]
|
| 204 |
-
return JSONResponse({
|
| 205 |
-
"status": "success",
|
| 206 |
-
"total" : len(files),
|
| 207 |
-
"files" : files,
|
| 208 |
-
})
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
)
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import time
|
| 3 |
+
import shutil
|
| 4 |
+
import uvicorn
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from fastapi import FastAPI, File, UploadFile, HTTPException
|
| 7 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 8 |
+
from fastapi.responses import JSONResponse, FileResponse
|
| 9 |
+
|
| 10 |
+
import sys
|
| 11 |
+
sys.path.append(str(Path(__file__).resolve().parent.parent))
|
| 12 |
+
|
| 13 |
+
from core.detection import PersonDetector
|
| 14 |
+
from core.tracker import ByteTracker
|
| 15 |
+
from core.osint import OSINTAudit
|
| 16 |
+
from config import OUTPUTS_DIR, GALLERY_DIR, API_HOST, API_PORT
|
| 17 |
+
|
| 18 |
+
app = FastAPI(
|
| 19 |
+
title="PhantomEye API",
|
| 20 |
+
description="AI-powered surveillance intelligence β Person Re-ID, Behavioral Analytics, OSINT Defense",
|
| 21 |
+
version="1.0.0",
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
app.add_middleware(
|
| 25 |
+
CORSMiddleware,
|
| 26 |
+
allow_origins=["*"],
|
| 27 |
+
allow_methods=["*"],
|
| 28 |
+
allow_headers=["*"],
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
detector = PersonDetector()
|
| 32 |
+
osint = OSINTAudit()
|
| 33 |
+
|
| 34 |
+
import cv2
|
| 35 |
+
import numpy as np
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@app.get("/")
|
| 39 |
+
def root():
|
| 40 |
+
return {
|
| 41 |
+
"system" : "PhantomEye",
|
| 42 |
+
"version" : "1.0.0",
|
| 43 |
+
"status" : "online",
|
| 44 |
+
"modules" : ["detection", "tracking", "analytics", "osint"],
|
| 45 |
+
"author" : "Abu-Sameer-66",
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
@app.get("/health")
|
| 50 |
+
def health():
|
| 51 |
+
return {
|
| 52 |
+
"status" : "healthy",
|
| 53 |
+
"gallery_size" : len(osint.gallery),
|
| 54 |
+
"timestamp" : time.strftime("%Y-%m-%d %H:%M:%S"),
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
@app.post("/detect")
|
| 59 |
+
async def detect_persons(file: UploadFile = File(...)):
|
| 60 |
+
if not file.filename.lower().endswith((".jpg", ".jpeg", ".png")):
|
| 61 |
+
raise HTTPException(400, "Only JPG/PNG images supported.")
|
| 62 |
+
|
| 63 |
+
data = await file.read()
|
| 64 |
+
arr = np.frombuffer(data, np.uint8)
|
| 65 |
+
image = cv2.imdecode(arr, cv2.IMREAD_COLOR)
|
| 66 |
+
|
| 67 |
+
if image is None:
|
| 68 |
+
raise HTTPException(400, "Cannot decode image.")
|
| 69 |
+
|
| 70 |
+
detections = detector.detect(image)
|
| 71 |
+
|
| 72 |
+
return JSONResponse({
|
| 73 |
+
"status" : "success",
|
| 74 |
+
"filename" : file.filename,
|
| 75 |
+
"total_persons" : len(detections),
|
| 76 |
+
"detections" : [
|
| 77 |
+
{
|
| 78 |
+
"id" : i + 1,
|
| 79 |
+
"bbox" : list(d["bbox"]),
|
| 80 |
+
"confidence": d["confidence"],
|
| 81 |
+
}
|
| 82 |
+
for i, d in enumerate(detections)
|
| 83 |
+
],
|
| 84 |
+
})
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
@app.post("/osint/audit")
|
| 88 |
+
async def osint_audit(file: UploadFile = File(...)):
|
| 89 |
+
if not file.filename.lower().endswith((".jpg", ".jpeg", ".png")):
|
| 90 |
+
raise HTTPException(400, "Only JPG/PNG images supported.")
|
| 91 |
+
|
| 92 |
+
data = await file.read()
|
| 93 |
+
arr = np.frombuffer(data, np.uint8)
|
| 94 |
+
image = cv2.imdecode(arr, cv2.IMREAD_COLOR)
|
| 95 |
+
|
| 96 |
+
if image is None:
|
| 97 |
+
raise HTTPException(400, "Cannot decode image.")
|
| 98 |
+
|
| 99 |
+
query_id = Path(file.filename).stem
|
| 100 |
+
result = osint.audit(image, query_id=query_id)
|
| 101 |
+
osint.save_report(result)
|
| 102 |
+
|
| 103 |
+
return JSONResponse({
|
| 104 |
+
"status": "success",
|
| 105 |
+
"audit" : result,
|
| 106 |
+
})
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
@app.post("/osint/add-to-gallery")
|
| 110 |
+
async def add_to_gallery(
|
| 111 |
+
file : UploadFile = File(...),
|
| 112 |
+
person_id: str = "unknown",
|
| 113 |
+
):
|
| 114 |
+
data = await file.read()
|
| 115 |
+
arr = np.frombuffer(data, np.uint8)
|
| 116 |
+
image = cv2.imdecode(arr, cv2.IMREAD_COLOR)
|
| 117 |
+
|
| 118 |
+
if image is None:
|
| 119 |
+
raise HTTPException(400, "Cannot decode image.")
|
| 120 |
+
|
| 121 |
+
success = osint.add_to_gallery(image, person_id)
|
| 122 |
+
|
| 123 |
+
if not success:
|
| 124 |
+
raise HTTPException(400, "No face detected in uploaded image.")
|
| 125 |
+
|
| 126 |
+
return JSONResponse({
|
| 127 |
+
"status" : "success",
|
| 128 |
+
"person_id" : person_id,
|
| 129 |
+
"gallery_size": len(osint.gallery),
|
| 130 |
+
"message" : f"Person '{person_id}' added to gallery.",
|
| 131 |
+
})
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
@app.get("/osint/gallery")
|
| 135 |
+
def get_gallery():
|
| 136 |
+
return JSONResponse({
|
| 137 |
+
"status" : "success",
|
| 138 |
+
"gallery_size": len(osint.gallery),
|
| 139 |
+
"persons" : list(osint.gallery.keys()),
|
| 140 |
+
})
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
@app.post("/track/video")
|
| 144 |
+
async def track_video(file: UploadFile = File(...)):
|
| 145 |
+
if not file.filename.lower().endswith((".mp4", ".avi", ".mov")):
|
| 146 |
+
raise HTTPException(400, "Only MP4/AVI/MOV videos supported.")
|
| 147 |
+
|
| 148 |
+
tmp_path = OUTPUTS_DIR / f"tmp_{int(time.time())}_{file.filename}"
|
| 149 |
+
OUTPUTS_DIR.mkdir(parents=True, exist_ok=True)
|
| 150 |
+
|
| 151 |
+
with open(tmp_path, "wb") as f:
|
| 152 |
+
shutil.copyfileobj(file.file, f)
|
| 153 |
+
|
| 154 |
+
cap = cv2.VideoCapture(str(tmp_path))
|
| 155 |
+
fps = int(cap.get(cv2.CAP_PROP_FPS)) or 25
|
| 156 |
+
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 157 |
+
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
| 158 |
+
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
| 159 |
+
cap.release()
|
| 160 |
+
|
| 161 |
+
tracker = ByteTracker()
|
| 162 |
+
cap = cv2.VideoCapture(str(tmp_path))
|
| 163 |
+
frame_data = []
|
| 164 |
+
frame_idx = 0
|
| 165 |
+
|
| 166 |
+
while frame_idx < min(total, fps * 10):
|
| 167 |
+
ret, frame = cap.read()
|
| 168 |
+
if not ret:
|
| 169 |
+
break
|
| 170 |
+
dets = detector.detect(frame)
|
| 171 |
+
active = tracker.update(dets)
|
| 172 |
+
frame_data.append({
|
| 173 |
+
"frame" : frame_idx,
|
| 174 |
+
"active_persons" : len(active),
|
| 175 |
+
"track_ids" : [t.track_id for t in active],
|
| 176 |
+
})
|
| 177 |
+
frame_idx += 1
|
| 178 |
+
|
| 179 |
+
cap.release()
|
| 180 |
+
tmp_path.unlink(missing_ok=True)
|
| 181 |
+
|
| 182 |
+
return JSONResponse({
|
| 183 |
+
"status" : "success",
|
| 184 |
+
"filename" : file.filename,
|
| 185 |
+
"resolution" : f"{w}x{h}",
|
| 186 |
+
"fps" : fps,
|
| 187 |
+
"frames_analyzed" : frame_idx,
|
| 188 |
+
"unique_persons" : tracker.next_id - 1,
|
| 189 |
+
"frame_summary" : frame_data[:10],
|
| 190 |
+
})
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
@app.get("/outputs")
|
| 194 |
+
def list_outputs():
|
| 195 |
+
OUTPUTS_DIR.mkdir(parents=True, exist_ok=True)
|
| 196 |
+
files = [
|
| 197 |
+
{
|
| 198 |
+
"name": f.name,
|
| 199 |
+
"size": f"{f.stat().st_size // 1024} KB",
|
| 200 |
+
}
|
| 201 |
+
for f in OUTPUTS_DIR.iterdir()
|
| 202 |
+
if f.is_file() and not f.name.startswith("tmp_")
|
| 203 |
+
]
|
| 204 |
+
return JSONResponse({
|
| 205 |
+
"status": "success",
|
| 206 |
+
"total" : len(files),
|
| 207 |
+
"files" : files,
|
| 208 |
+
})
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
# ββ AUTH ENDPOINTS ββββββββββββββββββββββββββββββββββββββββββ
|
| 213 |
+
from api.auth import generate_api_key, validate_api_key, create_access_token, get_api_key_stats
|
| 214 |
+
from fastapi import Header
|
| 215 |
+
|
| 216 |
+
@app.post("/auth/generate-key")
|
| 217 |
+
async def api_generate_key(client_name: str, tier: str = "free"):
|
| 218 |
+
"""Generate a new PhantomEye API key."""
|
| 219 |
+
if tier not in ["free", "pro"]:
|
| 220 |
+
return JSONResponse({"error": "tier must be free or pro"}, status_code=400)
|
| 221 |
+
key = generate_api_key(client_name, tier)
|
| 222 |
+
return JSONResponse({
|
| 223 |
+
"status": "success",
|
| 224 |
+
"api_key": key,
|
| 225 |
+
"client": client_name,
|
| 226 |
+
"tier": tier,
|
| 227 |
+
"rate_limit": 100 if tier == "free" else 10000,
|
| 228 |
+
"message": "Store this key securely β it will not be shown again"
|
| 229 |
+
})
|
| 230 |
+
|
| 231 |
+
@app.post("/auth/token")
|
| 232 |
+
async def api_get_token(api_key: str = Header(..., alias="X-API-Key")):
|
| 233 |
+
"""Exchange API key for JWT access token."""
|
| 234 |
+
key_data = validate_api_key(api_key)
|
| 235 |
+
token = create_access_token({"sub": key_data["client"], "tier": key_data["tier"]})
|
| 236 |
+
return JSONResponse({
|
| 237 |
+
"status": "success",
|
| 238 |
+
"access_token": token,
|
| 239 |
+
"token_type": "bearer",
|
| 240 |
+
"expires_in": "24 hours",
|
| 241 |
+
"tier": key_data["tier"]
|
| 242 |
+
})
|
| 243 |
+
|
| 244 |
+
@app.get("/auth/stats")
|
| 245 |
+
async def api_key_stats(api_key: str = Header(..., alias="X-API-Key")):
|
| 246 |
+
"""Get API key usage statistics."""
|
| 247 |
+
validate_api_key(api_key)
|
| 248 |
+
return JSONResponse(get_api_key_stats())
|
| 249 |
+
|
| 250 |
+
@app.get("/auth/validate")
|
| 251 |
+
async def api_validate_key(api_key: str = Header(..., alias="X-API-Key")):
|
| 252 |
+
"""Validate an API key and return its metadata."""
|
| 253 |
+
data = validate_api_key(api_key)
|
| 254 |
+
return JSONResponse({
|
| 255 |
+
"status": "valid",
|
| 256 |
+
"client": data["client"],
|
| 257 |
+
"tier": data["tier"],
|
| 258 |
+
"calls_today": data["calls_today"],
|
| 259 |
+
"rate_limit": data["rate_limit"],
|
| 260 |
+
"remaining": data["rate_limit"] - data["calls_today"]
|
| 261 |
+
})
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
if __name__ == "__main__":
|
| 265 |
+
import os
|
| 266 |
+
port = int(os.environ.get("PORT", 8000))
|
| 267 |
+
uvicorn.run(
|
| 268 |
+
"api.main:app",
|
| 269 |
+
host=API_HOST,
|
| 270 |
+
port=port,
|
| 271 |
+
reload=False,
|
| 272 |
)
|
app.py
CHANGED
|
@@ -1,522 +1,1207 @@
|
|
| 1 |
-
import cv2
|
| 2 |
-
import sys
|
| 3 |
-
import time
|
| 4 |
-
import
|
| 5 |
-
import
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
from core.
|
| 12 |
-
from core.
|
| 13 |
-
from core.
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
}
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
.
|
| 86 |
-
content: '';
|
| 87 |
-
position:
|
| 88 |
-
top: 0; left: 0;
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
}
|
| 115 |
-
|
| 116 |
-
.
|
| 117 |
-
font-family: '
|
| 118 |
-
font-size: 0.
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
.hero-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
}
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
.
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
}
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
color:
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
}
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
font-family: '
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
)
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
)
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 522 |
main()
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import sys
|
| 3 |
+
import time
|
| 4 |
+
import uuid
|
| 5 |
+
import numpy as np
|
| 6 |
+
import streamlit as st
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
sys.path.append(str(Path(__file__).resolve().parent))
|
| 10 |
+
|
| 11 |
+
from core.detection import PersonDetector
|
| 12 |
+
from core.tracker import ByteTracker
|
| 13 |
+
from core.analytics import BehavioralAnalyzer
|
| 14 |
+
from core.osint import OSINTAudit
|
| 15 |
+
|
| 16 |
+
st.set_page_config(
|
| 17 |
+
page_title="PhantomEye β AI Surveillance Intelligence",
|
| 18 |
+
page_icon="π",
|
| 19 |
+
layout="wide",
|
| 20 |
+
initial_sidebar_state="collapsed",
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
st.markdown("""
|
| 24 |
+
<style>
|
| 25 |
+
@import url('https://fonts.googleapis.com/css2?family=Rajdhani:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@300;400;500;600&family=Exo+2:wght@100;200;300;400;700;900&display=swap');
|
| 26 |
+
|
| 27 |
+
:root {
|
| 28 |
+
--bg-primary: #020408;
|
| 29 |
+
--bg-secondary: #050d15;
|
| 30 |
+
--bg-card: rgba(6, 18, 32, 0.85);
|
| 31 |
+
--bg-glass: rgba(0, 180, 255, 0.04);
|
| 32 |
+
--accent-blue: #00b4ff;
|
| 33 |
+
--accent-cyan: #00fff0;
|
| 34 |
+
--accent-amber: #ffb300;
|
| 35 |
+
--accent-red: #ff3355;
|
| 36 |
+
--accent-green: #00ff88;
|
| 37 |
+
--border-glow: rgba(0, 180, 255, 0.35);
|
| 38 |
+
--border-subtle: rgba(0, 180, 255, 0.12);
|
| 39 |
+
--text-primary: #e8f4ff;
|
| 40 |
+
--text-secondary: #7ab3d4;
|
| 41 |
+
--text-dim: #3a6080;
|
| 42 |
+
--grid-color: rgba(0, 180, 255, 0.035);
|
| 43 |
+
--shadow-blue: 0 0 50px rgba(0, 180, 255, 0.18);
|
| 44 |
+
--shadow-card: 0 8px 32px rgba(0, 0, 0, 0.7);
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
| 48 |
+
|
| 49 |
+
html, body, [class*="css"] {
|
| 50 |
+
font-family: 'IBM Plex Mono', monospace;
|
| 51 |
+
background: var(--bg-primary) !important;
|
| 52 |
+
color: var(--text-primary) !important;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
/* ββ TOP ACCENT BAR ββββββββββββββββββββββββββββββββ */
|
| 56 |
+
.stApp::after {
|
| 57 |
+
content: '';
|
| 58 |
+
position: fixed;
|
| 59 |
+
top: 0; left: 0; right: 0;
|
| 60 |
+
height: 2px;
|
| 61 |
+
background: linear-gradient(90deg,
|
| 62 |
+
transparent 0%,
|
| 63 |
+
var(--accent-blue) 20%,
|
| 64 |
+
var(--accent-cyan) 50%,
|
| 65 |
+
var(--accent-blue) 80%,
|
| 66 |
+
transparent 100%);
|
| 67 |
+
z-index: 9999;
|
| 68 |
+
animation: top-bar-glow 4s ease-in-out infinite alternate;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
@keyframes top-bar-glow {
|
| 72 |
+
from { opacity: 0.6; }
|
| 73 |
+
to { opacity: 1; filter: brightness(1.4); }
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
.stApp {
|
| 77 |
+
background:
|
| 78 |
+
radial-gradient(ellipse at 15% 40%, rgba(0, 60, 130, 0.18) 0%, transparent 55%),
|
| 79 |
+
radial-gradient(ellipse at 85% 15%, rgba(0, 30, 90, 0.22) 0%, transparent 50%),
|
| 80 |
+
radial-gradient(ellipse at 50% 85%, rgba(0, 40, 110, 0.12) 0%, transparent 60%),
|
| 81 |
+
linear-gradient(180deg, #020408 0%, #030a14 100%) !important;
|
| 82 |
+
min-height: 100vh;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.stApp::before {
|
| 86 |
+
content: '';
|
| 87 |
+
position: fixed;
|
| 88 |
+
top: 0; left: 0; right: 0; bottom: 0;
|
| 89 |
+
background-image:
|
| 90 |
+
linear-gradient(var(--grid-color) 1px, transparent 1px),
|
| 91 |
+
linear-gradient(90deg, var(--grid-color) 1px, transparent 1px);
|
| 92 |
+
background-size: 60px 60px;
|
| 93 |
+
pointer-events: none;
|
| 94 |
+
z-index: 0;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
/* ββ SESSION BAR βββββββββββββββββββββββββββββββββββ */
|
| 98 |
+
.session-bar {
|
| 99 |
+
display: flex;
|
| 100 |
+
justify-content: space-between;
|
| 101 |
+
align-items: center;
|
| 102 |
+
background: rgba(0, 10, 20, 0.7);
|
| 103 |
+
border: 1px solid var(--border-subtle);
|
| 104 |
+
border-radius: 6px;
|
| 105 |
+
padding: 0.45rem 1.2rem;
|
| 106 |
+
margin-bottom: 1.8rem;
|
| 107 |
+
font-family: 'IBM Plex Mono', monospace;
|
| 108 |
+
font-size: 0.68rem;
|
| 109 |
+
backdrop-filter: blur(12px);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.session-bar .sid { color: var(--text-secondary); }
|
| 113 |
+
.session-bar .sid span { color: var(--accent-blue); }
|
| 114 |
+
.session-bar .status { color: var(--accent-green); letter-spacing: 0.2em; }
|
| 115 |
+
.session-bar .status::before { content: 'β '; animation: blink 1.5s infinite; }
|
| 116 |
+
.session-bar .badge {
|
| 117 |
+
font-family: 'Rajdhani', sans-serif;
|
| 118 |
+
font-size: 0.6rem; font-weight: 700;
|
| 119 |
+
letter-spacing: 0.25em; text-transform: uppercase;
|
| 120 |
+
color: var(--accent-cyan);
|
| 121 |
+
background: rgba(0,255,240,0.08);
|
| 122 |
+
border: 1px solid rgba(0,255,240,0.3);
|
| 123 |
+
border-radius: 4px;
|
| 124 |
+
padding: 0.15rem 0.6rem;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
/* ββ HERO / LANDING βββββββββββββββββββββββββββββββ */
|
| 128 |
+
.hero-wrap {
|
| 129 |
+
display: flex; flex-direction: column;
|
| 130 |
+
align-items: center; justify-content: center;
|
| 131 |
+
min-height: 92vh; padding: 3rem 1rem;
|
| 132 |
+
position: relative;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.hero-wrap::before {
|
| 136 |
+
content: '';
|
| 137 |
+
position: absolute;
|
| 138 |
+
width: 700px; height: 700px;
|
| 139 |
+
background: radial-gradient(circle, rgba(0, 180, 255, 0.07) 0%, transparent 70%);
|
| 140 |
+
border-radius: 50%;
|
| 141 |
+
top: 50%; left: 50%;
|
| 142 |
+
transform: translate(-50%, -50%);
|
| 143 |
+
animation: pulse-glow 5s ease-in-out infinite;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
@keyframes pulse-glow {
|
| 147 |
+
0%, 100% { transform: translate(-50%, -50%) scale(1); opacity: 0.5; }
|
| 148 |
+
50% { transform: translate(-50%, -50%) scale(1.12); opacity: 1; }
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.hero-eye {
|
| 152 |
+
font-size: 5.5rem; margin-bottom: 1.5rem;
|
| 153 |
+
animation: float 5s ease-in-out infinite;
|
| 154 |
+
filter: drop-shadow(0 0 40px rgba(0, 180, 255, 0.9));
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
@keyframes float {
|
| 158 |
+
0%, 100% { transform: translateY(0px) rotate(-2deg); }
|
| 159 |
+
50% { transform: translateY(-22px) rotate(2deg); }
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.hero-title {
|
| 163 |
+
font-family: 'Exo 2', sans-serif;
|
| 164 |
+
font-size: clamp(3.5rem, 8vw, 7.5rem);
|
| 165 |
+
font-weight: 900;
|
| 166 |
+
letter-spacing: 0.15em;
|
| 167 |
+
background: linear-gradient(135deg, #ffffff 0%, var(--accent-blue) 40%, var(--accent-cyan) 100%);
|
| 168 |
+
-webkit-background-clip: text;
|
| 169 |
+
-webkit-text-fill-color: transparent;
|
| 170 |
+
background-clip: text;
|
| 171 |
+
margin-bottom: 0.5rem;
|
| 172 |
+
animation: title-reveal 1s ease-out forwards;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
@keyframes title-reveal {
|
| 176 |
+
from { opacity: 0; transform: translateY(30px); }
|
| 177 |
+
to { opacity: 1; transform: translateY(0); }
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
.hero-sub {
|
| 181 |
+
font-family: 'Rajdhani', sans-serif;
|
| 182 |
+
font-size: clamp(0.85rem, 2vw, 1.05rem);
|
| 183 |
+
font-weight: 300;
|
| 184 |
+
letter-spacing: 0.45em;
|
| 185 |
+
color: var(--text-secondary);
|
| 186 |
+
margin-bottom: 0.5rem;
|
| 187 |
+
text-transform: uppercase;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
.hero-status {
|
| 191 |
+
font-size: 0.7rem;
|
| 192 |
+
color: var(--accent-green);
|
| 193 |
+
letter-spacing: 0.3em;
|
| 194 |
+
margin-bottom: 3rem;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
.hero-status::before {
|
| 198 |
+
content: 'β ';
|
| 199 |
+
animation: blink 1.5s infinite;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
@keyframes blink {
|
| 203 |
+
0%, 100% { opacity: 1; }
|
| 204 |
+
50% { opacity: 0.15; }
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
/* ββ STATS ROW βββββββββββββββββββββββββββββββββββββ */
|
| 208 |
+
.stats-row {
|
| 209 |
+
display: flex; gap: 2rem; margin-bottom: 2.5rem;
|
| 210 |
+
justify-content: center; flex-wrap: wrap;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
.stat-item {
|
| 214 |
+
text-align: center;
|
| 215 |
+
background: var(--bg-card);
|
| 216 |
+
border: 1px solid var(--border-subtle);
|
| 217 |
+
border-radius: 10px;
|
| 218 |
+
padding: 1rem 1.8rem;
|
| 219 |
+
backdrop-filter: blur(16px);
|
| 220 |
+
min-width: 110px;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.stat-value {
|
| 224 |
+
font-family: 'Exo 2', sans-serif;
|
| 225 |
+
font-size: 1.6rem; font-weight: 900;
|
| 226 |
+
color: var(--accent-blue);
|
| 227 |
+
display: block;
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
.stat-label {
|
| 231 |
+
font-size: 0.62rem; letter-spacing: 0.25em;
|
| 232 |
+
color: var(--text-dim); text-transform: uppercase;
|
| 233 |
+
margin-top: 0.2rem; display: block;
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
/* ββ MODULE GRID βββββββββββββββββββββββββββββββββββ */
|
| 237 |
+
.module-grid {
|
| 238 |
+
display: grid;
|
| 239 |
+
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
| 240 |
+
gap: 1.4rem;
|
| 241 |
+
width: 100%;
|
| 242 |
+
max-width: 1100px;
|
| 243 |
+
margin: 0 auto 3rem;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
.mod-card {
|
| 247 |
+
background: var(--bg-card);
|
| 248 |
+
border: 1px solid var(--border-subtle);
|
| 249 |
+
border-radius: 14px;
|
| 250 |
+
padding: 2rem 1.8rem;
|
| 251 |
+
position: relative;
|
| 252 |
+
overflow: hidden;
|
| 253 |
+
transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1);
|
| 254 |
+
backdrop-filter: blur(20px);
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
.mod-card::before {
|
| 258 |
+
content: '';
|
| 259 |
+
position: absolute;
|
| 260 |
+
top: 0; left: 0; right: 0;
|
| 261 |
+
height: 2px;
|
| 262 |
+
background: linear-gradient(90deg, transparent, var(--accent-blue), var(--accent-cyan), transparent);
|
| 263 |
+
opacity: 0;
|
| 264 |
+
transition: opacity 0.3s;
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
.mod-card::after {
|
| 268 |
+
content: '';
|
| 269 |
+
position: absolute;
|
| 270 |
+
inset: 0;
|
| 271 |
+
background: radial-gradient(ellipse at top left, rgba(0,180,255,0.1) 0%, transparent 65%);
|
| 272 |
+
opacity: 0;
|
| 273 |
+
transition: opacity 0.4s;
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
.mod-card:hover {
|
| 277 |
+
border-color: var(--border-glow);
|
| 278 |
+
transform: translateY(-7px);
|
| 279 |
+
box-shadow: var(--shadow-blue), var(--shadow-card);
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
.mod-card:hover::before { opacity: 1; }
|
| 283 |
+
.mod-card:hover::after { opacity: 1; }
|
| 284 |
+
|
| 285 |
+
.mod-icon { font-size: 2rem; margin-bottom: 1rem; display: block; }
|
| 286 |
+
.mod-name {
|
| 287 |
+
font-family: 'Rajdhani', sans-serif;
|
| 288 |
+
font-size: 0.95rem; font-weight: 600;
|
| 289 |
+
letter-spacing: 0.2em; color: var(--accent-blue);
|
| 290 |
+
text-transform: uppercase; margin-bottom: 0.8rem;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
.mod-tag {
|
| 294 |
+
display: inline-block;
|
| 295 |
+
font-size: 0.58rem; letter-spacing: 0.18em;
|
| 296 |
+
color: var(--accent-cyan);
|
| 297 |
+
background: rgba(0,255,240,0.07);
|
| 298 |
+
border: 1px solid rgba(0,255,240,0.2);
|
| 299 |
+
border-radius: 3px; padding: 0.1rem 0.5rem;
|
| 300 |
+
margin-bottom: 0.7rem; text-transform: uppercase;
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
.mod-desc {
|
| 304 |
+
font-size: 0.76rem;
|
| 305 |
+
color: var(--text-secondary);
|
| 306 |
+
line-height: 1.75;
|
| 307 |
+
letter-spacing: 0.02em;
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
/* ββ SCAN LINE βββββββββββββββββββββββββββββββββββββ */
|
| 311 |
+
.scan-line {
|
| 312 |
+
width: 100%; max-width: 900px;
|
| 313 |
+
height: 1px;
|
| 314 |
+
background: linear-gradient(90deg, transparent, var(--accent-blue), var(--accent-cyan), var(--accent-blue), transparent);
|
| 315 |
+
margin: 2rem auto;
|
| 316 |
+
position: relative; overflow: hidden;
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
.scan-line::after {
|
| 320 |
+
content: '';
|
| 321 |
+
position: absolute;
|
| 322 |
+
width: 80px; height: 100%;
|
| 323 |
+
background: linear-gradient(90deg, transparent, rgba(0,255,240,0.9), transparent);
|
| 324 |
+
animation: scan 3s linear infinite;
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
@keyframes scan {
|
| 328 |
+
from { left: -80px; }
|
| 329 |
+
to { left: 100%; }
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
/* ββ APP HEADER ββββββββββββββββββββββββββββββββββββ */
|
| 333 |
+
.app-header {
|
| 334 |
+
font-family: 'Exo 2', sans-serif;
|
| 335 |
+
font-size: 1.7rem; font-weight: 700;
|
| 336 |
+
letter-spacing: 0.3em;
|
| 337 |
+
background: linear-gradient(135deg, #fff, var(--accent-blue));
|
| 338 |
+
-webkit-background-clip: text;
|
| 339 |
+
-webkit-text-fill-color: transparent;
|
| 340 |
+
background-clip: text;
|
| 341 |
+
text-align: center;
|
| 342 |
+
padding: 1.5rem 0 0.4rem;
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
.app-sub {
|
| 346 |
+
font-family: 'Rajdhani', sans-serif;
|
| 347 |
+
font-size: 0.72rem;
|
| 348 |
+
color: var(--text-dim);
|
| 349 |
+
letter-spacing: 0.45em;
|
| 350 |
+
text-align: center;
|
| 351 |
+
margin-bottom: 2rem;
|
| 352 |
+
text-transform: uppercase;
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
/* ββ MODULE NAV BUTTONS ββββββββββββββββββββββββββββ */
|
| 356 |
+
.stButton > button {
|
| 357 |
+
font-family: 'Rajdhani', sans-serif !important;
|
| 358 |
+
font-weight: 600 !important;
|
| 359 |
+
letter-spacing: 0.12em !important;
|
| 360 |
+
font-size: 0.8rem !important;
|
| 361 |
+
background: var(--bg-card) !important;
|
| 362 |
+
color: var(--accent-blue) !important;
|
| 363 |
+
border: 1px solid var(--border-subtle) !important;
|
| 364 |
+
border-radius: 8px !important;
|
| 365 |
+
padding: 0.7rem 1rem !important;
|
| 366 |
+
transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1) !important;
|
| 367 |
+
position: relative !important;
|
| 368 |
+
overflow: hidden !important;
|
| 369 |
+
text-transform: uppercase !important;
|
| 370 |
+
width: 100% !important;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
.stButton > button:hover {
|
| 374 |
+
background: rgba(0, 180, 255, 0.1) !important;
|
| 375 |
+
border-color: var(--accent-blue) !important;
|
| 376 |
+
color: var(--accent-cyan) !important;
|
| 377 |
+
box-shadow: 0 0 25px rgba(0, 180, 255, 0.25),
|
| 378 |
+
inset 0 0 20px rgba(0, 180, 255, 0.05) !important;
|
| 379 |
+
transform: translateY(-2px) !important;
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
.stButton > button[kind="primary"] {
|
| 383 |
+
background: linear-gradient(135deg, rgba(0,100,200,0.4), rgba(0,180,255,0.2)) !important;
|
| 384 |
+
border-color: var(--accent-blue) !important;
|
| 385 |
+
color: #fff !important;
|
| 386 |
+
box-shadow: 0 0 25px rgba(0,180,255,0.25) !important;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
/* ββ SECTION HEADERS βββββββββββββββββββββββββββββββ */
|
| 390 |
+
.section-hdr {
|
| 391 |
+
font-family: 'Exo 2', sans-serif;
|
| 392 |
+
font-size: 1.25rem; font-weight: 700;
|
| 393 |
+
letter-spacing: 0.25em; color: var(--accent-blue);
|
| 394 |
+
text-transform: uppercase;
|
| 395 |
+
padding: 0.5rem 0;
|
| 396 |
+
border-bottom: 1px solid var(--border-subtle);
|
| 397 |
+
margin-bottom: 0.5rem;
|
| 398 |
+
position: relative;
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
.section-hdr::after {
|
| 402 |
+
content: '';
|
| 403 |
+
position: absolute;
|
| 404 |
+
bottom: -1px; left: 0;
|
| 405 |
+
width: 90px; height: 2px;
|
| 406 |
+
background: linear-gradient(90deg, var(--accent-blue), var(--accent-cyan));
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
.section-sub {
|
| 410 |
+
font-size: 0.73rem; color: var(--text-secondary);
|
| 411 |
+
letter-spacing: 0.15em; margin-bottom: 2rem;
|
| 412 |
+
text-transform: uppercase;
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
/* ββ TERMINAL STATUS BAR βββββββββββββββββββββββββββ */
|
| 416 |
+
.terminal {
|
| 417 |
+
background: rgba(0,10,20,0.92);
|
| 418 |
+
border: 1px solid var(--border-subtle);
|
| 419 |
+
border-left: 3px solid var(--accent-blue);
|
| 420 |
+
border-radius: 6px;
|
| 421 |
+
padding: 0.8rem 1.2rem;
|
| 422 |
+
font-size: 0.72rem; color: var(--accent-green);
|
| 423 |
+
letter-spacing: 0.15em; margin-top: 1.5rem;
|
| 424 |
+
position: relative; overflow: hidden;
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
.terminal::before {
|
| 428 |
+
content: '';
|
| 429 |
+
position: absolute; inset: 0;
|
| 430 |
+
background: repeating-linear-gradient(
|
| 431 |
+
0deg,
|
| 432 |
+
transparent, transparent 2px,
|
| 433 |
+
rgba(0,255,136,0.012) 2px, rgba(0,255,136,0.012) 4px
|
| 434 |
+
);
|
| 435 |
+
pointer-events: none;
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
/* ββ STREAMLIT OVERRIDES βββββββββββββββββββββββββββ */
|
| 439 |
+
.stFileUploader {
|
| 440 |
+
background: var(--bg-card) !important;
|
| 441 |
+
border: 1px dashed var(--border-glow) !important;
|
| 442 |
+
border-radius: 10px !important;
|
| 443 |
+
padding: 1rem !important;
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
.stTextInput > div > div {
|
| 447 |
+
background: var(--bg-card) !important;
|
| 448 |
+
border: 1px solid var(--border-subtle) !important;
|
| 449 |
+
border-radius: 8px !important;
|
| 450 |
+
color: var(--text-primary) !important;
|
| 451 |
+
font-family: 'IBM Plex Mono', monospace !important;
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
.stTextInput > div > div:focus-within {
|
| 455 |
+
border-color: var(--accent-blue) !important;
|
| 456 |
+
box-shadow: 0 0 15px rgba(0,180,255,0.15) !important;
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
.stSelectbox > div > div {
|
| 460 |
+
background: var(--bg-card) !important;
|
| 461 |
+
border: 1px solid var(--border-subtle) !important;
|
| 462 |
+
border-radius: 8px !important;
|
| 463 |
+
color: var(--text-primary) !important;
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
.stNumberInput > div > div {
|
| 467 |
+
background: var(--bg-card) !important;
|
| 468 |
+
border: 1px solid var(--border-subtle) !important;
|
| 469 |
+
border-radius: 8px !important;
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
.stSlider > div > div > div { background: var(--accent-blue) !important; }
|
| 473 |
+
|
| 474 |
+
div[data-testid="metric-container"] {
|
| 475 |
+
background: var(--bg-card) !important;
|
| 476 |
+
border: 1px solid var(--border-subtle) !important;
|
| 477 |
+
border-radius: 10px !important;
|
| 478 |
+
padding: 1rem !important;
|
| 479 |
+
transition: border-color 0.3s;
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
div[data-testid="metric-container"]:hover {
|
| 483 |
+
border-color: var(--border-glow) !important;
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
div[data-testid="metric-container"] label {
|
| 487 |
+
color: var(--text-secondary) !important;
|
| 488 |
+
font-size: 0.68rem !important;
|
| 489 |
+
letter-spacing: 0.2em !important;
|
| 490 |
+
font-family: 'Rajdhani', sans-serif !important;
|
| 491 |
+
font-weight: 600 !important;
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
div[data-testid="metric-container"] div[data-testid="metric-value"] {
|
| 495 |
+
color: var(--accent-blue) !important;
|
| 496 |
+
font-family: 'Exo 2', sans-serif !important;
|
| 497 |
+
font-weight: 700 !important;
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
div[data-testid="stDataFrame"] {
|
| 501 |
+
background: var(--bg-card) !important;
|
| 502 |
+
border: 1px solid var(--border-subtle) !important;
|
| 503 |
+
border-radius: 10px !important;
|
| 504 |
+
overflow: hidden !important;
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
.stSuccess {
|
| 508 |
+
background: rgba(0,255,136,0.07) !important;
|
| 509 |
+
border: 1px solid rgba(0,255,136,0.28) !important;
|
| 510 |
+
border-radius: 8px !important;
|
| 511 |
+
color: var(--accent-green) !important;
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
.stError, .stWarning {
|
| 515 |
+
background: rgba(255,51,85,0.07) !important;
|
| 516 |
+
border: 1px solid rgba(255,51,85,0.28) !important;
|
| 517 |
+
border-radius: 8px !important;
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
.stInfo {
|
| 521 |
+
background: rgba(0,180,255,0.07) !important;
|
| 522 |
+
border: 1px solid rgba(0,180,255,0.2) !important;
|
| 523 |
+
border-radius: 8px !important;
|
| 524 |
+
color: var(--accent-blue) !important;
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
hr { border-color: var(--border-subtle) !important; margin: 1.5rem 0 !important; }
|
| 528 |
+
|
| 529 |
+
::-webkit-scrollbar { width: 4px; }
|
| 530 |
+
::-webkit-scrollbar-track { background: var(--bg-primary); }
|
| 531 |
+
::-webkit-scrollbar-thumb { background: var(--accent-blue); border-radius: 2px; opacity: 0.5; }
|
| 532 |
+
|
| 533 |
+
.stSpinner > div {
|
| 534 |
+
border-color: var(--accent-blue) transparent transparent transparent !important;
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
section[data-testid="stSidebar"] { display: none !important; }
|
| 538 |
+
#MainMenu { visibility: hidden; }
|
| 539 |
+
footer { visibility: hidden; }
|
| 540 |
+
header { visibility: hidden; }
|
| 541 |
+
|
| 542 |
+
@keyframes fadeInUp {
|
| 543 |
+
from { opacity: 0; transform: translateY(20px); }
|
| 544 |
+
to { opacity: 1; transform: translateY(0); }
|
| 545 |
+
}
|
| 546 |
+
|
| 547 |
+
.stMarkdown, .stButton, .stFileUploader {
|
| 548 |
+
animation: fadeInUp 0.4s ease-out forwards;
|
| 549 |
+
}
|
| 550 |
+
</style>
|
| 551 |
+
""", unsafe_allow_html=True)
|
| 552 |
+
|
| 553 |
+
|
| 554 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 555 |
+
# CACHED LOADERS
|
| 556 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 557 |
+
@st.cache_resource
|
| 558 |
+
def load_detector():
|
| 559 |
+
return PersonDetector()
|
| 560 |
+
|
| 561 |
+
@st.cache_resource
|
| 562 |
+
def load_osint():
|
| 563 |
+
return OSINTAudit()
|
| 564 |
+
|
| 565 |
+
@st.cache_resource
|
| 566 |
+
def load_emotion_model():
|
| 567 |
+
from core.emotion import process_frame_emotion
|
| 568 |
+
return process_frame_emotion
|
| 569 |
+
|
| 570 |
+
@st.cache_resource
|
| 571 |
+
def load_weapon_model_cached():
|
| 572 |
+
from core.weapon import load_weapon_model
|
| 573 |
+
return load_weapon_model()
|
| 574 |
+
|
| 575 |
+
|
| 576 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 577 |
+
# SESSION BAR (replaces trust bar β no limits)
|
| 578 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 579 |
+
def render_session_bar():
|
| 580 |
+
sid = st.session_state.get("session_id", "PE-XXXXXXXX")
|
| 581 |
+
st.markdown(f"""
|
| 582 |
+
<div class="session-bar">
|
| 583 |
+
<div class="sid"><span>β</span> SESSION: <span>{sid}</span></div>
|
| 584 |
+
<div class="status">ALL SYSTEMS ONLINE</div>
|
| 585 |
+
<div class="badge">OPEN ACCESS</div>
|
| 586 |
+
</div>
|
| 587 |
+
""", unsafe_allow_html=True)
|
| 588 |
+
|
| 589 |
+
|
| 590 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 591 |
+
# LANDING PAGE
|
| 592 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 593 |
+
def landing():
|
| 594 |
+
st.markdown("""
|
| 595 |
+
<div class="hero-wrap">
|
| 596 |
+
<div class="hero-eye">π</div>
|
| 597 |
+
<div class="hero-title">PHANTOMEYE</div>
|
| 598 |
+
<div class="hero-sub">AI-POWERED SURVEILLANCE INTELLIGENCE SYSTEM</div>
|
| 599 |
+
<div class="hero-status">[ SYSTEM ONLINE ] Β· OPEN ACCESS Β· BUILD v3.0</div>
|
| 600 |
+
|
| 601 |
+
<div class="stats-row">
|
| 602 |
+
<div class="stat-item">
|
| 603 |
+
<span class="stat-value">8</span>
|
| 604 |
+
<span class="stat-label">Modules</span>
|
| 605 |
+
</div>
|
| 606 |
+
<div class="stat-item">
|
| 607 |
+
<span class="stat-value">97%</span>
|
| 608 |
+
<span class="stat-label">Accuracy</span>
|
| 609 |
+
</div>
|
| 610 |
+
<div class="stat-item">
|
| 611 |
+
<span class="stat-value">9</span>
|
| 612 |
+
<span class="stat-label">Weapon Classes</span>
|
| 613 |
+
</div>
|
| 614 |
+
<div class="stat-item">
|
| 615 |
+
<span class="stat-value">CPU</span>
|
| 616 |
+
<span class="stat-label">No GPU Needed</span>
|
| 617 |
+
</div>
|
| 618 |
+
</div>
|
| 619 |
+
|
| 620 |
+
<div class="scan-line"></div>
|
| 621 |
+
|
| 622 |
+
<div class="module-grid">
|
| 623 |
+
<div class="mod-card">
|
| 624 |
+
<div class="mod-icon">π―</div>
|
| 625 |
+
<div class="mod-name">Person Detection</div>
|
| 626 |
+
<div class="mod-tag">YOLOv8-nano</div>
|
| 627 |
+
<div class="mod-desc">Detects every person in any image with confidence scores and bounding boxes in real-time.</div>
|
| 628 |
+
</div>
|
| 629 |
+
<div class="mod-card">
|
| 630 |
+
<div class="mod-icon">π₯</div>
|
| 631 |
+
<div class="mod-name">Behavioral Analytics</div>
|
| 632 |
+
<div class="mod-tag">ByteTrack Β· OpenCV</div>
|
| 633 |
+
<div class="mod-desc">Live heatmap, persistent ID tracking, dwell time and automated loitering alerts from video.</div>
|
| 634 |
+
</div>
|
| 635 |
+
<div class="mod-card">
|
| 636 |
+
<div class="mod-icon">π΅οΈ</div>
|
| 637 |
+
<div class="mod-name">OSINT Audit</div>
|
| 638 |
+
<div class="mod-tag">LBPH Face Recognition</div>
|
| 639 |
+
<div class="mod-desc">Upload a face β get a privacy exposure score 0β100 with gallery-matched identities.</div>
|
| 640 |
+
</div>
|
| 641 |
+
<div class="mod-card">
|
| 642 |
+
<div class="mod-icon">π§ </div>
|
| 643 |
+
<div class="mod-name">Emotion Intelligence</div>
|
| 644 |
+
<div class="mod-tag">DeepFace Β· TensorFlow</div>
|
| 645 |
+
<div class="mod-desc">Age, gender and dominant emotion recognition on any face image with confidence scores.</div>
|
| 646 |
+
</div>
|
| 647 |
+
<div class="mod-card">
|
| 648 |
+
<div class="mod-icon">π¬</div>
|
| 649 |
+
<div class="mod-name">NL Query Engine</div>
|
| 650 |
+
<div class="mod-tag">Groq LLaMA 3</div>
|
| 651 |
+
<div class="mod-desc">Ask in plain English or Roman Urdu β AI extracts filters and matches subjects instantly.</div>
|
| 652 |
+
</div>
|
| 653 |
+
<div class="mod-card">
|
| 654 |
+
<div class="mod-icon">β οΈ</div>
|
| 655 |
+
<div class="mod-name">Weapon Detection</div>
|
| 656 |
+
<div class="mod-tag">YOLOv8 Custom Β· 9 Classes</div>
|
| 657 |
+
<div class="mod-desc">Handgun 89.5% Β· Shotgun 96.3% Β· SMG 98.6% β trained on 714 real-world images.</div>
|
| 658 |
+
</div>
|
| 659 |
+
<div class="mod-card">
|
| 660 |
+
<div class="mod-icon">π</div>
|
| 661 |
+
<div class="mod-name">Intel Report</div>
|
| 662 |
+
<div class="mod-tag">fpdf2 Β· Cyberpunk PDF</div>
|
| 663 |
+
<div class="mod-desc">One-click classified PDF: session overview, threat alerts, subject log, query history.</div>
|
| 664 |
+
</div>
|
| 665 |
+
<div class="mod-card">
|
| 666 |
+
<div class="mod-icon">β‘</div>
|
| 667 |
+
<div class="mod-name">System Intel</div>
|
| 668 |
+
<div class="mod-tag">Live Status</div>
|
| 669 |
+
<div class="mod-desc">Module health, API endpoints, model benchmarks and full deployment information.</div>
|
| 670 |
+
</div>
|
| 671 |
+
</div>
|
| 672 |
+
</div>
|
| 673 |
+
""", unsafe_allow_html=True)
|
| 674 |
+
|
| 675 |
+
cols = st.columns([1, 2, 1])
|
| 676 |
+
with cols[1]:
|
| 677 |
+
if st.button("INITIALIZE SYSTEM β", key="enter_btn"):
|
| 678 |
+
st.session_state.page = "home"
|
| 679 |
+
st.rerun()
|
| 680 |
+
|
| 681 |
+
|
| 682 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 683 |
+
# HOME β MODULE SELECTOR
|
| 684 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 685 |
+
def home():
|
| 686 |
+
render_session_bar()
|
| 687 |
+
st.markdown('<div class="app-header">π PHANTOMEYE</div>', unsafe_allow_html=True)
|
| 688 |
+
st.markdown('<div class="app-sub">SELECT INTELLIGENCE MODULE Β· ALL SYSTEMS ACTIVE</div>', unsafe_allow_html=True)
|
| 689 |
+
|
| 690 |
+
modules = [
|
| 691 |
+
("DETECTION", "π― Detection"),
|
| 692 |
+
("ANALYTICS", "π₯ Analytics"),
|
| 693 |
+
("OSINT", "π΅οΈ OSINT"),
|
| 694 |
+
("EMOTION", "π§ Emotion"),
|
| 695 |
+
("NL QUERY", "π¬ NL Query"),
|
| 696 |
+
("WEAPON", "β οΈ Weapon"),
|
| 697 |
+
("REPORT", "π Report"),
|
| 698 |
+
("INTEL", "β‘ System"),
|
| 699 |
+
]
|
| 700 |
+
cols = st.columns(len(modules))
|
| 701 |
+
for i, (key, label) in enumerate(modules):
|
| 702 |
+
with cols[i]:
|
| 703 |
+
if st.button(label, key=f"mod_{key}"):
|
| 704 |
+
st.session_state.page = key
|
| 705 |
+
st.rerun()
|
| 706 |
+
|
| 707 |
+
st.markdown("<hr>", unsafe_allow_html=True)
|
| 708 |
+
st.markdown(
|
| 709 |
+
'<div class="terminal">[ PHANTOMEYE v3.0 ] Β· YOLOv8 loaded Β· ByteTrack active Β· '
|
| 710 |
+
'DeepFace online Β· Groq LLaMA connected Β· Weapon model ready Β· All 8 modules ACTIVE</div>',
|
| 711 |
+
unsafe_allow_html=True
|
| 712 |
+
)
|
| 713 |
+
|
| 714 |
+
|
| 715 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 716 |
+
# SHARED BACK BUTTON
|
| 717 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 718 |
+
def back_button():
|
| 719 |
+
if st.button("β BACK TO MODULES"):
|
| 720 |
+
st.session_state.page = "home"
|
| 721 |
+
st.rerun()
|
| 722 |
+
|
| 723 |
+
|
| 724 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 725 |
+
# MODULE PAGES
|
| 726 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 727 |
+
def detection_page():
|
| 728 |
+
render_session_bar()
|
| 729 |
+
back_button()
|
| 730 |
+
st.markdown('<div class="section-hdr">π― Person Detection</div>', unsafe_allow_html=True)
|
| 731 |
+
st.markdown('<div class="section-sub">YOLOv8-nano Β· CPU inference Β· upload any image</div>', unsafe_allow_html=True)
|
| 732 |
+
st.markdown(
|
| 733 |
+
'<div class="terminal">YOLOv8-nano Β· class 0 person only Β· '
|
| 734 |
+
'confidence threshold 0.4 Β· CPU optimized</div>',
|
| 735 |
+
unsafe_allow_html=True
|
| 736 |
+
)
|
| 737 |
+
|
| 738 |
+
uploaded = st.file_uploader("", type=["jpg", "jpeg", "png"], key="det_up")
|
| 739 |
+
|
| 740 |
+
if uploaded:
|
| 741 |
+
data = np.frombuffer(uploaded.read(), np.uint8)
|
| 742 |
+
image = cv2.imdecode(data, cv2.IMREAD_COLOR)
|
| 743 |
+
if image is None:
|
| 744 |
+
st.error("Cannot decode image.")
|
| 745 |
+
return
|
| 746 |
+
|
| 747 |
+
with st.spinner("SCANNING..."):
|
| 748 |
+
detector = load_detector()
|
| 749 |
+
t0 = time.time()
|
| 750 |
+
detections = detector.detect(image)
|
| 751 |
+
elapsed = round(time.time() - t0, 3)
|
| 752 |
+
annotated = detector.draw(image, detections)
|
| 753 |
+
|
| 754 |
+
c1, c2, c3, c4 = st.columns(4)
|
| 755 |
+
c1.metric("PERSONS DETECTED", len(detections))
|
| 756 |
+
c2.metric("INFERENCE TIME", f"{elapsed}s")
|
| 757 |
+
c3.metric("MODEL", "YOLOv8n")
|
| 758 |
+
c4.metric("DEVICE", "CPU")
|
| 759 |
+
|
| 760 |
+
st.image(
|
| 761 |
+
cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB),
|
| 762 |
+
caption="DETECTION OUTPUT", use_container_width=True
|
| 763 |
+
)
|
| 764 |
+
|
| 765 |
+
if detections:
|
| 766 |
+
st.markdown('<div class="section-hdr">Detection Log</div>', unsafe_allow_html=True)
|
| 767 |
+
for i, d in enumerate(detections):
|
| 768 |
+
with st.expander(f"PERSON_{i+1:03d} Β· CONF: {d['confidence']}"):
|
| 769 |
+
st.json({"id": i+1, "bbox": list(d["bbox"]), "confidence": d["confidence"]})
|
| 770 |
+
|
| 771 |
+
|
| 772 |
+
def analytics_page():
|
| 773 |
+
render_session_bar()
|
| 774 |
+
back_button()
|
| 775 |
+
st.markdown('<div class="section-hdr">π₯ Behavioral Analytics</div>', unsafe_allow_html=True)
|
| 776 |
+
st.markdown('<div class="section-sub">ByteTrack Β· heatmap Β· dwell time Β· loitering alerts</div>', unsafe_allow_html=True)
|
| 777 |
+
st.markdown(
|
| 778 |
+
'<div class="terminal">Upload video Β· persistent ID tracking Β· '
|
| 779 |
+
'real-time behavioral heatmap generation</div>',
|
| 780 |
+
unsafe_allow_html=True
|
| 781 |
+
)
|
| 782 |
+
|
| 783 |
+
uploaded = st.file_uploader("", type=["mp4", "avi", "mov"], key="ana_up")
|
| 784 |
+
|
| 785 |
+
if uploaded:
|
| 786 |
+
tmp = Path("outputs") / f"tmp_{int(time.time())}.mp4"
|
| 787 |
+
tmp.parent.mkdir(exist_ok=True)
|
| 788 |
+
with open(tmp, "wb") as f:
|
| 789 |
+
f.write(uploaded.read())
|
| 790 |
+
|
| 791 |
+
cap = cv2.VideoCapture(str(tmp))
|
| 792 |
+
fps = int(cap.get(cv2.CAP_PROP_FPS)) or 25
|
| 793 |
+
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
| 794 |
+
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
| 795 |
+
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 796 |
+
cap.release()
|
| 797 |
+
|
| 798 |
+
st.markdown(
|
| 799 |
+
f'<div class="terminal">{w}Γ{h} @ {fps}fps Β· {total} frames loaded Β· '
|
| 800 |
+
f'analysis limit: {min(total, fps*15)} frames</div>',
|
| 801 |
+
unsafe_allow_html=True
|
| 802 |
+
)
|
| 803 |
+
|
| 804 |
+
if st.button("RUN BEHAVIORAL ANALYSIS"):
|
| 805 |
+
detector = load_detector()
|
| 806 |
+
tracker = ByteTracker()
|
| 807 |
+
analyzer = BehavioralAnalyzer(w, h, fps)
|
| 808 |
+
cap = cv2.VideoCapture(str(tmp))
|
| 809 |
+
limit = min(total, fps * 15)
|
| 810 |
+
prog = st.progress(0)
|
| 811 |
+
stat = st.empty()
|
| 812 |
+
|
| 813 |
+
for i in range(limit):
|
| 814 |
+
ret, frame = cap.read()
|
| 815 |
+
if not ret: break
|
| 816 |
+
dets = detector.detect(frame)
|
| 817 |
+
active = tracker.update(dets)
|
| 818 |
+
analyzer.update(active)
|
| 819 |
+
prog.progress(int((i / limit) * 100))
|
| 820 |
+
if i % 25 == 0:
|
| 821 |
+
stat.markdown(
|
| 822 |
+
f'<div class="terminal">PROCESSING FRAME {i}/{limit} Β· '
|
| 823 |
+
f'ACTIVE PERSONS: {len(active)}</div>',
|
| 824 |
+
unsafe_allow_html=True
|
| 825 |
+
)
|
| 826 |
+
|
| 827 |
+
cap.release()
|
| 828 |
+
tmp.unlink(missing_ok=True)
|
| 829 |
+
prog.progress(100)
|
| 830 |
+
stat.empty()
|
| 831 |
+
|
| 832 |
+
s = analyzer.summary()
|
| 833 |
+
st.success("β ANALYSIS COMPLETE")
|
| 834 |
+
|
| 835 |
+
c1, c2, c3, c4 = st.columns(4)
|
| 836 |
+
c1.metric("TOTAL PERSONS", s.get("total_persons", 0))
|
| 837 |
+
c2.metric("AVG DWELL TIME", f"{s.get('avg_dwell_sec', 0)}s")
|
| 838 |
+
c3.metric("MAX DWELL TIME", f"{s.get('max_dwell_sec', 0)}s")
|
| 839 |
+
c4.metric("LOITER ALERTS", s.get("total_alerts", 0))
|
| 840 |
+
|
| 841 |
+
if s.get("total_alerts", 0) > 0:
|
| 842 |
+
st.warning(f"β LOITERING ALERT β Subject IDs: {s.get('loiterers', [])}")
|
| 843 |
+
|
| 844 |
+
heat = analyzer.get_heatmap_overlay(np.zeros((h, w, 3), dtype=np.uint8))
|
| 845 |
+
st.image(
|
| 846 |
+
cv2.cvtColor(heat, cv2.COLOR_BGR2RGB),
|
| 847 |
+
caption="BEHAVIORAL HEATMAP β RED = HIGH ACTIVITY ZONE",
|
| 848 |
+
use_container_width=True
|
| 849 |
+
)
|
| 850 |
+
|
| 851 |
+
|
| 852 |
+
def osint_page():
|
| 853 |
+
render_session_bar()
|
| 854 |
+
back_button()
|
| 855 |
+
st.markdown('<div class="section-hdr">π΅οΈ OSINT Privacy Audit</div>', unsafe_allow_html=True)
|
| 856 |
+
st.markdown('<div class="section-sub">Face upload Β· privacy exposure score Β· gallery match Β· risk report</div>', unsafe_allow_html=True)
|
| 857 |
+
st.markdown(
|
| 858 |
+
'<div class="terminal">LBPH embeddings Β· score 0β100 Β· '
|
| 859 |
+
'LOW / MEDIUM / HIGH risk classification</div>',
|
| 860 |
+
unsafe_allow_html=True
|
| 861 |
+
)
|
| 862 |
+
|
| 863 |
+
c1, c2 = st.columns([1, 1])
|
| 864 |
+
with c1:
|
| 865 |
+
query_file = st.file_uploader("", type=["jpg", "jpeg", "png"], key="osint_up")
|
| 866 |
+
with c2:
|
| 867 |
+
osint = load_osint()
|
| 868 |
+
st.metric("GALLERY SIZE", f"{len(osint.gallery)} persons")
|
| 869 |
+
st.metric("ENGINE", "LBPH Face Recognition")
|
| 870 |
+
|
| 871 |
+
if query_file and st.button("EXECUTE AUDIT"):
|
| 872 |
+
data = np.frombuffer(query_file.read(), np.uint8)
|
| 873 |
+
image = cv2.imdecode(data, cv2.IMREAD_COLOR)
|
| 874 |
+
if image is None:
|
| 875 |
+
st.error("Cannot decode image.")
|
| 876 |
+
return
|
| 877 |
+
|
| 878 |
+
with st.spinner("RUNNING OSINT AUDIT..."):
|
| 879 |
+
result = osint.audit(image, query_id=Path(query_file.name).stem)
|
| 880 |
+
|
| 881 |
+
risk = result["risk_level"]
|
| 882 |
+
score = result["exposure_score"]
|
| 883 |
+
matches = result["matches"]
|
| 884 |
+
|
| 885 |
+
c1, c2, c3 = st.columns(3)
|
| 886 |
+
c1.metric("RISK LEVEL", risk)
|
| 887 |
+
c2.metric("EXPOSURE SCORE", f"{score}/100")
|
| 888 |
+
c3.metric("MATCHES FOUND", len(matches))
|
| 889 |
+
|
| 890 |
+
st.markdown(
|
| 891 |
+
f'<div class="terminal">{result["message"]}</div>',
|
| 892 |
+
unsafe_allow_html=True
|
| 893 |
+
)
|
| 894 |
+
|
| 895 |
+
if matches:
|
| 896 |
+
st.markdown('<div class="section-hdr">Match Log</div>', unsafe_allow_html=True)
|
| 897 |
+
for m in matches:
|
| 898 |
+
st.markdown(
|
| 899 |
+
f'<div class="terminal">MATCH: {m["matched_id"]} Β· '
|
| 900 |
+
f'CONF: {m["confidence"]}% Β· SOURCE: {m["source"]}</div>',
|
| 901 |
+
unsafe_allow_html=True
|
| 902 |
+
)
|
| 903 |
+
|
| 904 |
+
vis = osint.visualize(image, result)
|
| 905 |
+
st.image(
|
| 906 |
+
cv2.cvtColor(vis, cv2.COLOR_BGR2RGB),
|
| 907 |
+
caption="OSINT VISUALIZATION",
|
| 908 |
+
use_container_width=True
|
| 909 |
+
)
|
| 910 |
+
|
| 911 |
+
|
| 912 |
+
def emotion_page():
|
| 913 |
+
render_session_bar()
|
| 914 |
+
process_frame_emotion = load_emotion_model()
|
| 915 |
+
back_button()
|
| 916 |
+
st.markdown('<div class="section-hdr">π§ Emotion Intelligence</div>', unsafe_allow_html=True)
|
| 917 |
+
st.markdown('<div class="section-sub">DeepFace Β· Age Β· Gender Β· Dominant Emotion per face</div>', unsafe_allow_html=True)
|
| 918 |
+
st.markdown(
|
| 919 |
+
'<div class="terminal">DeepFace + TensorFlow Β· 15% min face size filter Β· '
|
| 920 |
+
'multi-face support</div>',
|
| 921 |
+
unsafe_allow_html=True
|
| 922 |
+
)
|
| 923 |
+
|
| 924 |
+
uploaded = st.file_uploader("Upload image", type=["jpg", "jpeg", "png"])
|
| 925 |
+
|
| 926 |
+
if uploaded:
|
| 927 |
+
from PIL import Image
|
| 928 |
+
img = Image.open(uploaded).convert("RGB")
|
| 929 |
+
frame = np.array(img)
|
| 930 |
+
frame_bgr = frame[:, :, ::-1].copy()
|
| 931 |
+
|
| 932 |
+
with st.spinner("ANALYZING FACES..."):
|
| 933 |
+
annotated, results = process_frame_emotion(frame_bgr)
|
| 934 |
+
|
| 935 |
+
col1, col2 = st.columns(2)
|
| 936 |
+
with col1:
|
| 937 |
+
st.image(frame, caption="ORIGINAL", use_container_width=True)
|
| 938 |
+
with col2:
|
| 939 |
+
st.image(annotated[:, :, ::-1], caption="EMOTION ANALYSIS", use_container_width=True)
|
| 940 |
+
|
| 941 |
+
if results:
|
| 942 |
+
st.markdown("<hr>")
|
| 943 |
+
st.markdown('<div class="section-hdr">Detected Subjects</div>', unsafe_allow_html=True)
|
| 944 |
+
for i, r in enumerate(results):
|
| 945 |
+
emotion = r.get("dominant_emotion", "N/A").upper()
|
| 946 |
+
age = int(r.get("age", 0))
|
| 947 |
+
gender = r.get("dominant_gender", r.get("gender", "N/A"))
|
| 948 |
+
if isinstance(gender, dict):
|
| 949 |
+
gender = max(gender, key=gender.get)
|
| 950 |
+
|
| 951 |
+
c1, c2, c3 = st.columns(3)
|
| 952 |
+
c1.metric(f"SUBJECT {i+1} EMOTION", emotion)
|
| 953 |
+
c2.metric("AGE ESTIMATE", f"{age} yrs")
|
| 954 |
+
c3.metric("GENDER", gender.upper())
|
| 955 |
+
else:
|
| 956 |
+
st.warning("No faces detected in this image.")
|
| 957 |
+
else:
|
| 958 |
+
st.info("Upload a face image to begin emotion analysis.")
|
| 959 |
+
|
| 960 |
+
|
| 961 |
+
def nlquery_page():
|
| 962 |
+
render_session_bar()
|
| 963 |
+
from core.nlquery import parse_nl_query, apply_filters
|
| 964 |
+
back_button()
|
| 965 |
+
st.markdown('<div class="section-hdr">π¬ NL Query Engine</div>', unsafe_allow_html=True)
|
| 966 |
+
st.markdown('<div class="section-sub">Groq LLaMA 3 Β· English + Roman Urdu Β· structured filter extraction</div>', unsafe_allow_html=True)
|
| 967 |
+
st.markdown(
|
| 968 |
+
'<div class="terminal">llama-3.1-8b-instant Β· structured JSON extraction Β· '
|
| 969 |
+
'apply_filters() on person records</div>',
|
| 970 |
+
unsafe_allow_html=True
|
| 971 |
+
)
|
| 972 |
+
|
| 973 |
+
query = st.text_input(
|
| 974 |
+
"Enter your query",
|
| 975 |
+
placeholder="e.g. show me angry men | log jo loiter kar rahy thy"
|
| 976 |
+
)
|
| 977 |
+
|
| 978 |
+
if query:
|
| 979 |
+
with st.spinner("PARSING QUERY..."):
|
| 980 |
+
result = parse_nl_query(query)
|
| 981 |
+
|
| 982 |
+
if result['success']:
|
| 983 |
+
filters = result['filters']
|
| 984 |
+
st.success(f"β Understood: {filters['summary']}")
|
| 985 |
+
|
| 986 |
+
col1, col2, col3 = st.columns(3)
|
| 987 |
+
col1.metric("EMOTION", filters['emotion'] or "ANY")
|
| 988 |
+
col2.metric("GENDER", filters['gender'] or "ANY")
|
| 989 |
+
col3.metric("MAX AGE", filters['max_age'] or "ANY")
|
| 990 |
+
|
| 991 |
+
col4, col5 = st.columns(2)
|
| 992 |
+
col4.metric("LOITERING", "YES" if filters['loitering'] else "ANY")
|
| 993 |
+
col5.metric("MIN DWELL", f"{filters['min_dwell_seconds']}s" if filters['min_dwell_seconds'] else "ANY")
|
| 994 |
+
|
| 995 |
+
st.markdown("<hr>")
|
| 996 |
+
st.markdown('<div class="section-hdr">Simulate Against Sample Data</div>', unsafe_allow_html=True)
|
| 997 |
+
|
| 998 |
+
sample_records = [
|
| 999 |
+
{"id": 1, "emotion": "angry", "gender": "Man", "age": 28, "dwell_seconds": 45, "loitering": False},
|
| 1000 |
+
{"id": 2, "emotion": "neutral", "gender": "Woman", "age": 22, "dwell_seconds": 180, "loitering": True},
|
| 1001 |
+
{"id": 3, "emotion": "happy", "gender": "Man", "age": 35, "dwell_seconds": 20, "loitering": False},
|
| 1002 |
+
{"id": 4, "emotion": "angry", "gender": "Man", "age": 41, "dwell_seconds": 200, "loitering": True},
|
| 1003 |
+
{"id": 5, "emotion": "sad", "gender": "Woman", "age": 19, "dwell_seconds": 90, "loitering": False},
|
| 1004 |
+
{"id": 6, "emotion": "fear", "gender": "Man", "age": 26, "dwell_seconds": 310, "loitering": True},
|
| 1005 |
+
]
|
| 1006 |
+
|
| 1007 |
+
matched = apply_filters(sample_records, filters)
|
| 1008 |
+
|
| 1009 |
+
if matched:
|
| 1010 |
+
st.success(f"{len(matched)} subject(s) matched out of {len(sample_records)}")
|
| 1011 |
+
import pandas as pd
|
| 1012 |
+
st.dataframe(pd.DataFrame(matched), use_container_width=True)
|
| 1013 |
+
else:
|
| 1014 |
+
st.warning("No subjects matched this query in sample data.")
|
| 1015 |
+
else:
|
| 1016 |
+
st.error(f"Query parse failed: {result['error']}")
|
| 1017 |
+
else:
|
| 1018 |
+
st.info("Type a query above β English or Roman Urdu both work.")
|
| 1019 |
+
|
| 1020 |
+
|
| 1021 |
+
def weapon_page():
|
| 1022 |
+
render_session_bar()
|
| 1023 |
+
back_button()
|
| 1024 |
+
st.markdown('<div class="section-hdr">β οΈ Weapon Detection</div>', unsafe_allow_html=True)
|
| 1025 |
+
st.markdown('<div class="section-sub">YOLOv8 Custom Β· 9 classes Β· Handgun Β· Knife Β· Shotgun Β· SMG Β· Rifle Β· Sword</div>', unsafe_allow_html=True)
|
| 1026 |
+
st.markdown(
|
| 1027 |
+
'<div class="terminal">mAP50: 53.2% Β· Handgun: 89.5% Β· Shotgun: 96.3% Β· '
|
| 1028 |
+
'SMG: 98.6% Β· trained on 714 real-world images</div>',
|
| 1029 |
+
unsafe_allow_html=True
|
| 1030 |
+
)
|
| 1031 |
+
|
| 1032 |
+
uploaded = st.file_uploader("Upload image", type=["jpg", "jpeg", "png"])
|
| 1033 |
+
|
| 1034 |
+
if uploaded:
|
| 1035 |
+
from PIL import Image
|
| 1036 |
+
from core.weapon import detect_weapons
|
| 1037 |
+
|
| 1038 |
+
img = Image.open(uploaded).convert("RGB")
|
| 1039 |
+
frame = np.array(img)
|
| 1040 |
+
frame_bgr = frame[:, :, ::-1].copy()
|
| 1041 |
+
model = load_weapon_model_cached()
|
| 1042 |
+
|
| 1043 |
+
with st.spinner("SCANNING FOR THREATS..."):
|
| 1044 |
+
annotated, detections = detect_weapons(frame_bgr, model)
|
| 1045 |
+
|
| 1046 |
+
col1, col2 = st.columns(2)
|
| 1047 |
+
with col1:
|
| 1048 |
+
st.image(frame, caption="ORIGINAL", use_container_width=True)
|
| 1049 |
+
with col2:
|
| 1050 |
+
st.image(annotated[:, :, ::-1], caption="THREAT ANALYSIS", use_container_width=True)
|
| 1051 |
+
|
| 1052 |
+
st.markdown("<hr>")
|
| 1053 |
+
if detections:
|
| 1054 |
+
st.error(f"β THREAT DETECTED β {len(detections)} weapon(s) found!")
|
| 1055 |
+
st.markdown('<div class="section-hdr">Detected Threats</div>', unsafe_allow_html=True)
|
| 1056 |
+
for d in detections:
|
| 1057 |
+
c1, c2 = st.columns(2)
|
| 1058 |
+
c1.metric("WEAPON CLASS", d['class_name'])
|
| 1059 |
+
c2.metric("CONFIDENCE", f"{d['confidence']:.0%}")
|
| 1060 |
+
else:
|
| 1061 |
+
st.success("β NO WEAPONS DETECTED β Scene clear")
|
| 1062 |
+
else:
|
| 1063 |
+
st.info("Upload an image to scan for weapons.")
|
| 1064 |
+
|
| 1065 |
+
|
| 1066 |
+
def report_page():
|
| 1067 |
+
render_session_bar()
|
| 1068 |
+
from core.reporter import generate_report
|
| 1069 |
+
back_button()
|
| 1070 |
+
st.markdown('<div class="section-hdr">π Intelligence Report</div>', unsafe_allow_html=True)
|
| 1071 |
+
st.markdown('<div class="section-sub">Branded cyberpunk PDF Β· CLASSIFIED header Β· one-click download</div>', unsafe_allow_html=True)
|
| 1072 |
+
st.markdown(
|
| 1073 |
+
'<div class="terminal">fpdf2 Β· dark bg Β· green text Β· threat alerts in red Β· '
|
| 1074 |
+
'session + subject + query log</div>',
|
| 1075 |
+
unsafe_allow_html=True
|
| 1076 |
+
)
|
| 1077 |
+
|
| 1078 |
+
st.markdown("### SESSION DATA")
|
| 1079 |
+
col1, col2 = st.columns(2)
|
| 1080 |
+
with col1:
|
| 1081 |
+
session_id = st.text_input("Session ID", value=st.session_state.get("session_id", "PE-SESSION-001"))
|
| 1082 |
+
total_persons = st.number_input("Total Persons", min_value=0, value=5)
|
| 1083 |
+
duration = st.number_input("Duration (sec)", min_value=0, value=300)
|
| 1084 |
+
with col2:
|
| 1085 |
+
loitering_alerts = st.number_input("Loitering Alerts", min_value=0, value=1)
|
| 1086 |
+
nl_query = st.text_input("NL Query (opt)", value="")
|
| 1087 |
+
nl_result = st.text_input("NL Result (opt)", value="")
|
| 1088 |
+
|
| 1089 |
+
st.markdown("### DETECTED SUBJECTS")
|
| 1090 |
+
num_subjects = st.slider("Number of subjects", 1, 10, 3)
|
| 1091 |
+
detections = []
|
| 1092 |
+
for i in range(num_subjects):
|
| 1093 |
+
c1, c2, c3, c4, c5, _ = st.columns(6)
|
| 1094 |
+
detections.append({
|
| 1095 |
+
"id": i + 1,
|
| 1096 |
+
"emotion": c1.selectbox(f"Emotion {i+1}", ["neutral","angry","happy","sad","fear","surprise"], key=f"em_{i}"),
|
| 1097 |
+
"gender": c2.selectbox(f"Gender {i+1}", ["Man","Woman"], key=f"gen_{i}"),
|
| 1098 |
+
"age": c3.number_input(f"Age {i+1}", 10, 80, 25, key=f"age_{i}"),
|
| 1099 |
+
"dwell_seconds":c4.number_input(f"Dwell {i+1}",0, 600, 60, key=f"dw_{i}"),
|
| 1100 |
+
"loitering": c5.checkbox(f"Loiter {i+1}", key=f"lo_{i}"),
|
| 1101 |
+
})
|
| 1102 |
+
|
| 1103 |
+
st.markdown("### WEAPON DETECTIONS")
|
| 1104 |
+
has_weapon = st.checkbox("Weapon detected in session?")
|
| 1105 |
+
weapon_detections = []
|
| 1106 |
+
if has_weapon:
|
| 1107 |
+
wc1, wc2 = st.columns(2)
|
| 1108 |
+
weapon_class = wc1.selectbox("Weapon Class", ["Handgun","Knife","Shotgun","SMG","Automatic Rifle","Sniper","Sword"])
|
| 1109 |
+
weapon_conf = wc2.slider("Confidence", 0.3, 1.0, 0.85)
|
| 1110 |
+
weapon_detections.append({"class_name": weapon_class, "confidence": weapon_conf})
|
| 1111 |
+
|
| 1112 |
+
st.markdown("<hr>")
|
| 1113 |
+
if st.button("GENERATE PDF REPORT", type="primary"):
|
| 1114 |
+
data = {
|
| 1115 |
+
"session_id": session_id,
|
| 1116 |
+
"total_persons": total_persons,
|
| 1117 |
+
"duration_seconds": duration,
|
| 1118 |
+
"loitering_alerts": loitering_alerts,
|
| 1119 |
+
"weapon_detections":weapon_detections,
|
| 1120 |
+
"detections": detections,
|
| 1121 |
+
"heatmap_img": None,
|
| 1122 |
+
"frame_sample": None,
|
| 1123 |
+
"nl_query": nl_query,
|
| 1124 |
+
"nl_result": nl_result,
|
| 1125 |
+
}
|
| 1126 |
+
with st.spinner("GENERATING CLASSIFIED REPORT..."):
|
| 1127 |
+
path = generate_report(data)
|
| 1128 |
+
|
| 1129 |
+
with open(path, "rb") as f:
|
| 1130 |
+
pdf_bytes = f.read()
|
| 1131 |
+
|
| 1132 |
+
st.success("β Report generated successfully!")
|
| 1133 |
+
st.download_button(
|
| 1134 |
+
label="β¬ DOWNLOAD PDF REPORT",
|
| 1135 |
+
data=pdf_bytes,
|
| 1136 |
+
file_name=f"phantomeye_report_{session_id}.pdf",
|
| 1137 |
+
mime="application/pdf"
|
| 1138 |
+
)
|
| 1139 |
+
|
| 1140 |
+
|
| 1141 |
+
def intel_page():
|
| 1142 |
+
render_session_bar()
|
| 1143 |
+
back_button()
|
| 1144 |
+
st.markdown('<div class="section-hdr">β‘ System Intelligence</div>', unsafe_allow_html=True)
|
| 1145 |
+
st.markdown('<div class="section-sub">Module health Β· API endpoints Β· model benchmarks</div>', unsafe_allow_html=True)
|
| 1146 |
+
|
| 1147 |
+
c1, c2, c3, c4 = st.columns(4)
|
| 1148 |
+
c1.metric("SYSTEM", "PhantomEye")
|
| 1149 |
+
c2.metric("VERSION", "v3.0.0")
|
| 1150 |
+
c3.metric("STATUS", "ONLINE")
|
| 1151 |
+
c4.metric("MODULES", "8 ACTIVE")
|
| 1152 |
+
|
| 1153 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
| 1154 |
+
|
| 1155 |
+
modules_info = [
|
| 1156 |
+
("DETECTION", "YOLOv8-nano", "Person detection Β· class 0 Β· confidence 0.4+ Β· CPU optimized"),
|
| 1157 |
+
("ANALYTICS", "ByteTrack", "Persistent ID tracking Β· heatmap Β· dwell time Β· loitering alerts"),
|
| 1158 |
+
("OSINT", "LBPH Face", "Privacy exposure score 0β100 Β· gallery match Β· LOW/MEDIUM/HIGH risk"),
|
| 1159 |
+
("EMOTION", "DeepFace + TF", "Age Β· Gender Β· Dominant Emotion Β· 15% face size filter"),
|
| 1160 |
+
("NL QUERY", "Groq LLaMA 3", "llama-3.1-8b-instant Β· English + Roman Urdu Β· JSON filter extraction"),
|
| 1161 |
+
("WEAPON", "YOLOv8 Custom", "9 classes Β· mAP50 53.2% Β· Handgun 89.5% Β· Shotgun 96.3% Β· SMG 98.6%"),
|
| 1162 |
+
("REPORT", "fpdf2", "Dark cyberpunk PDF Β· CLASSIFIED header Β· threat + subject + query log"),
|
| 1163 |
+
("API", "FastAPI", "12 endpoints Β· OAS 3.1 Β· CORS enabled Β· Railway deployed"),
|
| 1164 |
+
]
|
| 1165 |
+
|
| 1166 |
+
for name, tech, desc in modules_info:
|
| 1167 |
+
with st.expander(f"{'β'} {name} Β· {tech} Β· ACTIVE"):
|
| 1168 |
+
st.markdown(f'<div class="terminal">{desc}</div>', unsafe_allow_html=True)
|
| 1169 |
+
|
| 1170 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
| 1171 |
+
st.json({
|
| 1172 |
+
"author": "Abu-Sameer-66",
|
| 1173 |
+
"github": "https://github.com/Abu-Sameer-66/PhantomEye",
|
| 1174 |
+
"huggingface": "https://abu-sameer-66-phantomeye.hf.space",
|
| 1175 |
+
"railway_api": "https://phantomeye-production.up.railway.app",
|
| 1176 |
+
"api_docs": "https://phantomeye-production.up.railway.app/docs",
|
| 1177 |
+
"stack": ["Python 3.10", "YOLOv8", "DeepFace", "FastAPI", "Streamlit", "Groq"],
|
| 1178 |
+
"status": "online",
|
| 1179 |
+
"access": "open β no auth required",
|
| 1180 |
+
})
|
| 1181 |
+
|
| 1182 |
+
|
| 1183 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1184 |
+
# MAIN ROUTER
|
| 1185 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1186 |
+
def main():
|
| 1187 |
+
if "page" not in st.session_state:
|
| 1188 |
+
st.session_state.page = "landing"
|
| 1189 |
+
if "session_id" not in st.session_state:
|
| 1190 |
+
st.session_state.session_id = "PE-" + str(uuid.uuid4())[:8].upper()
|
| 1191 |
+
|
| 1192 |
+
page = st.session_state.page
|
| 1193 |
+
|
| 1194 |
+
if page == "landing": landing()
|
| 1195 |
+
elif page == "home": home()
|
| 1196 |
+
elif page == "DETECTION": detection_page()
|
| 1197 |
+
elif page == "ANALYTICS": analytics_page()
|
| 1198 |
+
elif page == "OSINT": osint_page()
|
| 1199 |
+
elif page == "EMOTION": emotion_page()
|
| 1200 |
+
elif page == "NL QUERY": nlquery_page()
|
| 1201 |
+
elif page == "WEAPON": weapon_page()
|
| 1202 |
+
elif page == "REPORT": report_page()
|
| 1203 |
+
elif page == "INTEL": intel_page()
|
| 1204 |
+
|
| 1205 |
+
|
| 1206 |
+
if __name__ == "__main__":
|
| 1207 |
main()
|
config.py
CHANGED
|
@@ -1,36 +1,36 @@
|
|
| 1 |
-
import os
|
| 2 |
-
from pathlib import Path
|
| 3 |
-
|
| 4 |
-
BASE_DIR = Path(__file__).resolve().parent
|
| 5 |
-
|
| 6 |
-
# Paths
|
| 7 |
-
DATA_DIR = BASE_DIR / "data"
|
| 8 |
-
VIDEOS_DIR = DATA_DIR / "videos"
|
| 9 |
-
GALLERY_DIR = DATA_DIR / "gallery"
|
| 10 |
-
MODELS_DIR = BASE_DIR / "models"
|
| 11 |
-
OUTPUTS_DIR = BASE_DIR / "outputs"
|
| 12 |
-
|
| 13 |
-
# Detection settings
|
| 14 |
-
DETECTION_CONF = 0.45
|
| 15 |
-
DETECTION_MODEL = "yolov8n.pt"
|
| 16 |
-
DEVICE = "cpu"
|
| 17 |
-
|
| 18 |
-
# Tracking settings
|
| 19 |
-
TRACK_MAX_AGE = 30
|
| 20 |
-
TRACK_MIN_HITS = 3
|
| 21 |
-
TRACK_IOU_THRESH = 0.3
|
| 22 |
-
|
| 23 |
-
# Re-ID settings
|
| 24 |
-
REID_MODEL = "osnet_x0_25"
|
| 25 |
-
REID_THRESHOLD = 0.65
|
| 26 |
-
|
| 27 |
-
# API settings
|
| 28 |
-
API_HOST = "0.0.0.0"
|
| 29 |
-
API_PORT = 8000
|
| 30 |
-
SECRET_KEY = "phantomeye-secret-key-change-in-production"
|
| 31 |
-
|
| 32 |
-
# Analytics settings
|
| 33 |
-
HEATMAP_ALPHA = 0.6
|
| 34 |
-
DWELL_TIME_THRESHOLD = 30
|
| 35 |
-
|
| 36 |
print(f"[PhantomEye] Config loaded β Base: {BASE_DIR}")
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
BASE_DIR = Path(__file__).resolve().parent
|
| 5 |
+
|
| 6 |
+
# Paths
|
| 7 |
+
DATA_DIR = BASE_DIR / "data"
|
| 8 |
+
VIDEOS_DIR = DATA_DIR / "videos"
|
| 9 |
+
GALLERY_DIR = DATA_DIR / "gallery"
|
| 10 |
+
MODELS_DIR = BASE_DIR / "models"
|
| 11 |
+
OUTPUTS_DIR = BASE_DIR / "outputs"
|
| 12 |
+
|
| 13 |
+
# Detection settings
|
| 14 |
+
DETECTION_CONF = 0.45
|
| 15 |
+
DETECTION_MODEL = "yolov8n.pt"
|
| 16 |
+
DEVICE = "cpu"
|
| 17 |
+
|
| 18 |
+
# Tracking settings
|
| 19 |
+
TRACK_MAX_AGE = 30
|
| 20 |
+
TRACK_MIN_HITS = 3
|
| 21 |
+
TRACK_IOU_THRESH = 0.3
|
| 22 |
+
|
| 23 |
+
# Re-ID settings
|
| 24 |
+
REID_MODEL = "osnet_x0_25"
|
| 25 |
+
REID_THRESHOLD = 0.65
|
| 26 |
+
|
| 27 |
+
# API settings
|
| 28 |
+
API_HOST = "0.0.0.0"
|
| 29 |
+
API_PORT = 8000
|
| 30 |
+
SECRET_KEY = "phantomeye-secret-key-change-in-production"
|
| 31 |
+
|
| 32 |
+
# Analytics settings
|
| 33 |
+
HEATMAP_ALPHA = 0.6
|
| 34 |
+
DWELL_TIME_THRESHOLD = 30
|
| 35 |
+
|
| 36 |
print(f"[PhantomEye] Config loaded β Base: {BASE_DIR}")
|
core/analytics.py
CHANGED
|
@@ -1,241 +1,241 @@
|
|
| 1 |
-
import cv2
|
| 2 |
-
import time
|
| 3 |
-
import numpy as np
|
| 4 |
-
from pathlib import Path
|
| 5 |
-
from collections import defaultdict
|
| 6 |
-
|
| 7 |
-
import sys
|
| 8 |
-
sys.path.append(str(Path(__file__).resolve().parent.parent))
|
| 9 |
-
|
| 10 |
-
from core.detection import PersonDetector
|
| 11 |
-
from core.tracker import ByteTracker
|
| 12 |
-
from config import OUTPUTS_DIR, HEATMAP_ALPHA, DWELL_TIME_THRESHOLD
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
class BehavioralAnalyzer:
|
| 16 |
-
|
| 17 |
-
def __init__(self, frame_width: int, frame_height: int, fps: int = 25):
|
| 18 |
-
self.width = frame_width
|
| 19 |
-
self.height = frame_height
|
| 20 |
-
self.fps = fps
|
| 21 |
-
self.heatmap = np.zeros((frame_height, frame_width), dtype=np.float32)
|
| 22 |
-
self.dwell = defaultdict(int)
|
| 23 |
-
self.last_pos = {}
|
| 24 |
-
self.alerts = []
|
| 25 |
-
self.frame_num = 0
|
| 26 |
-
print("[PhantomEye] Behavioral analyzer ready")
|
| 27 |
-
|
| 28 |
-
def update(self, active_tracks: list):
|
| 29 |
-
self.frame_num += 1
|
| 30 |
-
|
| 31 |
-
for trk in active_tracks:
|
| 32 |
-
cx, cy = trk.center()
|
| 33 |
-
|
| 34 |
-
cx = max(0, min(cx, self.width - 1))
|
| 35 |
-
cy = max(0, min(cy, self.height - 1))
|
| 36 |
-
|
| 37 |
-
cv2.circle(
|
| 38 |
-
self.heatmap,
|
| 39 |
-
(cx, cy), 25, 1.0, -1
|
| 40 |
-
)
|
| 41 |
-
|
| 42 |
-
self.dwell[trk.track_id] += 1
|
| 43 |
-
|
| 44 |
-
dwell_secs = self.dwell[trk.track_id] / self.fps
|
| 45 |
-
if dwell_secs >= DWELL_TIME_THRESHOLD:
|
| 46 |
-
already = any(
|
| 47 |
-
a["id"] == trk.track_id and a["type"] == "loitering"
|
| 48 |
-
for a in self.alerts
|
| 49 |
-
)
|
| 50 |
-
if not already:
|
| 51 |
-
self.alerts.append({
|
| 52 |
-
"type" : "loitering",
|
| 53 |
-
"id" : trk.track_id,
|
| 54 |
-
"bbox" : trk.bbox,
|
| 55 |
-
"dwell_sec": round(dwell_secs, 1),
|
| 56 |
-
"frame" : self.frame_num,
|
| 57 |
-
})
|
| 58 |
-
|
| 59 |
-
def get_heatmap_overlay(self, frame: np.ndarray) -> np.ndarray:
|
| 60 |
-
if self.heatmap.max() == 0:
|
| 61 |
-
return frame
|
| 62 |
-
|
| 63 |
-
normalized = cv2.normalize(
|
| 64 |
-
self.heatmap, None, 0, 255, cv2.NORM_MINMAX
|
| 65 |
-
)
|
| 66 |
-
heat_uint8 = normalized.astype(np.uint8)
|
| 67 |
-
heat_color = cv2.applyColorMap(heat_uint8, cv2.COLORMAP_JET)
|
| 68 |
-
blurred = cv2.GaussianBlur(heat_color, (31, 31), 0)
|
| 69 |
-
|
| 70 |
-
mask = heat_uint8 > 10
|
| 71 |
-
output = frame.copy()
|
| 72 |
-
output[mask] = cv2.addWeighted(
|
| 73 |
-
frame, 1 - HEATMAP_ALPHA,
|
| 74 |
-
blurred, HEATMAP_ALPHA, 0
|
| 75 |
-
)[mask]
|
| 76 |
-
|
| 77 |
-
return output
|
| 78 |
-
|
| 79 |
-
def draw_alerts(self, frame: np.ndarray, active_tracks: list) -> np.ndarray:
|
| 80 |
-
out = frame.copy()
|
| 81 |
-
|
| 82 |
-
active_ids = {t.track_id for t in active_tracks}
|
| 83 |
-
recent = [
|
| 84 |
-
a for a in self.alerts
|
| 85 |
-
if a["id"] in active_ids and a["type"] == "loitering"
|
| 86 |
-
]
|
| 87 |
-
|
| 88 |
-
for alert in recent:
|
| 89 |
-
x1, y1, x2, y2 = alert["bbox"]
|
| 90 |
-
cv2.rectangle(out, (x1, y1), (x2, y2), (0, 0, 255), 3)
|
| 91 |
-
label = f"ALERT ID:{alert['id']} {alert['dwell_sec']}s"
|
| 92 |
-
cv2.putText(
|
| 93 |
-
out, label,
|
| 94 |
-
(x1, y1 - 10),
|
| 95 |
-
cv2.FONT_HERSHEY_SIMPLEX, 0.6,
|
| 96 |
-
(0, 0, 255), 2, cv2.LINE_AA
|
| 97 |
-
)
|
| 98 |
-
|
| 99 |
-
return out
|
| 100 |
-
|
| 101 |
-
def draw_dwell_info(self, frame: np.ndarray, active_tracks: list) -> np.ndarray:
|
| 102 |
-
out = frame.copy()
|
| 103 |
-
|
| 104 |
-
for trk in active_tracks:
|
| 105 |
-
x1, y1, x2, y2 = trk.bbox
|
| 106 |
-
secs = round(self.dwell[trk.track_id] / self.fps, 1)
|
| 107 |
-
cv2.putText(
|
| 108 |
-
out,
|
| 109 |
-
f"{secs}s",
|
| 110 |
-
(x1, y2 + 16),
|
| 111 |
-
cv2.FONT_HERSHEY_SIMPLEX, 0.45,
|
| 112 |
-
(255, 255, 0), 1, cv2.LINE_AA
|
| 113 |
-
)
|
| 114 |
-
|
| 115 |
-
return out
|
| 116 |
-
|
| 117 |
-
def summary(self) -> dict:
|
| 118 |
-
if not self.dwell:
|
| 119 |
-
return {}
|
| 120 |
-
|
| 121 |
-
dwell_secs = {
|
| 122 |
-
k: round(v / self.fps, 1)
|
| 123 |
-
for k, v in self.dwell.items()
|
| 124 |
-
}
|
| 125 |
-
return {
|
| 126 |
-
"total_persons" : len(self.dwell),
|
| 127 |
-
"total_alerts" : len(self.alerts),
|
| 128 |
-
"avg_dwell_sec" : round(
|
| 129 |
-
sum(dwell_secs.values()) / len(dwell_secs), 2
|
| 130 |
-
),
|
| 131 |
-
"max_dwell_sec" : max(dwell_secs.values()),
|
| 132 |
-
"loiterers" : [
|
| 133 |
-
a["id"] for a in self.alerts
|
| 134 |
-
if a["type"] == "loitering"
|
| 135 |
-
],
|
| 136 |
-
}
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
def run_analytics(video_path: str, save: bool = True, show: bool = True):
|
| 140 |
-
|
| 141 |
-
video_path = Path(video_path)
|
| 142 |
-
if not video_path.exists():
|
| 143 |
-
print(f"[ERROR] Video not found: {video_path}")
|
| 144 |
-
return
|
| 145 |
-
|
| 146 |
-
detector = PersonDetector()
|
| 147 |
-
tracker = ByteTracker()
|
| 148 |
-
cap = cv2.VideoCapture(str(video_path))
|
| 149 |
-
|
| 150 |
-
if not cap.isOpened():
|
| 151 |
-
print(f"[ERROR] Cannot open: {video_path}")
|
| 152 |
-
return
|
| 153 |
-
|
| 154 |
-
fps = int(cap.get(cv2.CAP_PROP_FPS)) or 25
|
| 155 |
-
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
| 156 |
-
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
| 157 |
-
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 158 |
-
|
| 159 |
-
analyzer = BehavioralAnalyzer(width, height, fps)
|
| 160 |
-
|
| 161 |
-
print(f"[PhantomEye] {video_path.name} β {width}x{height} {fps}fps")
|
| 162 |
-
|
| 163 |
-
writer = None
|
| 164 |
-
if save:
|
| 165 |
-
OUTPUTS_DIR.mkdir(parents=True, exist_ok=True)
|
| 166 |
-
out_path = OUTPUTS_DIR / (video_path.stem + "_analytics.mp4")
|
| 167 |
-
writer = cv2.VideoWriter(
|
| 168 |
-
str(out_path),
|
| 169 |
-
cv2.VideoWriter_fourcc(*"mp4v"),
|
| 170 |
-
fps, (width, height)
|
| 171 |
-
)
|
| 172 |
-
print(f"[PhantomEye] Saving to: {out_path}")
|
| 173 |
-
|
| 174 |
-
show_heat = False
|
| 175 |
-
start_time = time.time()
|
| 176 |
-
|
| 177 |
-
while True:
|
| 178 |
-
ret, frame = cap.read()
|
| 179 |
-
if not ret:
|
| 180 |
-
break
|
| 181 |
-
|
| 182 |
-
detections = detector.detect(frame)
|
| 183 |
-
active_tracks = tracker.update(detections)
|
| 184 |
-
analyzer.update(active_tracks)
|
| 185 |
-
|
| 186 |
-
if show_heat:
|
| 187 |
-
display = analyzer.get_heatmap_overlay(frame)
|
| 188 |
-
else:
|
| 189 |
-
display = frame.copy()
|
| 190 |
-
|
| 191 |
-
display = tracker.draw(display, active_tracks)
|
| 192 |
-
display = analyzer.draw_dwell_info(display, active_tracks)
|
| 193 |
-
display = analyzer.draw_alerts(display, active_tracks)
|
| 194 |
-
|
| 195 |
-
mode_text = "MODE: HEATMAP [H]" if show_heat else "MODE: TRACKING [H]"
|
| 196 |
-
cv2.putText(
|
| 197 |
-
display, mode_text,
|
| 198 |
-
(width - 220, height - 12),
|
| 199 |
-
cv2.FONT_HERSHEY_SIMPLEX, 0.45,
|
| 200 |
-
(200, 200, 200), 1, cv2.LINE_AA
|
| 201 |
-
)
|
| 202 |
-
|
| 203 |
-
if writer:
|
| 204 |
-
writer.write(display)
|
| 205 |
-
|
| 206 |
-
if show:
|
| 207 |
-
cv2.imshow("PhantomEye β Analytics [H=heatmap Q=quit]", display)
|
| 208 |
-
key = cv2.waitKey(1) & 0xFF
|
| 209 |
-
if key == ord("q"):
|
| 210 |
-
break
|
| 211 |
-
elif key == ord("h"):
|
| 212 |
-
show_heat = not show_heat
|
| 213 |
-
|
| 214 |
-
if tracker.frame_count % 30 == 0:
|
| 215 |
-
print(
|
| 216 |
-
f"\r[Frame {tracker.frame_count}/{total}]"
|
| 217 |
-
f" Active: {len(active_tracks)}"
|
| 218 |
-
f" Alerts: {len(analyzer.alerts)}"
|
| 219 |
-
f" Time: {time.time()-start_time:.1f}s",
|
| 220 |
-
end=""
|
| 221 |
-
)
|
| 222 |
-
|
| 223 |
-
cap.release()
|
| 224 |
-
if writer:
|
| 225 |
-
writer.release()
|
| 226 |
-
cv2.destroyAllWindows()
|
| 227 |
-
|
| 228 |
-
summary = analyzer.summary()
|
| 229 |
-
print(f"\n\n[PhantomEye] Analytics done!")
|
| 230 |
-
print(f" Total persons tracked : {summary.get('total_persons', 0)}")
|
| 231 |
-
print(f" Avg dwell time : {summary.get('avg_dwell_sec', 0)}s")
|
| 232 |
-
print(f" Max dwell time : {summary.get('max_dwell_sec', 0)}s")
|
| 233 |
-
print(f" Loitering alerts : {summary.get('total_alerts', 0)}")
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
if __name__ == "__main__":
|
| 237 |
-
import sys
|
| 238 |
-
if len(sys.argv) < 2:
|
| 239 |
-
print("Usage: python core/analytics.py <video_path>")
|
| 240 |
-
else:
|
| 241 |
run_analytics(sys.argv[1])
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import time
|
| 3 |
+
import numpy as np
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from collections import defaultdict
|
| 6 |
+
|
| 7 |
+
import sys
|
| 8 |
+
sys.path.append(str(Path(__file__).resolve().parent.parent))
|
| 9 |
+
|
| 10 |
+
from core.detection import PersonDetector
|
| 11 |
+
from core.tracker import ByteTracker
|
| 12 |
+
from config import OUTPUTS_DIR, HEATMAP_ALPHA, DWELL_TIME_THRESHOLD
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class BehavioralAnalyzer:
|
| 16 |
+
|
| 17 |
+
def __init__(self, frame_width: int, frame_height: int, fps: int = 25):
|
| 18 |
+
self.width = frame_width
|
| 19 |
+
self.height = frame_height
|
| 20 |
+
self.fps = fps
|
| 21 |
+
self.heatmap = np.zeros((frame_height, frame_width), dtype=np.float32)
|
| 22 |
+
self.dwell = defaultdict(int)
|
| 23 |
+
self.last_pos = {}
|
| 24 |
+
self.alerts = []
|
| 25 |
+
self.frame_num = 0
|
| 26 |
+
print("[PhantomEye] Behavioral analyzer ready")
|
| 27 |
+
|
| 28 |
+
def update(self, active_tracks: list):
|
| 29 |
+
self.frame_num += 1
|
| 30 |
+
|
| 31 |
+
for trk in active_tracks:
|
| 32 |
+
cx, cy = trk.center()
|
| 33 |
+
|
| 34 |
+
cx = max(0, min(cx, self.width - 1))
|
| 35 |
+
cy = max(0, min(cy, self.height - 1))
|
| 36 |
+
|
| 37 |
+
cv2.circle(
|
| 38 |
+
self.heatmap,
|
| 39 |
+
(cx, cy), 25, 1.0, -1
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
self.dwell[trk.track_id] += 1
|
| 43 |
+
|
| 44 |
+
dwell_secs = self.dwell[trk.track_id] / self.fps
|
| 45 |
+
if dwell_secs >= DWELL_TIME_THRESHOLD:
|
| 46 |
+
already = any(
|
| 47 |
+
a["id"] == trk.track_id and a["type"] == "loitering"
|
| 48 |
+
for a in self.alerts
|
| 49 |
+
)
|
| 50 |
+
if not already:
|
| 51 |
+
self.alerts.append({
|
| 52 |
+
"type" : "loitering",
|
| 53 |
+
"id" : trk.track_id,
|
| 54 |
+
"bbox" : trk.bbox,
|
| 55 |
+
"dwell_sec": round(dwell_secs, 1),
|
| 56 |
+
"frame" : self.frame_num,
|
| 57 |
+
})
|
| 58 |
+
|
| 59 |
+
def get_heatmap_overlay(self, frame: np.ndarray) -> np.ndarray:
|
| 60 |
+
if self.heatmap.max() == 0:
|
| 61 |
+
return frame
|
| 62 |
+
|
| 63 |
+
normalized = cv2.normalize(
|
| 64 |
+
self.heatmap, None, 0, 255, cv2.NORM_MINMAX
|
| 65 |
+
)
|
| 66 |
+
heat_uint8 = normalized.astype(np.uint8)
|
| 67 |
+
heat_color = cv2.applyColorMap(heat_uint8, cv2.COLORMAP_JET)
|
| 68 |
+
blurred = cv2.GaussianBlur(heat_color, (31, 31), 0)
|
| 69 |
+
|
| 70 |
+
mask = heat_uint8 > 10
|
| 71 |
+
output = frame.copy()
|
| 72 |
+
output[mask] = cv2.addWeighted(
|
| 73 |
+
frame, 1 - HEATMAP_ALPHA,
|
| 74 |
+
blurred, HEATMAP_ALPHA, 0
|
| 75 |
+
)[mask]
|
| 76 |
+
|
| 77 |
+
return output
|
| 78 |
+
|
| 79 |
+
def draw_alerts(self, frame: np.ndarray, active_tracks: list) -> np.ndarray:
|
| 80 |
+
out = frame.copy()
|
| 81 |
+
|
| 82 |
+
active_ids = {t.track_id for t in active_tracks}
|
| 83 |
+
recent = [
|
| 84 |
+
a for a in self.alerts
|
| 85 |
+
if a["id"] in active_ids and a["type"] == "loitering"
|
| 86 |
+
]
|
| 87 |
+
|
| 88 |
+
for alert in recent:
|
| 89 |
+
x1, y1, x2, y2 = alert["bbox"]
|
| 90 |
+
cv2.rectangle(out, (x1, y1), (x2, y2), (0, 0, 255), 3)
|
| 91 |
+
label = f"ALERT ID:{alert['id']} {alert['dwell_sec']}s"
|
| 92 |
+
cv2.putText(
|
| 93 |
+
out, label,
|
| 94 |
+
(x1, y1 - 10),
|
| 95 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.6,
|
| 96 |
+
(0, 0, 255), 2, cv2.LINE_AA
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
return out
|
| 100 |
+
|
| 101 |
+
def draw_dwell_info(self, frame: np.ndarray, active_tracks: list) -> np.ndarray:
|
| 102 |
+
out = frame.copy()
|
| 103 |
+
|
| 104 |
+
for trk in active_tracks:
|
| 105 |
+
x1, y1, x2, y2 = trk.bbox
|
| 106 |
+
secs = round(self.dwell[trk.track_id] / self.fps, 1)
|
| 107 |
+
cv2.putText(
|
| 108 |
+
out,
|
| 109 |
+
f"{secs}s",
|
| 110 |
+
(x1, y2 + 16),
|
| 111 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.45,
|
| 112 |
+
(255, 255, 0), 1, cv2.LINE_AA
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
return out
|
| 116 |
+
|
| 117 |
+
def summary(self) -> dict:
|
| 118 |
+
if not self.dwell:
|
| 119 |
+
return {}
|
| 120 |
+
|
| 121 |
+
dwell_secs = {
|
| 122 |
+
k: round(v / self.fps, 1)
|
| 123 |
+
for k, v in self.dwell.items()
|
| 124 |
+
}
|
| 125 |
+
return {
|
| 126 |
+
"total_persons" : len(self.dwell),
|
| 127 |
+
"total_alerts" : len(self.alerts),
|
| 128 |
+
"avg_dwell_sec" : round(
|
| 129 |
+
sum(dwell_secs.values()) / len(dwell_secs), 2
|
| 130 |
+
),
|
| 131 |
+
"max_dwell_sec" : max(dwell_secs.values()),
|
| 132 |
+
"loiterers" : [
|
| 133 |
+
a["id"] for a in self.alerts
|
| 134 |
+
if a["type"] == "loitering"
|
| 135 |
+
],
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def run_analytics(video_path: str, save: bool = True, show: bool = True):
|
| 140 |
+
|
| 141 |
+
video_path = Path(video_path)
|
| 142 |
+
if not video_path.exists():
|
| 143 |
+
print(f"[ERROR] Video not found: {video_path}")
|
| 144 |
+
return
|
| 145 |
+
|
| 146 |
+
detector = PersonDetector()
|
| 147 |
+
tracker = ByteTracker()
|
| 148 |
+
cap = cv2.VideoCapture(str(video_path))
|
| 149 |
+
|
| 150 |
+
if not cap.isOpened():
|
| 151 |
+
print(f"[ERROR] Cannot open: {video_path}")
|
| 152 |
+
return
|
| 153 |
+
|
| 154 |
+
fps = int(cap.get(cv2.CAP_PROP_FPS)) or 25
|
| 155 |
+
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
| 156 |
+
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
| 157 |
+
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 158 |
+
|
| 159 |
+
analyzer = BehavioralAnalyzer(width, height, fps)
|
| 160 |
+
|
| 161 |
+
print(f"[PhantomEye] {video_path.name} β {width}x{height} {fps}fps")
|
| 162 |
+
|
| 163 |
+
writer = None
|
| 164 |
+
if save:
|
| 165 |
+
OUTPUTS_DIR.mkdir(parents=True, exist_ok=True)
|
| 166 |
+
out_path = OUTPUTS_DIR / (video_path.stem + "_analytics.mp4")
|
| 167 |
+
writer = cv2.VideoWriter(
|
| 168 |
+
str(out_path),
|
| 169 |
+
cv2.VideoWriter_fourcc(*"mp4v"),
|
| 170 |
+
fps, (width, height)
|
| 171 |
+
)
|
| 172 |
+
print(f"[PhantomEye] Saving to: {out_path}")
|
| 173 |
+
|
| 174 |
+
show_heat = False
|
| 175 |
+
start_time = time.time()
|
| 176 |
+
|
| 177 |
+
while True:
|
| 178 |
+
ret, frame = cap.read()
|
| 179 |
+
if not ret:
|
| 180 |
+
break
|
| 181 |
+
|
| 182 |
+
detections = detector.detect(frame)
|
| 183 |
+
active_tracks = tracker.update(detections)
|
| 184 |
+
analyzer.update(active_tracks)
|
| 185 |
+
|
| 186 |
+
if show_heat:
|
| 187 |
+
display = analyzer.get_heatmap_overlay(frame)
|
| 188 |
+
else:
|
| 189 |
+
display = frame.copy()
|
| 190 |
+
|
| 191 |
+
display = tracker.draw(display, active_tracks)
|
| 192 |
+
display = analyzer.draw_dwell_info(display, active_tracks)
|
| 193 |
+
display = analyzer.draw_alerts(display, active_tracks)
|
| 194 |
+
|
| 195 |
+
mode_text = "MODE: HEATMAP [H]" if show_heat else "MODE: TRACKING [H]"
|
| 196 |
+
cv2.putText(
|
| 197 |
+
display, mode_text,
|
| 198 |
+
(width - 220, height - 12),
|
| 199 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.45,
|
| 200 |
+
(200, 200, 200), 1, cv2.LINE_AA
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
if writer:
|
| 204 |
+
writer.write(display)
|
| 205 |
+
|
| 206 |
+
if show:
|
| 207 |
+
cv2.imshow("PhantomEye β Analytics [H=heatmap Q=quit]", display)
|
| 208 |
+
key = cv2.waitKey(1) & 0xFF
|
| 209 |
+
if key == ord("q"):
|
| 210 |
+
break
|
| 211 |
+
elif key == ord("h"):
|
| 212 |
+
show_heat = not show_heat
|
| 213 |
+
|
| 214 |
+
if tracker.frame_count % 30 == 0:
|
| 215 |
+
print(
|
| 216 |
+
f"\r[Frame {tracker.frame_count}/{total}]"
|
| 217 |
+
f" Active: {len(active_tracks)}"
|
| 218 |
+
f" Alerts: {len(analyzer.alerts)}"
|
| 219 |
+
f" Time: {time.time()-start_time:.1f}s",
|
| 220 |
+
end=""
|
| 221 |
+
)
|
| 222 |
+
|
| 223 |
+
cap.release()
|
| 224 |
+
if writer:
|
| 225 |
+
writer.release()
|
| 226 |
+
cv2.destroyAllWindows()
|
| 227 |
+
|
| 228 |
+
summary = analyzer.summary()
|
| 229 |
+
print(f"\n\n[PhantomEye] Analytics done!")
|
| 230 |
+
print(f" Total persons tracked : {summary.get('total_persons', 0)}")
|
| 231 |
+
print(f" Avg dwell time : {summary.get('avg_dwell_sec', 0)}s")
|
| 232 |
+
print(f" Max dwell time : {summary.get('max_dwell_sec', 0)}s")
|
| 233 |
+
print(f" Loitering alerts : {summary.get('total_alerts', 0)}")
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
if __name__ == "__main__":
|
| 237 |
+
import sys
|
| 238 |
+
if len(sys.argv) < 2:
|
| 239 |
+
print("Usage: python core/analytics.py <video_path>")
|
| 240 |
+
else:
|
| 241 |
run_analytics(sys.argv[1])
|
core/detection.py
CHANGED
|
@@ -1,173 +1,173 @@
|
|
| 1 |
-
import cv2
|
| 2 |
-
import time
|
| 3 |
-
import numpy as np
|
| 4 |
-
from pathlib import Path
|
| 5 |
-
from ultralytics import YOLO
|
| 6 |
-
|
| 7 |
-
import sys
|
| 8 |
-
sys.path.append(str(Path(__file__).resolve().parent.parent))
|
| 9 |
-
from config import DETECTION_CONF, DETECTION_MODEL, DEVICE, OUTPUTS_DIR
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
class PersonDetector:
|
| 13 |
-
|
| 14 |
-
def __init__(self):
|
| 15 |
-
self.model = YOLO(DETECTION_MODEL)
|
| 16 |
-
self.conf = DETECTION_CONF
|
| 17 |
-
self.device = DEVICE
|
| 18 |
-
self.frame_count = 0
|
| 19 |
-
self.total_detections = 0
|
| 20 |
-
print(f"[PhantomEye] Detector ready β model: {DETECTION_MODEL} device: {DEVICE}")
|
| 21 |
-
|
| 22 |
-
def detect(self, frame: np.ndarray) -> list:
|
| 23 |
-
results = self.model(
|
| 24 |
-
frame,
|
| 25 |
-
conf=self.conf,
|
| 26 |
-
classes=[0],
|
| 27 |
-
device=self.device,
|
| 28 |
-
verbose=False
|
| 29 |
-
)[0]
|
| 30 |
-
|
| 31 |
-
detections = []
|
| 32 |
-
for box in results.boxes:
|
| 33 |
-
x1, y1, x2, y2 = map(int, box.xyxy[0].tolist())
|
| 34 |
-
conf_score = float(box.conf[0])
|
| 35 |
-
detections.append({
|
| 36 |
-
"bbox" : (x1, y1, x2, y2),
|
| 37 |
-
"confidence": round(conf_score, 3),
|
| 38 |
-
"cx" : (x1 + x2) // 2,
|
| 39 |
-
"cy" : (x1 + x2) // 2,
|
| 40 |
-
})
|
| 41 |
-
|
| 42 |
-
self.frame_count += 1
|
| 43 |
-
self.total_detections += len(detections)
|
| 44 |
-
return detections
|
| 45 |
-
|
| 46 |
-
def draw(self, frame: np.ndarray, detections: list) -> np.ndarray:
|
| 47 |
-
out = frame.copy()
|
| 48 |
-
|
| 49 |
-
for i, det in enumerate(detections):
|
| 50 |
-
x1, y1, x2, y2 = det["bbox"]
|
| 51 |
-
conf = det["confidence"]
|
| 52 |
-
|
| 53 |
-
cv2.rectangle(out, (x1, y1), (x2, y2), (0, 255, 100), 2)
|
| 54 |
-
|
| 55 |
-
label = f"Person {conf:.2f}"
|
| 56 |
-
label_w, label_h = cv2.getTextSize(
|
| 57 |
-
label, cv2.FONT_HERSHEY_SIMPLEX, 0.55, 1
|
| 58 |
-
)[0]
|
| 59 |
-
cv2.rectangle(
|
| 60 |
-
out,
|
| 61 |
-
(x1, y1 - label_h - 8),
|
| 62 |
-
(x1 + label_w + 4, y1),
|
| 63 |
-
(0, 255, 100), -1
|
| 64 |
-
)
|
| 65 |
-
cv2.putText(
|
| 66 |
-
out, label,
|
| 67 |
-
(x1 + 2, y1 - 5),
|
| 68 |
-
cv2.FONT_HERSHEY_SIMPLEX, 0.55,
|
| 69 |
-
(0, 0, 0), 1, cv2.LINE_AA
|
| 70 |
-
)
|
| 71 |
-
|
| 72 |
-
header = f"PhantomEye | Frame: {self.frame_count} | Persons: {len(detections)}"
|
| 73 |
-
cv2.rectangle(out, (0, 0), (len(header) * 9 + 10, 28), (0, 0, 0), -1)
|
| 74 |
-
cv2.putText(
|
| 75 |
-
out, header,
|
| 76 |
-
(6, 18),
|
| 77 |
-
cv2.FONT_HERSHEY_SIMPLEX, 0.55,
|
| 78 |
-
(0, 255, 100), 1, cv2.LINE_AA
|
| 79 |
-
)
|
| 80 |
-
|
| 81 |
-
return out
|
| 82 |
-
|
| 83 |
-
def stats(self) -> dict:
|
| 84 |
-
avg = (
|
| 85 |
-
round(self.total_detections / self.frame_count, 2)
|
| 86 |
-
if self.frame_count > 0 else 0
|
| 87 |
-
)
|
| 88 |
-
return {
|
| 89 |
-
"total_frames" : self.frame_count,
|
| 90 |
-
"total_detections": self.total_detections,
|
| 91 |
-
"avg_per_frame" : avg,
|
| 92 |
-
}
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
def run_on_video(video_path: str, save: bool = True, show: bool = True):
|
| 96 |
-
|
| 97 |
-
video_path = Path(video_path)
|
| 98 |
-
if not video_path.exists():
|
| 99 |
-
print(f"[ERROR] Video not found: {video_path}")
|
| 100 |
-
return
|
| 101 |
-
|
| 102 |
-
detector = PersonDetector()
|
| 103 |
-
cap = cv2.VideoCapture(str(video_path))
|
| 104 |
-
|
| 105 |
-
if not cap.isOpened():
|
| 106 |
-
print(f"[ERROR] Cannot open video: {video_path}")
|
| 107 |
-
return
|
| 108 |
-
|
| 109 |
-
fps = int(cap.get(cv2.CAP_PROP_FPS)) or 25
|
| 110 |
-
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
| 111 |
-
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
| 112 |
-
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 113 |
-
|
| 114 |
-
print(f"[PhantomEye] Video : {video_path.name}")
|
| 115 |
-
print(f"[PhantomEye] Size : {width}x{height} FPS: {fps} Frames: {total}")
|
| 116 |
-
|
| 117 |
-
writer = None
|
| 118 |
-
if save:
|
| 119 |
-
OUTPUTS_DIR.mkdir(parents=True, exist_ok=True)
|
| 120 |
-
out_path = OUTPUTS_DIR / (video_path.stem + "_detected.mp4")
|
| 121 |
-
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
|
| 122 |
-
writer = cv2.VideoWriter(str(out_path), fourcc, fps, (width, height))
|
| 123 |
-
print(f"[PhantomEye] Saving : {out_path}")
|
| 124 |
-
|
| 125 |
-
start_time = time.time()
|
| 126 |
-
|
| 127 |
-
while True:
|
| 128 |
-
ret, frame = cap.read()
|
| 129 |
-
if not ret:
|
| 130 |
-
break
|
| 131 |
-
|
| 132 |
-
detections = detector.detect(frame)
|
| 133 |
-
annotated = detector.draw(frame, detections)
|
| 134 |
-
|
| 135 |
-
if writer:
|
| 136 |
-
writer.write(annotated)
|
| 137 |
-
|
| 138 |
-
if show:
|
| 139 |
-
cv2.imshow("PhantomEye β Detection [Q to quit]", annotated)
|
| 140 |
-
if cv2.waitKey(1) & 0xFF == ord("q"):
|
| 141 |
-
print("\n[PhantomEye] Stopped by user.")
|
| 142 |
-
break
|
| 143 |
-
|
| 144 |
-
if detector.frame_count % 30 == 0:
|
| 145 |
-
elapsed = time.time() - start_time
|
| 146 |
-
print(
|
| 147 |
-
f"\r[Frame {detector.frame_count}/{total}] "
|
| 148 |
-
f"Persons: {len(detections)} "
|
| 149 |
-
f"Elapsed: {elapsed:.1f}s",
|
| 150 |
-
end=""
|
| 151 |
-
)
|
| 152 |
-
|
| 153 |
-
cap.release()
|
| 154 |
-
if writer:
|
| 155 |
-
writer.release()
|
| 156 |
-
cv2.destroyAllWindows()
|
| 157 |
-
|
| 158 |
-
stats = detector.stats()
|
| 159 |
-
print(f"\n\n[PhantomEye] DONE")
|
| 160 |
-
print(f" Frames processed : {stats['total_frames']}")
|
| 161 |
-
print(f" Total detections : {stats['total_detections']}")
|
| 162 |
-
print(f" Avg persons/frame: {stats['avg_per_frame']}")
|
| 163 |
-
if save:
|
| 164 |
-
print(f" Output saved to : {OUTPUTS_DIR}")
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
if __name__ == "__main__":
|
| 168 |
-
import sys
|
| 169 |
-
if len(sys.argv) < 2:
|
| 170 |
-
print("Usage: python core/detection.py <video_path>")
|
| 171 |
-
print("Example: python core/detection.py data/videos/test.mp4")
|
| 172 |
-
else:
|
| 173 |
run_on_video(sys.argv[1])
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import time
|
| 3 |
+
import numpy as np
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from ultralytics import YOLO
|
| 6 |
+
|
| 7 |
+
import sys
|
| 8 |
+
sys.path.append(str(Path(__file__).resolve().parent.parent))
|
| 9 |
+
from config import DETECTION_CONF, DETECTION_MODEL, DEVICE, OUTPUTS_DIR
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class PersonDetector:
|
| 13 |
+
|
| 14 |
+
def __init__(self):
|
| 15 |
+
self.model = YOLO(DETECTION_MODEL)
|
| 16 |
+
self.conf = DETECTION_CONF
|
| 17 |
+
self.device = DEVICE
|
| 18 |
+
self.frame_count = 0
|
| 19 |
+
self.total_detections = 0
|
| 20 |
+
print(f"[PhantomEye] Detector ready β model: {DETECTION_MODEL} device: {DEVICE}")
|
| 21 |
+
|
| 22 |
+
def detect(self, frame: np.ndarray) -> list:
|
| 23 |
+
results = self.model(
|
| 24 |
+
frame,
|
| 25 |
+
conf=self.conf,
|
| 26 |
+
classes=[0],
|
| 27 |
+
device=self.device,
|
| 28 |
+
verbose=False
|
| 29 |
+
)[0]
|
| 30 |
+
|
| 31 |
+
detections = []
|
| 32 |
+
for box in results.boxes:
|
| 33 |
+
x1, y1, x2, y2 = map(int, box.xyxy[0].tolist())
|
| 34 |
+
conf_score = float(box.conf[0])
|
| 35 |
+
detections.append({
|
| 36 |
+
"bbox" : (x1, y1, x2, y2),
|
| 37 |
+
"confidence": round(conf_score, 3),
|
| 38 |
+
"cx" : (x1 + x2) // 2,
|
| 39 |
+
"cy" : (x1 + x2) // 2,
|
| 40 |
+
})
|
| 41 |
+
|
| 42 |
+
self.frame_count += 1
|
| 43 |
+
self.total_detections += len(detections)
|
| 44 |
+
return detections
|
| 45 |
+
|
| 46 |
+
def draw(self, frame: np.ndarray, detections: list) -> np.ndarray:
|
| 47 |
+
out = frame.copy()
|
| 48 |
+
|
| 49 |
+
for i, det in enumerate(detections):
|
| 50 |
+
x1, y1, x2, y2 = det["bbox"]
|
| 51 |
+
conf = det["confidence"]
|
| 52 |
+
|
| 53 |
+
cv2.rectangle(out, (x1, y1), (x2, y2), (0, 255, 100), 2)
|
| 54 |
+
|
| 55 |
+
label = f"Person {conf:.2f}"
|
| 56 |
+
label_w, label_h = cv2.getTextSize(
|
| 57 |
+
label, cv2.FONT_HERSHEY_SIMPLEX, 0.55, 1
|
| 58 |
+
)[0]
|
| 59 |
+
cv2.rectangle(
|
| 60 |
+
out,
|
| 61 |
+
(x1, y1 - label_h - 8),
|
| 62 |
+
(x1 + label_w + 4, y1),
|
| 63 |
+
(0, 255, 100), -1
|
| 64 |
+
)
|
| 65 |
+
cv2.putText(
|
| 66 |
+
out, label,
|
| 67 |
+
(x1 + 2, y1 - 5),
|
| 68 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.55,
|
| 69 |
+
(0, 0, 0), 1, cv2.LINE_AA
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
header = f"PhantomEye | Frame: {self.frame_count} | Persons: {len(detections)}"
|
| 73 |
+
cv2.rectangle(out, (0, 0), (len(header) * 9 + 10, 28), (0, 0, 0), -1)
|
| 74 |
+
cv2.putText(
|
| 75 |
+
out, header,
|
| 76 |
+
(6, 18),
|
| 77 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.55,
|
| 78 |
+
(0, 255, 100), 1, cv2.LINE_AA
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
return out
|
| 82 |
+
|
| 83 |
+
def stats(self) -> dict:
|
| 84 |
+
avg = (
|
| 85 |
+
round(self.total_detections / self.frame_count, 2)
|
| 86 |
+
if self.frame_count > 0 else 0
|
| 87 |
+
)
|
| 88 |
+
return {
|
| 89 |
+
"total_frames" : self.frame_count,
|
| 90 |
+
"total_detections": self.total_detections,
|
| 91 |
+
"avg_per_frame" : avg,
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def run_on_video(video_path: str, save: bool = True, show: bool = True):
|
| 96 |
+
|
| 97 |
+
video_path = Path(video_path)
|
| 98 |
+
if not video_path.exists():
|
| 99 |
+
print(f"[ERROR] Video not found: {video_path}")
|
| 100 |
+
return
|
| 101 |
+
|
| 102 |
+
detector = PersonDetector()
|
| 103 |
+
cap = cv2.VideoCapture(str(video_path))
|
| 104 |
+
|
| 105 |
+
if not cap.isOpened():
|
| 106 |
+
print(f"[ERROR] Cannot open video: {video_path}")
|
| 107 |
+
return
|
| 108 |
+
|
| 109 |
+
fps = int(cap.get(cv2.CAP_PROP_FPS)) or 25
|
| 110 |
+
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
| 111 |
+
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
| 112 |
+
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 113 |
+
|
| 114 |
+
print(f"[PhantomEye] Video : {video_path.name}")
|
| 115 |
+
print(f"[PhantomEye] Size : {width}x{height} FPS: {fps} Frames: {total}")
|
| 116 |
+
|
| 117 |
+
writer = None
|
| 118 |
+
if save:
|
| 119 |
+
OUTPUTS_DIR.mkdir(parents=True, exist_ok=True)
|
| 120 |
+
out_path = OUTPUTS_DIR / (video_path.stem + "_detected.mp4")
|
| 121 |
+
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
|
| 122 |
+
writer = cv2.VideoWriter(str(out_path), fourcc, fps, (width, height))
|
| 123 |
+
print(f"[PhantomEye] Saving : {out_path}")
|
| 124 |
+
|
| 125 |
+
start_time = time.time()
|
| 126 |
+
|
| 127 |
+
while True:
|
| 128 |
+
ret, frame = cap.read()
|
| 129 |
+
if not ret:
|
| 130 |
+
break
|
| 131 |
+
|
| 132 |
+
detections = detector.detect(frame)
|
| 133 |
+
annotated = detector.draw(frame, detections)
|
| 134 |
+
|
| 135 |
+
if writer:
|
| 136 |
+
writer.write(annotated)
|
| 137 |
+
|
| 138 |
+
if show:
|
| 139 |
+
cv2.imshow("PhantomEye β Detection [Q to quit]", annotated)
|
| 140 |
+
if cv2.waitKey(1) & 0xFF == ord("q"):
|
| 141 |
+
print("\n[PhantomEye] Stopped by user.")
|
| 142 |
+
break
|
| 143 |
+
|
| 144 |
+
if detector.frame_count % 30 == 0:
|
| 145 |
+
elapsed = time.time() - start_time
|
| 146 |
+
print(
|
| 147 |
+
f"\r[Frame {detector.frame_count}/{total}] "
|
| 148 |
+
f"Persons: {len(detections)} "
|
| 149 |
+
f"Elapsed: {elapsed:.1f}s",
|
| 150 |
+
end=""
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
cap.release()
|
| 154 |
+
if writer:
|
| 155 |
+
writer.release()
|
| 156 |
+
cv2.destroyAllWindows()
|
| 157 |
+
|
| 158 |
+
stats = detector.stats()
|
| 159 |
+
print(f"\n\n[PhantomEye] DONE")
|
| 160 |
+
print(f" Frames processed : {stats['total_frames']}")
|
| 161 |
+
print(f" Total detections : {stats['total_detections']}")
|
| 162 |
+
print(f" Avg persons/frame: {stats['avg_per_frame']}")
|
| 163 |
+
if save:
|
| 164 |
+
print(f" Output saved to : {OUTPUTS_DIR}")
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
if __name__ == "__main__":
|
| 168 |
+
import sys
|
| 169 |
+
if len(sys.argv) < 2:
|
| 170 |
+
print("Usage: python core/detection.py <video_path>")
|
| 171 |
+
print("Example: python core/detection.py data/videos/test.mp4")
|
| 172 |
+
else:
|
| 173 |
run_on_video(sys.argv[1])
|
core/emotion.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import numpy as np
|
| 3 |
+
from deepface import DeepFace
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
# Suppress TF logs
|
| 7 |
+
import os
|
| 8 |
+
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
|
| 9 |
+
os.environ["TF_ENABLE_ONEDNN_OPTS"] = "0"
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
EMOTION_COLORS = {
|
| 13 |
+
"happy": (0, 255, 128),
|
| 14 |
+
"sad": (255, 100, 50),
|
| 15 |
+
"angry": (0, 0, 255),
|
| 16 |
+
"fear": (180, 0, 255),
|
| 17 |
+
"surprise": (0, 220, 255),
|
| 18 |
+
"disgust": (0, 140, 60),
|
| 19 |
+
"neutral": (0, 255, 136),
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
FONT = cv2.FONT_HERSHEY_SIMPLEX
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def analyze_faces(frame: np.ndarray) -> list[dict]:
|
| 26 |
+
try:
|
| 27 |
+
results = DeepFace.analyze(
|
| 28 |
+
img_path=frame,
|
| 29 |
+
actions=["age", "gender", "emotion"],
|
| 30 |
+
detector_backend="opencv",
|
| 31 |
+
enforce_detection=False,
|
| 32 |
+
silent=True,
|
| 33 |
+
)
|
| 34 |
+
results = results if isinstance(results, list) else [results]
|
| 35 |
+
|
| 36 |
+
h, w = frame.shape[:2]
|
| 37 |
+
min_face = int(min(h, w) * 0.25)
|
| 38 |
+
filtered = [
|
| 39 |
+
r for r in results
|
| 40 |
+
if r.get("region", {}).get("w", 0) >= min_face
|
| 41 |
+
and r.get("region", {}).get("h", 0) >= min_face
|
| 42 |
+
]
|
| 43 |
+
return filtered
|
| 44 |
+
except Exception:
|
| 45 |
+
return []
|
| 46 |
+
|
| 47 |
+
def draw_emotion_overlays(frame: np.ndarray, results: list[dict]) -> np.ndarray:
|
| 48 |
+
"""
|
| 49 |
+
Draw age, gender, emotion overlay boxes on frame for each detected face.
|
| 50 |
+
"""
|
| 51 |
+
for face in results:
|
| 52 |
+
region = face.get("region", {})
|
| 53 |
+
x = region.get("x", 0)
|
| 54 |
+
y = region.get("y", 0)
|
| 55 |
+
w = region.get("w", 0)
|
| 56 |
+
h = region.get("h", 0)
|
| 57 |
+
|
| 58 |
+
if w == 0 or h == 0:
|
| 59 |
+
continue
|
| 60 |
+
|
| 61 |
+
emotion = face.get("dominant_emotion", "neutral")
|
| 62 |
+
age = int(face.get("age", 0))
|
| 63 |
+
gender = face.get("dominant_gender", face.get("gender", "Unknown"))
|
| 64 |
+
|
| 65 |
+
if isinstance(gender, dict):
|
| 66 |
+
gender = max(gender, key=gender.get)
|
| 67 |
+
|
| 68 |
+
color = EMOTION_COLORS.get(emotion, (0, 255, 136))
|
| 69 |
+
|
| 70 |
+
# Face bounding box
|
| 71 |
+
cv2.rectangle(frame, (x, y), (x + w, y + h), color, 2)
|
| 72 |
+
|
| 73 |
+
# Top label β emotion
|
| 74 |
+
label_emotion = f"{emotion.upper()}"
|
| 75 |
+
cv2.rectangle(frame, (x, y - 22), (x + w, y), color, -1)
|
| 76 |
+
cv2.putText(frame, label_emotion, (x + 4, y - 6),
|
| 77 |
+
FONT, 0.52, (0, 0, 0), 1, cv2.LINE_AA)
|
| 78 |
+
|
| 79 |
+
# Bottom label β age + gender
|
| 80 |
+
label_demo = f"Age:{age} {gender}"
|
| 81 |
+
cv2.rectangle(frame, (x, y + h), (x + w, y + h + 22), color, -1)
|
| 82 |
+
cv2.putText(frame, label_demo, (x + 4, y + h + 15),
|
| 83 |
+
FONT, 0.52, (0, 0, 0), 1, cv2.LINE_AA)
|
| 84 |
+
|
| 85 |
+
return frame
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def process_frame_emotion(frame: np.ndarray) -> tuple[np.ndarray, list[dict]]:
|
| 89 |
+
"""
|
| 90 |
+
Main entry point β analyze + draw.
|
| 91 |
+
Returns annotated frame and raw results list.
|
| 92 |
+
"""
|
| 93 |
+
results = analyze_faces(frame)
|
| 94 |
+
annotated = draw_emotion_overlays(frame.copy(), results)
|
| 95 |
+
return annotated, results
|
core/nlquery.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
from groq import Groq
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
|
| 6 |
+
load_dotenv()
|
| 7 |
+
|
| 8 |
+
client = Groq(api_key=os.getenv("GROQ_API_KEY"))
|
| 9 |
+
|
| 10 |
+
SYSTEM_PROMPT = """You are an AI surveillance query parser for PhantomEye system.
|
| 11 |
+
User gives natural language queries about tracked people in a surveillance scene.
|
| 12 |
+
You extract structured filters from the query.
|
| 13 |
+
|
| 14 |
+
Return ONLY valid JSON with these fields:
|
| 15 |
+
{
|
| 16 |
+
"emotion": null or one of ["angry","happy","sad","fear","surprise","disgust","neutral"],
|
| 17 |
+
"gender": null or "Man" or "Woman",
|
| 18 |
+
"min_age": null or integer,
|
| 19 |
+
"max_age": null or integer,
|
| 20 |
+
"min_dwell_seconds": null or integer,
|
| 21 |
+
"loitering": null or true or false,
|
| 22 |
+
"summary": "one line human readable summary of the filter"
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
Examples:
|
| 26 |
+
"show me all angry men" -> {"emotion":"angry","gender":"Man","min_age":null,"max_age":null,"min_dwell_seconds":null,"loitering":null,"summary":"Angry men"}
|
| 27 |
+
"who was loitering?" -> {"emotion":null,"gender":null,"min_age":null,"max_age":null,"min_dwell_seconds":null,"loitering":true,"summary":"People detected loitering"}
|
| 28 |
+
"young women under 30" -> {"emotion":null,"gender":"Woman","min_age":null,"max_age":30,"min_dwell_seconds":null,"loitering":null,"summary":"Women under 30 years old"}
|
| 29 |
+
"people who stayed more than 2 minutes" -> {"emotion":null,"gender":null,"min_age":null,"max_age":null,"min_dwell_seconds":120,"loitering":null,"summary":"People with dwell time over 2 minutes"}
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def parse_nl_query(query: str) -> dict:
|
| 34 |
+
"""
|
| 35 |
+
Convert natural language query to structured filter dict via Groq LLM.
|
| 36 |
+
Returns parsed filter or error dict.
|
| 37 |
+
"""
|
| 38 |
+
try:
|
| 39 |
+
response = client.chat.completions.create(
|
| 40 |
+
model="llama-3.1-8b-instant",
|
| 41 |
+
messages=[
|
| 42 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 43 |
+
{"role": "user", "content": query}
|
| 44 |
+
],
|
| 45 |
+
temperature=0.1,
|
| 46 |
+
max_tokens=256,
|
| 47 |
+
)
|
| 48 |
+
raw = response.choices[0].message.content.strip()
|
| 49 |
+
# Strip markdown fences if present
|
| 50 |
+
if raw.startswith("```"):
|
| 51 |
+
raw = raw.split("```")[1]
|
| 52 |
+
if raw.startswith("json"):
|
| 53 |
+
raw = raw[4:]
|
| 54 |
+
parsed = json.loads(raw.strip())
|
| 55 |
+
return {"success": True, "filters": parsed}
|
| 56 |
+
except Exception as e:
|
| 57 |
+
return {"success": False, "error": str(e), "filters": {}}
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def apply_filters(records: list[dict], filters: dict) -> list[dict]:
|
| 61 |
+
"""
|
| 62 |
+
Apply parsed filters to a list of person records.
|
| 63 |
+
Each record: {id, emotion, gender, age, dwell_seconds, loitering}
|
| 64 |
+
Returns filtered list.
|
| 65 |
+
"""
|
| 66 |
+
results = records
|
| 67 |
+
if filters.get("emotion"):
|
| 68 |
+
results = [r for r in results if r.get("emotion", "").lower() == filters["emotion"].lower()]
|
| 69 |
+
if filters.get("gender"):
|
| 70 |
+
results = [r for r in results if r.get("gender", "").lower() == filters["gender"].lower()]
|
| 71 |
+
if filters.get("min_age") is not None:
|
| 72 |
+
results = [r for r in results if r.get("age", 0) >= filters["min_age"]]
|
| 73 |
+
if filters.get("max_age") is not None:
|
| 74 |
+
results = [r for r in results if r.get("age", 0) <= filters["max_age"]]
|
| 75 |
+
if filters.get("min_dwell_seconds") is not None:
|
| 76 |
+
results = [r for r in results if r.get("dwell_seconds", 0) >= filters["min_dwell_seconds"]]
|
| 77 |
+
if filters.get("loitering") is not None:
|
| 78 |
+
results = [r for r in results if r.get("loitering", False) == filters["loitering"]]
|
| 79 |
+
return results
|
core/osint.py
CHANGED
|
@@ -1,272 +1,272 @@
|
|
| 1 |
-
import cv2
|
| 2 |
-
import json
|
| 3 |
-
import time
|
| 4 |
-
import numpy as np
|
| 5 |
-
from pathlib import Path
|
| 6 |
-
from collections import defaultdict
|
| 7 |
-
|
| 8 |
-
import sys
|
| 9 |
-
sys.path.append(str(Path(__file__).resolve().parent.parent))
|
| 10 |
-
from config import OUTPUTS_DIR, GALLERY_DIR
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
class FaceEmbedder:
|
| 14 |
-
|
| 15 |
-
def __init__(self):
|
| 16 |
-
model_path = Path(__file__).parent.parent / "models"
|
| 17 |
-
self.face_cascade = cv2.CascadeClassifier(
|
| 18 |
-
cv2.data.haarcascades + "haarcascade_frontalface_default.xml"
|
| 19 |
-
)
|
| 20 |
-
self.recognizer = cv2.face.LBPHFaceRecognizer_create()
|
| 21 |
-
self.trained = False
|
| 22 |
-
self.gallery = {}
|
| 23 |
-
print("[PhantomEye] OSINT face embedder ready β OpenCV LBPH")
|
| 24 |
-
|
| 25 |
-
def detect_faces(self, image: np.ndarray) -> list:
|
| 26 |
-
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
| 27 |
-
faces = self.face_cascade.detectMultiScale(
|
| 28 |
-
gray,
|
| 29 |
-
scaleFactor=1.1,
|
| 30 |
-
minNeighbors=5,
|
| 31 |
-
minSize=(60, 60)
|
| 32 |
-
)
|
| 33 |
-
results = []
|
| 34 |
-
for (x, y, w, h) in faces:
|
| 35 |
-
face_roi = gray[y:y+h, x:x+w]
|
| 36 |
-
face_resized = cv2.resize(face_roi, (100, 100))
|
| 37 |
-
results.append({
|
| 38 |
-
"bbox" : (x, y, x+w, y+h),
|
| 39 |
-
"roi_gray" : face_resized,
|
| 40 |
-
"roi_color": image[y:y+h, x:x+w],
|
| 41 |
-
})
|
| 42 |
-
return results
|
| 43 |
-
|
| 44 |
-
def compute_histogram(self, face_gray: np.ndarray) -> np.ndarray:
|
| 45 |
-
hist = cv2.calcHist(
|
| 46 |
-
[face_gray], [0], None, [256], [0, 256]
|
| 47 |
-
)
|
| 48 |
-
cv2.normalize(hist, hist)
|
| 49 |
-
return hist.flatten()
|
| 50 |
-
|
| 51 |
-
def compare(self, hist1: np.ndarray, hist2: np.ndarray) -> float:
|
| 52 |
-
score = cv2.compareHist(
|
| 53 |
-
hist1.reshape(-1, 1).astype(np.float32),
|
| 54 |
-
hist2.reshape(-1, 1).astype(np.float32),
|
| 55 |
-
cv2.HISTCMP_CORREL
|
| 56 |
-
)
|
| 57 |
-
return round(float(score), 4)
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
class OSINTAudit:
|
| 61 |
-
|
| 62 |
-
def __init__(self):
|
| 63 |
-
self.embedder = FaceEmbedder()
|
| 64 |
-
self.gallery = {}
|
| 65 |
-
self.audit_log = []
|
| 66 |
-
GALLERY_DIR.mkdir(parents=True, exist_ok=True)
|
| 67 |
-
self._load_gallery()
|
| 68 |
-
print(f"[PhantomEye] OSINT gallery loaded β {len(self.gallery)} persons")
|
| 69 |
-
|
| 70 |
-
def _load_gallery(self):
|
| 71 |
-
supported = [".jpg", ".jpeg", ".png"]
|
| 72 |
-
for img_path in GALLERY_DIR.iterdir():
|
| 73 |
-
if img_path.suffix.lower() not in supported:
|
| 74 |
-
continue
|
| 75 |
-
img = cv2.imread(str(img_path))
|
| 76 |
-
if img is None:
|
| 77 |
-
continue
|
| 78 |
-
faces = self.embedder.detect_faces(img)
|
| 79 |
-
if faces:
|
| 80 |
-
hist = self.embedder.compute_histogram(faces[0]["roi_gray"])
|
| 81 |
-
self.gallery[img_path.stem] = {
|
| 82 |
-
"hist" : hist,
|
| 83 |
-
"source": img_path.name,
|
| 84 |
-
}
|
| 85 |
-
|
| 86 |
-
def add_to_gallery(self, image: np.ndarray, person_id: str) -> bool:
|
| 87 |
-
faces = self.embedder.detect_faces(image)
|
| 88 |
-
if not faces:
|
| 89 |
-
print(f"[OSINT] No face detected in image for ID: {person_id}")
|
| 90 |
-
return False
|
| 91 |
-
hist = self.embedder.compute_histogram(faces[0]["roi_gray"])
|
| 92 |
-
self.gallery[person_id] = {
|
| 93 |
-
"hist" : hist,
|
| 94 |
-
"source": "runtime_upload",
|
| 95 |
-
}
|
| 96 |
-
out_path = GALLERY_DIR / f"{person_id}.jpg"
|
| 97 |
-
cv2.imwrite(str(out_path), faces[0]["roi_color"])
|
| 98 |
-
print(f"[OSINT] Added to gallery: {person_id}")
|
| 99 |
-
return True
|
| 100 |
-
|
| 101 |
-
def audit(self, query_image: np.ndarray, query_id: str = "unknown") -> dict:
|
| 102 |
-
faces = self.embedder.detect_faces(query_image)
|
| 103 |
-
|
| 104 |
-
if not faces:
|
| 105 |
-
return {
|
| 106 |
-
"query_id" : query_id,
|
| 107 |
-
"face_detected" : False,
|
| 108 |
-
"exposure_score": 0,
|
| 109 |
-
"matches" : [],
|
| 110 |
-
"risk_level" : "UNKNOWN",
|
| 111 |
-
"message" : "No face detected in query image.",
|
| 112 |
-
}
|
| 113 |
-
|
| 114 |
-
query_hist = self.embedder.compute_histogram(faces[0]["roi_gray"])
|
| 115 |
-
|
| 116 |
-
matches = []
|
| 117 |
-
for person_id, data in self.gallery.items():
|
| 118 |
-
if person_id == query_id:
|
| 119 |
-
continue
|
| 120 |
-
score = self.embedder.compare(query_hist, data["hist"])
|
| 121 |
-
if score > 0.4:
|
| 122 |
-
matches.append({
|
| 123 |
-
"matched_id" : person_id,
|
| 124 |
-
"confidence" : round(score * 100, 1),
|
| 125 |
-
"source" : data["source"],
|
| 126 |
-
})
|
| 127 |
-
|
| 128 |
-
matches.sort(key=lambda x: x["confidence"], reverse=True)
|
| 129 |
-
top_matches = matches[:5]
|
| 130 |
-
|
| 131 |
-
exposure_score = self._compute_exposure(top_matches)
|
| 132 |
-
risk_level = self._risk_level(exposure_score)
|
| 133 |
-
|
| 134 |
-
result = {
|
| 135 |
-
"query_id" : query_id,
|
| 136 |
-
"face_detected" : True,
|
| 137 |
-
"exposure_score": exposure_score,
|
| 138 |
-
"matches" : top_matches,
|
| 139 |
-
"risk_level" : risk_level,
|
| 140 |
-
"total_checked" : len(self.gallery),
|
| 141 |
-
"message" : self._message(risk_level, len(top_matches)),
|
| 142 |
-
}
|
| 143 |
-
|
| 144 |
-
self.audit_log.append({
|
| 145 |
-
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
| 146 |
-
"result" : result,
|
| 147 |
-
})
|
| 148 |
-
|
| 149 |
-
return result
|
| 150 |
-
|
| 151 |
-
def _compute_exposure(self, matches: list) -> int:
|
| 152 |
-
if not matches:
|
| 153 |
-
return 5
|
| 154 |
-
top_conf = matches[0]["confidence"] if matches else 0
|
| 155 |
-
count = len(matches)
|
| 156 |
-
score = min(100, int(top_conf * 0.7 + count * 6))
|
| 157 |
-
return score
|
| 158 |
-
|
| 159 |
-
def _risk_level(self, score: int) -> str:
|
| 160 |
-
if score >= 70:
|
| 161 |
-
return "HIGH"
|
| 162 |
-
elif score >= 40:
|
| 163 |
-
return "MEDIUM"
|
| 164 |
-
else:
|
| 165 |
-
return "LOW"
|
| 166 |
-
|
| 167 |
-
def _message(self, risk: str, match_count: int) -> str:
|
| 168 |
-
msgs = {
|
| 169 |
-
"HIGH" : f"Critical β {match_count} strong matches found. High digital footprint.",
|
| 170 |
-
"MEDIUM": f"Moderate exposure β {match_count} partial matches detected.",
|
| 171 |
-
"LOW" : "Low exposure β minimal matches in reference gallery.",
|
| 172 |
-
}
|
| 173 |
-
return msgs.get(risk, "Audit complete.")
|
| 174 |
-
|
| 175 |
-
def save_report(self, result: dict) -> Path:
|
| 176 |
-
OUTPUTS_DIR.mkdir(parents=True, exist_ok=True)
|
| 177 |
-
fname = f"osint_report_{result['query_id']}_{int(time.time())}.json"
|
| 178 |
-
out_path = OUTPUTS_DIR / fname
|
| 179 |
-
with open(out_path, "w") as f:
|
| 180 |
-
json.dump(result, f, indent=2)
|
| 181 |
-
print(f"[OSINT] Report saved: {out_path}")
|
| 182 |
-
return out_path
|
| 183 |
-
|
| 184 |
-
def visualize(self, query_image: np.ndarray, result: dict) -> np.ndarray:
|
| 185 |
-
out = query_image.copy()
|
| 186 |
-
h, w = out.shape[:2]
|
| 187 |
-
|
| 188 |
-
risk_colors = {
|
| 189 |
-
"HIGH" : (0, 0, 255),
|
| 190 |
-
"MEDIUM" : (0, 165, 255),
|
| 191 |
-
"LOW" : (0, 255, 100),
|
| 192 |
-
"UNKNOWN": (128, 128, 128),
|
| 193 |
-
}
|
| 194 |
-
color = risk_colors.get(result["risk_level"], (128, 128, 128))
|
| 195 |
-
|
| 196 |
-
cv2.rectangle(out, (0, 0), (w, 90), (0, 0, 0), -1)
|
| 197 |
-
|
| 198 |
-
cv2.putText(out, "PhantomEye OSINT Audit",
|
| 199 |
-
(10, 22), cv2.FONT_HERSHEY_SIMPLEX, 0.65,
|
| 200 |
-
(0, 255, 100), 1, cv2.LINE_AA)
|
| 201 |
-
|
| 202 |
-
cv2.putText(out,
|
| 203 |
-
f"Risk: {result['risk_level']} Score: {result['exposure_score']}/100",
|
| 204 |
-
(10, 48), cv2.FONT_HERSHEY_SIMPLEX, 0.65,
|
| 205 |
-
color, 2, cv2.LINE_AA)
|
| 206 |
-
|
| 207 |
-
cv2.putText(out,
|
| 208 |
-
f"Matches: {len(result['matches'])} Checked: {result['total_checked']}",
|
| 209 |
-
(10, 72), cv2.FONT_HERSHEY_SIMPLEX, 0.55,
|
| 210 |
-
(200, 200, 200), 1, cv2.LINE_AA)
|
| 211 |
-
|
| 212 |
-
cv2.putText(out, result["message"],
|
| 213 |
-
(10, h - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.45,
|
| 214 |
-
(200, 200, 200), 1, cv2.LINE_AA)
|
| 215 |
-
|
| 216 |
-
return out
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
def run_osint_demo(query_image_path: str):
|
| 220 |
-
query_path = Path(query_image_path)
|
| 221 |
-
if not query_path.exists():
|
| 222 |
-
print(f"[ERROR] Image not found: {query_path}")
|
| 223 |
-
return
|
| 224 |
-
|
| 225 |
-
audit = OSINTAudit()
|
| 226 |
-
|
| 227 |
-
query_img = cv2.imread(str(query_path))
|
| 228 |
-
if query_img is None:
|
| 229 |
-
print(f"[ERROR] Cannot read image: {query_path}")
|
| 230 |
-
return
|
| 231 |
-
|
| 232 |
-
print(f"\n[OSINT] Running audit on: {query_path.name}")
|
| 233 |
-
print(f"[OSINT] Gallery size: {len(audit.gallery)} persons")
|
| 234 |
-
|
| 235 |
-
result = audit.audit(query_img, query_id=query_path.stem)
|
| 236 |
-
|
| 237 |
-
print(f"\n{'='*50}")
|
| 238 |
-
print(f" PHANTOMEYE OSINT AUDIT REPORT")
|
| 239 |
-
print(f"{'='*50}")
|
| 240 |
-
print(f" Query : {result['query_id']}")
|
| 241 |
-
print(f" Face found : {result['face_detected']}")
|
| 242 |
-
print(f" Risk level : {result['risk_level']}")
|
| 243 |
-
print(f" Score : {result['exposure_score']} / 100")
|
| 244 |
-
print(f" Matches : {len(result['matches'])}")
|
| 245 |
-
print(f" Message : {result['message']}")
|
| 246 |
-
|
| 247 |
-
if result["matches"]:
|
| 248 |
-
print(f"\n Top matches:")
|
| 249 |
-
for m in result["matches"]:
|
| 250 |
-
print(f" - {m['matched_id']} confidence: {m['confidence']}%")
|
| 251 |
-
|
| 252 |
-
print(f"{'='*50}\n")
|
| 253 |
-
|
| 254 |
-
audit.save_report(result)
|
| 255 |
-
|
| 256 |
-
vis = audit.visualize(query_img, result)
|
| 257 |
-
out_path = OUTPUTS_DIR / f"osint_{query_path.stem}.jpg"
|
| 258 |
-
cv2.imwrite(str(out_path), vis)
|
| 259 |
-
|
| 260 |
-
cv2.imshow("PhantomEye β OSINT Audit [any key to close]", vis)
|
| 261 |
-
cv2.waitKey(0)
|
| 262 |
-
cv2.destroyAllWindows()
|
| 263 |
-
|
| 264 |
-
print(f"[OSINT] Visual saved: {out_path}")
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
if __name__ == "__main__":
|
| 268 |
-
if len(sys.argv) < 2:
|
| 269 |
-
print("Usage: python core/osint.py <image_path>")
|
| 270 |
-
print("Example: python core/osint.py data/gallery/person1.jpg")
|
| 271 |
-
else:
|
| 272 |
run_osint_demo(sys.argv[1])
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import json
|
| 3 |
+
import time
|
| 4 |
+
import numpy as np
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from collections import defaultdict
|
| 7 |
+
|
| 8 |
+
import sys
|
| 9 |
+
sys.path.append(str(Path(__file__).resolve().parent.parent))
|
| 10 |
+
from config import OUTPUTS_DIR, GALLERY_DIR
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class FaceEmbedder:
|
| 14 |
+
|
| 15 |
+
def __init__(self):
|
| 16 |
+
model_path = Path(__file__).parent.parent / "models"
|
| 17 |
+
self.face_cascade = cv2.CascadeClassifier(
|
| 18 |
+
cv2.data.haarcascades + "haarcascade_frontalface_default.xml"
|
| 19 |
+
)
|
| 20 |
+
self.recognizer = cv2.face.LBPHFaceRecognizer_create()
|
| 21 |
+
self.trained = False
|
| 22 |
+
self.gallery = {}
|
| 23 |
+
print("[PhantomEye] OSINT face embedder ready β OpenCV LBPH")
|
| 24 |
+
|
| 25 |
+
def detect_faces(self, image: np.ndarray) -> list:
|
| 26 |
+
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
| 27 |
+
faces = self.face_cascade.detectMultiScale(
|
| 28 |
+
gray,
|
| 29 |
+
scaleFactor=1.1,
|
| 30 |
+
minNeighbors=5,
|
| 31 |
+
minSize=(60, 60)
|
| 32 |
+
)
|
| 33 |
+
results = []
|
| 34 |
+
for (x, y, w, h) in faces:
|
| 35 |
+
face_roi = gray[y:y+h, x:x+w]
|
| 36 |
+
face_resized = cv2.resize(face_roi, (100, 100))
|
| 37 |
+
results.append({
|
| 38 |
+
"bbox" : (x, y, x+w, y+h),
|
| 39 |
+
"roi_gray" : face_resized,
|
| 40 |
+
"roi_color": image[y:y+h, x:x+w],
|
| 41 |
+
})
|
| 42 |
+
return results
|
| 43 |
+
|
| 44 |
+
def compute_histogram(self, face_gray: np.ndarray) -> np.ndarray:
|
| 45 |
+
hist = cv2.calcHist(
|
| 46 |
+
[face_gray], [0], None, [256], [0, 256]
|
| 47 |
+
)
|
| 48 |
+
cv2.normalize(hist, hist)
|
| 49 |
+
return hist.flatten()
|
| 50 |
+
|
| 51 |
+
def compare(self, hist1: np.ndarray, hist2: np.ndarray) -> float:
|
| 52 |
+
score = cv2.compareHist(
|
| 53 |
+
hist1.reshape(-1, 1).astype(np.float32),
|
| 54 |
+
hist2.reshape(-1, 1).astype(np.float32),
|
| 55 |
+
cv2.HISTCMP_CORREL
|
| 56 |
+
)
|
| 57 |
+
return round(float(score), 4)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class OSINTAudit:
|
| 61 |
+
|
| 62 |
+
def __init__(self):
|
| 63 |
+
self.embedder = FaceEmbedder()
|
| 64 |
+
self.gallery = {}
|
| 65 |
+
self.audit_log = []
|
| 66 |
+
GALLERY_DIR.mkdir(parents=True, exist_ok=True)
|
| 67 |
+
self._load_gallery()
|
| 68 |
+
print(f"[PhantomEye] OSINT gallery loaded β {len(self.gallery)} persons")
|
| 69 |
+
|
| 70 |
+
def _load_gallery(self):
|
| 71 |
+
supported = [".jpg", ".jpeg", ".png"]
|
| 72 |
+
for img_path in GALLERY_DIR.iterdir():
|
| 73 |
+
if img_path.suffix.lower() not in supported:
|
| 74 |
+
continue
|
| 75 |
+
img = cv2.imread(str(img_path))
|
| 76 |
+
if img is None:
|
| 77 |
+
continue
|
| 78 |
+
faces = self.embedder.detect_faces(img)
|
| 79 |
+
if faces:
|
| 80 |
+
hist = self.embedder.compute_histogram(faces[0]["roi_gray"])
|
| 81 |
+
self.gallery[img_path.stem] = {
|
| 82 |
+
"hist" : hist,
|
| 83 |
+
"source": img_path.name,
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
def add_to_gallery(self, image: np.ndarray, person_id: str) -> bool:
|
| 87 |
+
faces = self.embedder.detect_faces(image)
|
| 88 |
+
if not faces:
|
| 89 |
+
print(f"[OSINT] No face detected in image for ID: {person_id}")
|
| 90 |
+
return False
|
| 91 |
+
hist = self.embedder.compute_histogram(faces[0]["roi_gray"])
|
| 92 |
+
self.gallery[person_id] = {
|
| 93 |
+
"hist" : hist,
|
| 94 |
+
"source": "runtime_upload",
|
| 95 |
+
}
|
| 96 |
+
out_path = GALLERY_DIR / f"{person_id}.jpg"
|
| 97 |
+
cv2.imwrite(str(out_path), faces[0]["roi_color"])
|
| 98 |
+
print(f"[OSINT] Added to gallery: {person_id}")
|
| 99 |
+
return True
|
| 100 |
+
|
| 101 |
+
def audit(self, query_image: np.ndarray, query_id: str = "unknown") -> dict:
|
| 102 |
+
faces = self.embedder.detect_faces(query_image)
|
| 103 |
+
|
| 104 |
+
if not faces:
|
| 105 |
+
return {
|
| 106 |
+
"query_id" : query_id,
|
| 107 |
+
"face_detected" : False,
|
| 108 |
+
"exposure_score": 0,
|
| 109 |
+
"matches" : [],
|
| 110 |
+
"risk_level" : "UNKNOWN",
|
| 111 |
+
"message" : "No face detected in query image.",
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
query_hist = self.embedder.compute_histogram(faces[0]["roi_gray"])
|
| 115 |
+
|
| 116 |
+
matches = []
|
| 117 |
+
for person_id, data in self.gallery.items():
|
| 118 |
+
if person_id == query_id:
|
| 119 |
+
continue
|
| 120 |
+
score = self.embedder.compare(query_hist, data["hist"])
|
| 121 |
+
if score > 0.4:
|
| 122 |
+
matches.append({
|
| 123 |
+
"matched_id" : person_id,
|
| 124 |
+
"confidence" : round(score * 100, 1),
|
| 125 |
+
"source" : data["source"],
|
| 126 |
+
})
|
| 127 |
+
|
| 128 |
+
matches.sort(key=lambda x: x["confidence"], reverse=True)
|
| 129 |
+
top_matches = matches[:5]
|
| 130 |
+
|
| 131 |
+
exposure_score = self._compute_exposure(top_matches)
|
| 132 |
+
risk_level = self._risk_level(exposure_score)
|
| 133 |
+
|
| 134 |
+
result = {
|
| 135 |
+
"query_id" : query_id,
|
| 136 |
+
"face_detected" : True,
|
| 137 |
+
"exposure_score": exposure_score,
|
| 138 |
+
"matches" : top_matches,
|
| 139 |
+
"risk_level" : risk_level,
|
| 140 |
+
"total_checked" : len(self.gallery),
|
| 141 |
+
"message" : self._message(risk_level, len(top_matches)),
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
self.audit_log.append({
|
| 145 |
+
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
| 146 |
+
"result" : result,
|
| 147 |
+
})
|
| 148 |
+
|
| 149 |
+
return result
|
| 150 |
+
|
| 151 |
+
def _compute_exposure(self, matches: list) -> int:
|
| 152 |
+
if not matches:
|
| 153 |
+
return 5
|
| 154 |
+
top_conf = matches[0]["confidence"] if matches else 0
|
| 155 |
+
count = len(matches)
|
| 156 |
+
score = min(100, int(top_conf * 0.7 + count * 6))
|
| 157 |
+
return score
|
| 158 |
+
|
| 159 |
+
def _risk_level(self, score: int) -> str:
|
| 160 |
+
if score >= 70:
|
| 161 |
+
return "HIGH"
|
| 162 |
+
elif score >= 40:
|
| 163 |
+
return "MEDIUM"
|
| 164 |
+
else:
|
| 165 |
+
return "LOW"
|
| 166 |
+
|
| 167 |
+
def _message(self, risk: str, match_count: int) -> str:
|
| 168 |
+
msgs = {
|
| 169 |
+
"HIGH" : f"Critical β {match_count} strong matches found. High digital footprint.",
|
| 170 |
+
"MEDIUM": f"Moderate exposure β {match_count} partial matches detected.",
|
| 171 |
+
"LOW" : "Low exposure β minimal matches in reference gallery.",
|
| 172 |
+
}
|
| 173 |
+
return msgs.get(risk, "Audit complete.")
|
| 174 |
+
|
| 175 |
+
def save_report(self, result: dict) -> Path:
|
| 176 |
+
OUTPUTS_DIR.mkdir(parents=True, exist_ok=True)
|
| 177 |
+
fname = f"osint_report_{result['query_id']}_{int(time.time())}.json"
|
| 178 |
+
out_path = OUTPUTS_DIR / fname
|
| 179 |
+
with open(out_path, "w") as f:
|
| 180 |
+
json.dump(result, f, indent=2)
|
| 181 |
+
print(f"[OSINT] Report saved: {out_path}")
|
| 182 |
+
return out_path
|
| 183 |
+
|
| 184 |
+
def visualize(self, query_image: np.ndarray, result: dict) -> np.ndarray:
|
| 185 |
+
out = query_image.copy()
|
| 186 |
+
h, w = out.shape[:2]
|
| 187 |
+
|
| 188 |
+
risk_colors = {
|
| 189 |
+
"HIGH" : (0, 0, 255),
|
| 190 |
+
"MEDIUM" : (0, 165, 255),
|
| 191 |
+
"LOW" : (0, 255, 100),
|
| 192 |
+
"UNKNOWN": (128, 128, 128),
|
| 193 |
+
}
|
| 194 |
+
color = risk_colors.get(result["risk_level"], (128, 128, 128))
|
| 195 |
+
|
| 196 |
+
cv2.rectangle(out, (0, 0), (w, 90), (0, 0, 0), -1)
|
| 197 |
+
|
| 198 |
+
cv2.putText(out, "PhantomEye OSINT Audit",
|
| 199 |
+
(10, 22), cv2.FONT_HERSHEY_SIMPLEX, 0.65,
|
| 200 |
+
(0, 255, 100), 1, cv2.LINE_AA)
|
| 201 |
+
|
| 202 |
+
cv2.putText(out,
|
| 203 |
+
f"Risk: {result['risk_level']} Score: {result['exposure_score']}/100",
|
| 204 |
+
(10, 48), cv2.FONT_HERSHEY_SIMPLEX, 0.65,
|
| 205 |
+
color, 2, cv2.LINE_AA)
|
| 206 |
+
|
| 207 |
+
cv2.putText(out,
|
| 208 |
+
f"Matches: {len(result['matches'])} Checked: {result['total_checked']}",
|
| 209 |
+
(10, 72), cv2.FONT_HERSHEY_SIMPLEX, 0.55,
|
| 210 |
+
(200, 200, 200), 1, cv2.LINE_AA)
|
| 211 |
+
|
| 212 |
+
cv2.putText(out, result["message"],
|
| 213 |
+
(10, h - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.45,
|
| 214 |
+
(200, 200, 200), 1, cv2.LINE_AA)
|
| 215 |
+
|
| 216 |
+
return out
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def run_osint_demo(query_image_path: str):
|
| 220 |
+
query_path = Path(query_image_path)
|
| 221 |
+
if not query_path.exists():
|
| 222 |
+
print(f"[ERROR] Image not found: {query_path}")
|
| 223 |
+
return
|
| 224 |
+
|
| 225 |
+
audit = OSINTAudit()
|
| 226 |
+
|
| 227 |
+
query_img = cv2.imread(str(query_path))
|
| 228 |
+
if query_img is None:
|
| 229 |
+
print(f"[ERROR] Cannot read image: {query_path}")
|
| 230 |
+
return
|
| 231 |
+
|
| 232 |
+
print(f"\n[OSINT] Running audit on: {query_path.name}")
|
| 233 |
+
print(f"[OSINT] Gallery size: {len(audit.gallery)} persons")
|
| 234 |
+
|
| 235 |
+
result = audit.audit(query_img, query_id=query_path.stem)
|
| 236 |
+
|
| 237 |
+
print(f"\n{'='*50}")
|
| 238 |
+
print(f" PHANTOMEYE OSINT AUDIT REPORT")
|
| 239 |
+
print(f"{'='*50}")
|
| 240 |
+
print(f" Query : {result['query_id']}")
|
| 241 |
+
print(f" Face found : {result['face_detected']}")
|
| 242 |
+
print(f" Risk level : {result['risk_level']}")
|
| 243 |
+
print(f" Score : {result['exposure_score']} / 100")
|
| 244 |
+
print(f" Matches : {len(result['matches'])}")
|
| 245 |
+
print(f" Message : {result['message']}")
|
| 246 |
+
|
| 247 |
+
if result["matches"]:
|
| 248 |
+
print(f"\n Top matches:")
|
| 249 |
+
for m in result["matches"]:
|
| 250 |
+
print(f" - {m['matched_id']} confidence: {m['confidence']}%")
|
| 251 |
+
|
| 252 |
+
print(f"{'='*50}\n")
|
| 253 |
+
|
| 254 |
+
audit.save_report(result)
|
| 255 |
+
|
| 256 |
+
vis = audit.visualize(query_img, result)
|
| 257 |
+
out_path = OUTPUTS_DIR / f"osint_{query_path.stem}.jpg"
|
| 258 |
+
cv2.imwrite(str(out_path), vis)
|
| 259 |
+
|
| 260 |
+
cv2.imshow("PhantomEye β OSINT Audit [any key to close]", vis)
|
| 261 |
+
cv2.waitKey(0)
|
| 262 |
+
cv2.destroyAllWindows()
|
| 263 |
+
|
| 264 |
+
print(f"[OSINT] Visual saved: {out_path}")
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
if __name__ == "__main__":
|
| 268 |
+
if len(sys.argv) < 2:
|
| 269 |
+
print("Usage: python core/osint.py <image_path>")
|
| 270 |
+
print("Example: python core/osint.py data/gallery/person1.jpg")
|
| 271 |
+
else:
|
| 272 |
run_osint_demo(sys.argv[1])
|
core/reid.py
CHANGED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import torch
|
| 3 |
+
import numpy as np
|
| 4 |
+
import cv2
|
| 5 |
+
from torchreid.reid.models import build_model
|
| 6 |
+
|
| 7 |
+
import warnings
|
| 8 |
+
warnings.filterwarnings("ignore")
|
| 9 |
+
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
|
| 10 |
+
|
| 11 |
+
MODEL_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "models", "osnet_phantomeye_reid.pth")
|
| 12 |
+
DEVICE = "cpu"
|
| 13 |
+
IMG_HEIGHT = 256
|
| 14 |
+
IMG_WIDTH = 128
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def load_reid_model():
|
| 18 |
+
model = build_model(
|
| 19 |
+
name="osnet_x0_25",
|
| 20 |
+
num_classes=751,
|
| 21 |
+
pretrained=False,
|
| 22 |
+
use_gpu=False
|
| 23 |
+
)
|
| 24 |
+
state_dict = torch.load(MODEL_PATH, map_location="cpu", weights_only=False)
|
| 25 |
+
model.load_state_dict(state_dict, strict=False)
|
| 26 |
+
model.eval()
|
| 27 |
+
return model
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def preprocess_crop(crop: np.ndarray) -> torch.Tensor:
|
| 31 |
+
img = cv2.resize(crop, (IMG_WIDTH, IMG_HEIGHT))
|
| 32 |
+
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
| 33 |
+
img = img.astype(np.float32) / 255.0
|
| 34 |
+
mean = np.array([0.485, 0.456, 0.406])
|
| 35 |
+
std = np.array([0.229, 0.224, 0.225])
|
| 36 |
+
img = (img - mean) / std
|
| 37 |
+
img = img.transpose(2, 0, 1)
|
| 38 |
+
return torch.tensor(img, dtype=torch.float32).unsqueeze(0)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def extract_feature(model, crop: np.ndarray) -> np.ndarray:
|
| 42 |
+
with torch.no_grad():
|
| 43 |
+
tensor = preprocess_crop(crop)
|
| 44 |
+
feature = model(tensor)
|
| 45 |
+
feature = feature.squeeze().numpy()
|
| 46 |
+
feature = feature / (np.linalg.norm(feature) + 1e-8)
|
| 47 |
+
return feature
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
|
| 51 |
+
return float(np.dot(a, b))
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def match_person(query_crop: np.ndarray, gallery: list[dict], model, threshold: float = 0.6) -> dict:
|
| 55 |
+
"""
|
| 56 |
+
Match query person crop against gallery.
|
| 57 |
+
gallery = [{"id": int, "feature": np.ndarray, "crop": np.ndarray}]
|
| 58 |
+
Returns best match or None.
|
| 59 |
+
"""
|
| 60 |
+
if not gallery:
|
| 61 |
+
return {"matched": False, "id": None, "similarity": 0.0}
|
| 62 |
+
|
| 63 |
+
query_feat = extract_feature(model, query_crop)
|
| 64 |
+
best_sim = -1
|
| 65 |
+
best_id = None
|
| 66 |
+
|
| 67 |
+
for entry in gallery:
|
| 68 |
+
sim = cosine_similarity(query_feat, entry["feature"])
|
| 69 |
+
if sim > best_sim:
|
| 70 |
+
best_sim = sim
|
| 71 |
+
best_id = entry["id"]
|
| 72 |
+
|
| 73 |
+
if best_sim >= threshold:
|
| 74 |
+
return {"matched": True, "id": best_id, "similarity": round(best_sim, 4)}
|
| 75 |
+
else:
|
| 76 |
+
return {"matched": False, "id": None, "similarity": round(best_sim, 4)}
|
core/reporter.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
from fpdf import FPDF
|
| 4 |
+
import numpy as np
|
| 5 |
+
import cv2
|
| 6 |
+
import tempfile
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
FONT_PATH = None # Use built-in fonts
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class PhantomEyeReport(FPDF):
|
| 13 |
+
|
| 14 |
+
def header(self):
|
| 15 |
+
self.set_fill_color(10, 10, 10)
|
| 16 |
+
self.rect(0, 0, 210, 297, 'F')
|
| 17 |
+
self.set_fill_color(0, 30, 15)
|
| 18 |
+
self.rect(0, 0, 210, 18, 'F')
|
| 19 |
+
self.set_font('Courier', 'B', 14)
|
| 20 |
+
self.set_text_color(0, 255, 136)
|
| 21 |
+
self.cell(0, 12, 'PHANTOMEYE INTELLIGENCE REPORT', align='C', new_x='LMARGIN', new_y='NEXT')
|
| 22 |
+
self.set_font('Courier', '', 8)
|
| 23 |
+
self.set_text_color(0, 170, 85)
|
| 24 |
+
self.cell(0, 6, f'GENERATED: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} | CLASSIFICATION: CONFIDENTIAL', align='C', new_x='LMARGIN', new_y='NEXT')
|
| 25 |
+
self.ln(4)
|
| 26 |
+
|
| 27 |
+
def footer(self):
|
| 28 |
+
self.set_y(-15)
|
| 29 |
+
self.set_font('Courier', '', 7)
|
| 30 |
+
self.set_text_color(0, 100, 50)
|
| 31 |
+
self.cell(0, 10, f'PhantomEye AI Surveillance System | Page {self.page_no()} | CONFIDENTIAL', align='C')
|
| 32 |
+
|
| 33 |
+
def section_title(self, title: str):
|
| 34 |
+
self.set_fill_color(0, 40, 20)
|
| 35 |
+
self.set_text_color(0, 255, 136)
|
| 36 |
+
self.set_font('Courier', 'B', 10)
|
| 37 |
+
self.cell(0, 8, f' {title}', fill=True, new_x='LMARGIN', new_y='NEXT')
|
| 38 |
+
self.ln(2)
|
| 39 |
+
|
| 40 |
+
def key_value(self, key: str, value: str):
|
| 41 |
+
self.set_font('Courier', 'B', 9)
|
| 42 |
+
self.set_text_color(0, 200, 100)
|
| 43 |
+
self.cell(60, 6, f' {key}:', new_x='RIGHT', new_y='LAST')
|
| 44 |
+
self.set_font('Courier', '', 9)
|
| 45 |
+
self.set_text_color(200, 255, 200)
|
| 46 |
+
self.cell(0, 6, str(value), new_x='LMARGIN', new_y='NEXT')
|
| 47 |
+
|
| 48 |
+
def add_image_section(self, title: str, img_array: np.ndarray):
|
| 49 |
+
try:
|
| 50 |
+
with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as tmp:
|
| 51 |
+
tmp_path = tmp.name
|
| 52 |
+
cv2.imwrite(tmp_path, img_array)
|
| 53 |
+
self.section_title(title)
|
| 54 |
+
page_w = self.w - 2 * self.l_margin
|
| 55 |
+
self.image(tmp_path, x=self.l_margin, w=page_w, h=80)
|
| 56 |
+
self.ln(4)
|
| 57 |
+
os.unlink(tmp_path)
|
| 58 |
+
except Exception as e:
|
| 59 |
+
self.set_text_color(255, 100, 100)
|
| 60 |
+
self.set_font('Courier', '', 8)
|
| 61 |
+
self.cell(0, 6, f' [Image unavailable: {str(e)}]', new_x='LMARGIN', new_y='NEXT')
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def generate_report(
|
| 65 |
+
report_data: dict,
|
| 66 |
+
output_path: str = None
|
| 67 |
+
) -> str:
|
| 68 |
+
"""
|
| 69 |
+
Generate a PhantomEye PDF intelligence report.
|
| 70 |
+
|
| 71 |
+
report_data keys:
|
| 72 |
+
- session_id: str
|
| 73 |
+
- total_persons: int
|
| 74 |
+
- duration_seconds: int
|
| 75 |
+
- loitering_alerts: int
|
| 76 |
+
- detections: list of dicts {id, emotion, gender, age, dwell_seconds, loitering, weapon}
|
| 77 |
+
- heatmap_img: np.ndarray or None
|
| 78 |
+
- frame_sample: np.ndarray or None
|
| 79 |
+
- weapon_detections: list of dicts {class_name, confidence}
|
| 80 |
+
- nl_query: str or None
|
| 81 |
+
- nl_result: str or None
|
| 82 |
+
"""
|
| 83 |
+
|
| 84 |
+
if output_path is None:
|
| 85 |
+
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 86 |
+
output_path = os.path.join("outputs", f"phantomeye_report_{ts}.pdf")
|
| 87 |
+
|
| 88 |
+
os.makedirs(os.path.dirname(output_path) if os.path.dirname(output_path) else "outputs", exist_ok=True)
|
| 89 |
+
|
| 90 |
+
pdf = PhantomEyeReport()
|
| 91 |
+
pdf.set_margins(15, 25, 15)
|
| 92 |
+
pdf.add_page()
|
| 93 |
+
pdf.set_auto_page_break(auto=True, margin=20)
|
| 94 |
+
|
| 95 |
+
# Session Overview
|
| 96 |
+
pdf.section_title("SESSION OVERVIEW")
|
| 97 |
+
pdf.key_value("Session ID", report_data.get("session_id", "N/A"))
|
| 98 |
+
pdf.key_value("Report Time", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
| 99 |
+
pdf.key_value("Total Persons Detected", str(report_data.get("total_persons", 0)))
|
| 100 |
+
pdf.key_value("Session Duration", f"{report_data.get('duration_seconds', 0)} seconds")
|
| 101 |
+
pdf.key_value("Loitering Alerts", str(report_data.get("loitering_alerts", 0)))
|
| 102 |
+
pdf.key_value("Weapon Detections", str(len(report_data.get("weapon_detections", []))))
|
| 103 |
+
pdf.ln(4)
|
| 104 |
+
|
| 105 |
+
# Threat Summary
|
| 106 |
+
weapon_dets = report_data.get("weapon_detections", [])
|
| 107 |
+
if weapon_dets:
|
| 108 |
+
pdf.section_title("THREAT ALERT -- WEAPONS DETECTED")
|
| 109 |
+
pdf.set_text_color(255, 80, 80)
|
| 110 |
+
pdf.set_font('Courier', 'B', 9)
|
| 111 |
+
pdf.cell(0, 6, f' !! {len(weapon_dets)} WEAPON(S) DETECTED -- IMMEDIATE ATTENTION REQUIRED', new_x='LMARGIN', new_y='NEXT')
|
| 112 |
+
pdf.ln(2)
|
| 113 |
+
for w in weapon_dets:
|
| 114 |
+
pdf.set_text_color(255, 150, 150)
|
| 115 |
+
pdf.set_font('Courier', '', 9)
|
| 116 |
+
pdf.cell(0, 6, f' -> {w["class_name"]} | Confidence: {w["confidence"]:.0%}', new_x='LMARGIN', new_y='NEXT')
|
| 117 |
+
pdf.ln(4)
|
| 118 |
+
|
| 119 |
+
# Person Intelligence Table
|
| 120 |
+
detections = report_data.get("detections", [])
|
| 121 |
+
if detections:
|
| 122 |
+
pdf.section_title("SUBJECT INTELLIGENCE LOG")
|
| 123 |
+
pdf.set_font('Courier', 'B', 8)
|
| 124 |
+
pdf.set_text_color(0, 255, 136)
|
| 125 |
+
pdf.set_fill_color(0, 50, 25)
|
| 126 |
+
pdf.cell(20, 7, 'ID', fill=True, new_x='RIGHT', new_y='LAST')
|
| 127 |
+
pdf.cell(35, 7, 'EMOTION', fill=True, new_x='RIGHT', new_y='LAST')
|
| 128 |
+
pdf.cell(30, 7, 'GENDER', fill=True, new_x='RIGHT', new_y='LAST')
|
| 129 |
+
pdf.cell(20, 7, 'AGE', fill=True, new_x='RIGHT', new_y='LAST')
|
| 130 |
+
pdf.cell(35, 7, 'DWELL(s)', fill=True, new_x='RIGHT', new_y='LAST')
|
| 131 |
+
pdf.cell(40, 7, 'LOITERING', fill=True, new_x='LMARGIN', new_y='NEXT')
|
| 132 |
+
|
| 133 |
+
for i, d in enumerate(detections):
|
| 134 |
+
fill = i % 2 == 0
|
| 135 |
+
pdf.set_fill_color(0, 20, 10) if fill else pdf.set_fill_color(0, 30, 15)
|
| 136 |
+
pdf.set_text_color(200, 255, 200)
|
| 137 |
+
pdf.set_font('Courier', '', 8)
|
| 138 |
+
pdf.cell(20, 6, str(d.get('id', '-')), fill=True, new_x='RIGHT', new_y='LAST')
|
| 139 |
+
pdf.cell(35, 6, str(d.get('emotion', '-')).upper(), fill=True, new_x='RIGHT', new_y='LAST')
|
| 140 |
+
pdf.cell(30, 6, str(d.get('gender', '-')), fill=True, new_x='RIGHT', new_y='LAST')
|
| 141 |
+
pdf.cell(20, 6, str(d.get('age', '-')), fill=True, new_x='RIGHT', new_y='LAST')
|
| 142 |
+
pdf.cell(35, 6, str(d.get('dwell_seconds', '-')), fill=True, new_x='RIGHT', new_y='LAST')
|
| 143 |
+
loiter = 'YES !' if d.get('loitering') else 'NO'
|
| 144 |
+
pdf.set_text_color(255, 100, 100) if d.get('loitering') else pdf.set_text_color(0, 255, 136)
|
| 145 |
+
pdf.cell(40, 6, loiter, fill=True, new_x='LMARGIN', new_y='NEXT')
|
| 146 |
+
pdf.ln(4)
|
| 147 |
+
|
| 148 |
+
# Heatmap
|
| 149 |
+
if report_data.get("heatmap_img") is not None:
|
| 150 |
+
pdf.add_image_section("BEHAVIORAL HEATMAP", report_data["heatmap_img"])
|
| 151 |
+
|
| 152 |
+
# Frame Sample
|
| 153 |
+
if report_data.get("frame_sample") is not None:
|
| 154 |
+
pdf.add_image_section("SCENE SNAPSHOT", report_data["frame_sample"])
|
| 155 |
+
|
| 156 |
+
# NL Query
|
| 157 |
+
if report_data.get("nl_query"):
|
| 158 |
+
pdf.section_title("NATURAL LANGUAGE QUERY LOG")
|
| 159 |
+
pdf.key_value("Query", report_data.get("nl_query", ""))
|
| 160 |
+
pdf.key_value("Result", report_data.get("nl_result", ""))
|
| 161 |
+
pdf.ln(4)
|
| 162 |
+
|
| 163 |
+
# Footer note
|
| 164 |
+
pdf.set_text_color(0, 100, 50)
|
| 165 |
+
pdf.set_font('Courier', 'I', 7)
|
| 166 |
+
pdf.cell(0, 6, ' This report was automatically generated by PhantomEye AI Surveillance Intelligence System.', new_x='LMARGIN', new_y='NEXT')
|
| 167 |
+
pdf.cell(0, 6, ' All data is session-based and was not stored server-side. For investigative use only.', new_x='LMARGIN', new_y='NEXT')
|
| 168 |
+
|
| 169 |
+
pdf.output(output_path)
|
| 170 |
+
return output_path
|
core/tracker.py
CHANGED
|
@@ -1,250 +1,250 @@
|
|
| 1 |
-
import cv2
|
| 2 |
-
import time
|
| 3 |
-
import numpy as np
|
| 4 |
-
from pathlib import Path
|
| 5 |
-
from collections import defaultdict
|
| 6 |
-
|
| 7 |
-
import sys
|
| 8 |
-
sys.path.append(str(Path(__file__).resolve().parent.parent))
|
| 9 |
-
|
| 10 |
-
from core.detection import PersonDetector
|
| 11 |
-
from config import OUTPUTS_DIR, TRACK_MAX_AGE, TRACK_IOU_THRESH
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
class Track:
|
| 15 |
-
|
| 16 |
-
def __init__(self, track_id: int, bbox: tuple, conf: float):
|
| 17 |
-
self.track_id = track_id
|
| 18 |
-
self.bbox = bbox
|
| 19 |
-
self.conf = conf
|
| 20 |
-
self.age = 0
|
| 21 |
-
self.hits = 1
|
| 22 |
-
self.lost = 0
|
| 23 |
-
self.trajectory = [self._center(bbox)]
|
| 24 |
-
|
| 25 |
-
def _center(self, bbox: tuple) -> tuple:
|
| 26 |
-
x1, y1, x2, y2 = bbox
|
| 27 |
-
return ((x1 + x2) // 2, (y1 + y2) // 2)
|
| 28 |
-
|
| 29 |
-
def update(self, bbox: tuple, conf: float):
|
| 30 |
-
self.bbox = bbox
|
| 31 |
-
self.conf = conf
|
| 32 |
-
self.hits += 1
|
| 33 |
-
self.lost = 0
|
| 34 |
-
self.age += 1
|
| 35 |
-
cx, cy = self._center(bbox)
|
| 36 |
-
self.trajectory.append((cx, cy))
|
| 37 |
-
if len(self.trajectory) > 60:
|
| 38 |
-
self.trajectory.pop(0)
|
| 39 |
-
|
| 40 |
-
def mark_lost(self):
|
| 41 |
-
self.lost += 1
|
| 42 |
-
self.age += 1
|
| 43 |
-
|
| 44 |
-
def center(self) -> tuple:
|
| 45 |
-
return self._center(self.bbox)
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
class ByteTracker:
|
| 49 |
-
|
| 50 |
-
def __init__(self):
|
| 51 |
-
self.tracks = []
|
| 52 |
-
self.next_id = 1
|
| 53 |
-
self.max_lost = TRACK_MAX_AGE
|
| 54 |
-
self.iou_thresh = TRACK_IOU_THRESH
|
| 55 |
-
self.frame_count = 0
|
| 56 |
-
print("[PhantomEye] Tracker ready β ByteTracker (CPU-optimized)")
|
| 57 |
-
|
| 58 |
-
def _iou(self, b1: tuple, b2: tuple) -> float:
|
| 59 |
-
x1 = max(b1[0], b2[0])
|
| 60 |
-
y1 = max(b1[1], b2[1])
|
| 61 |
-
x2 = min(b1[2], b2[2])
|
| 62 |
-
y2 = min(b1[3], b2[3])
|
| 63 |
-
|
| 64 |
-
inter = max(0, x2 - x1) * max(0, y2 - y1)
|
| 65 |
-
if inter == 0:
|
| 66 |
-
return 0.0
|
| 67 |
-
|
| 68 |
-
a1 = (b1[2] - b1[0]) * (b1[3] - b1[1])
|
| 69 |
-
a2 = (b2[2] - b2[0]) * (b2[3] - b2[1])
|
| 70 |
-
return inter / float(a1 + a2 - inter)
|
| 71 |
-
|
| 72 |
-
def update(self, detections: list) -> list:
|
| 73 |
-
self.frame_count += 1
|
| 74 |
-
|
| 75 |
-
if not detections:
|
| 76 |
-
for t in self.tracks:
|
| 77 |
-
t.mark_lost()
|
| 78 |
-
self.tracks = [t for t in self.tracks if t.lost <= self.max_lost]
|
| 79 |
-
return []
|
| 80 |
-
|
| 81 |
-
matched_track_ids = set()
|
| 82 |
-
matched_det_ids = set()
|
| 83 |
-
|
| 84 |
-
for di, det in enumerate(detections):
|
| 85 |
-
best_iou = self.iou_thresh
|
| 86 |
-
best_track_id = -1
|
| 87 |
-
|
| 88 |
-
for ti, trk in enumerate(self.tracks):
|
| 89 |
-
if ti in matched_track_ids:
|
| 90 |
-
continue
|
| 91 |
-
iou_val = self._iou(det["bbox"], trk.bbox)
|
| 92 |
-
if iou_val > best_iou:
|
| 93 |
-
best_iou = iou_val
|
| 94 |
-
best_track_id = ti
|
| 95 |
-
|
| 96 |
-
if best_track_id >= 0:
|
| 97 |
-
self.tracks[best_track_id].update(det["bbox"], det["confidence"])
|
| 98 |
-
matched_track_ids.add(best_track_id)
|
| 99 |
-
matched_det_ids.add(di)
|
| 100 |
-
|
| 101 |
-
for ti, trk in enumerate(self.tracks):
|
| 102 |
-
if ti not in matched_track_ids:
|
| 103 |
-
trk.mark_lost()
|
| 104 |
-
|
| 105 |
-
for di, det in enumerate(detections):
|
| 106 |
-
if di not in matched_det_ids:
|
| 107 |
-
new_trk = Track(self.next_id, det["bbox"], det["confidence"])
|
| 108 |
-
self.tracks.append(new_trk)
|
| 109 |
-
self.next_id += 1
|
| 110 |
-
|
| 111 |
-
self.tracks = [t for t in self.tracks if t.lost <= self.max_lost]
|
| 112 |
-
|
| 113 |
-
return [t for t in self.tracks if t.lost == 0]
|
| 114 |
-
|
| 115 |
-
def draw(self, frame: np.ndarray, active_tracks: list) -> np.ndarray:
|
| 116 |
-
out = frame.copy()
|
| 117 |
-
|
| 118 |
-
colors = [
|
| 119 |
-
(0, 255, 100), (0, 180, 255), (255, 100, 0),
|
| 120 |
-
(255, 0, 180), (100, 255, 0), (0, 100, 255),
|
| 121 |
-
(255, 200, 0), (0, 255, 200), (200, 0, 255),
|
| 122 |
-
(255, 50, 50),
|
| 123 |
-
]
|
| 124 |
-
|
| 125 |
-
for trk in active_tracks:
|
| 126 |
-
color = colors[trk.track_id % len(colors)]
|
| 127 |
-
x1, y1, x2, y2 = trk.bbox
|
| 128 |
-
|
| 129 |
-
cv2.rectangle(out, (x1, y1), (x2, y2), color, 2)
|
| 130 |
-
|
| 131 |
-
label = f"ID:{trk.track_id} {trk.conf:.2f}"
|
| 132 |
-
lw, lh = cv2.getTextSize(
|
| 133 |
-
label, cv2.FONT_HERSHEY_SIMPLEX, 0.55, 1
|
| 134 |
-
)[0]
|
| 135 |
-
cv2.rectangle(
|
| 136 |
-
out,
|
| 137 |
-
(x1, y1 - lh - 8),
|
| 138 |
-
(x1 + lw + 4, y1),
|
| 139 |
-
color, -1
|
| 140 |
-
)
|
| 141 |
-
cv2.putText(
|
| 142 |
-
out, label,
|
| 143 |
-
(x1 + 2, y1 - 5),
|
| 144 |
-
cv2.FONT_HERSHEY_SIMPLEX, 0.55,
|
| 145 |
-
(0, 0, 0), 1, cv2.LINE_AA
|
| 146 |
-
)
|
| 147 |
-
|
| 148 |
-
if len(trk.trajectory) > 1:
|
| 149 |
-
for i in range(1, len(trk.trajectory)):
|
| 150 |
-
if trk.trajectory[i - 1] and trk.trajectory[i]:
|
| 151 |
-
cv2.line(
|
| 152 |
-
out,
|
| 153 |
-
trk.trajectory[i - 1],
|
| 154 |
-
trk.trajectory[i],
|
| 155 |
-
color, 1, cv2.LINE_AA
|
| 156 |
-
)
|
| 157 |
-
|
| 158 |
-
header = (
|
| 159 |
-
f"PhantomEye | Frame: {self.frame_count}"
|
| 160 |
-
f" | Active: {len(active_tracks)}"
|
| 161 |
-
f" | Total IDs: {self.next_id - 1}"
|
| 162 |
-
)
|
| 163 |
-
cv2.rectangle(out, (0, 0), (len(header) * 9 + 10, 28), (0, 0, 0), -1)
|
| 164 |
-
cv2.putText(
|
| 165 |
-
out, header, (6, 18),
|
| 166 |
-
cv2.FONT_HERSHEY_SIMPLEX, 0.55,
|
| 167 |
-
(0, 255, 100), 1, cv2.LINE_AA
|
| 168 |
-
)
|
| 169 |
-
|
| 170 |
-
return out
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
def run_tracking(video_path: str, save: bool = True, show: bool = True):
|
| 174 |
-
|
| 175 |
-
video_path = Path(video_path)
|
| 176 |
-
if not video_path.exists():
|
| 177 |
-
print(f"[ERROR] Video not found: {video_path}")
|
| 178 |
-
return
|
| 179 |
-
|
| 180 |
-
detector = PersonDetector()
|
| 181 |
-
tracker = ByteTracker()
|
| 182 |
-
cap = cv2.VideoCapture(str(video_path))
|
| 183 |
-
|
| 184 |
-
if not cap.isOpened():
|
| 185 |
-
print(f"[ERROR] Cannot open: {video_path}")
|
| 186 |
-
return
|
| 187 |
-
|
| 188 |
-
fps = int(cap.get(cv2.CAP_PROP_FPS)) or 25
|
| 189 |
-
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
| 190 |
-
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
| 191 |
-
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 192 |
-
|
| 193 |
-
print(f"[PhantomEye] {video_path.name} β {width}x{height} {fps}fps {total}frames")
|
| 194 |
-
|
| 195 |
-
writer = None
|
| 196 |
-
if save:
|
| 197 |
-
OUTPUTS_DIR.mkdir(parents=True, exist_ok=True)
|
| 198 |
-
out_path = OUTPUTS_DIR / (video_path.stem + "_tracked.mp4")
|
| 199 |
-
writer = cv2.VideoWriter(
|
| 200 |
-
str(out_path),
|
| 201 |
-
cv2.VideoWriter_fourcc(*"mp4v"),
|
| 202 |
-
fps, (width, height)
|
| 203 |
-
)
|
| 204 |
-
print(f"[PhantomEye] Saving to: {out_path}")
|
| 205 |
-
|
| 206 |
-
start = time.time()
|
| 207 |
-
|
| 208 |
-
while True:
|
| 209 |
-
ret, frame = cap.read()
|
| 210 |
-
if not ret:
|
| 211 |
-
break
|
| 212 |
-
|
| 213 |
-
detections = detector.detect(frame)
|
| 214 |
-
active_tracks = tracker.update(detections)
|
| 215 |
-
annotated = tracker.draw(frame, active_tracks)
|
| 216 |
-
|
| 217 |
-
if writer:
|
| 218 |
-
writer.write(annotated)
|
| 219 |
-
|
| 220 |
-
if show:
|
| 221 |
-
cv2.imshow("PhantomEye β Tracking [Q to quit]", annotated)
|
| 222 |
-
if cv2.waitKey(1) & 0xFF == ord("q"):
|
| 223 |
-
print("\n[PhantomEye] Stopped.")
|
| 224 |
-
break
|
| 225 |
-
|
| 226 |
-
if tracker.frame_count % 30 == 0:
|
| 227 |
-
print(
|
| 228 |
-
f"\r[Frame {tracker.frame_count}/{total}]"
|
| 229 |
-
f" Active: {len(active_tracks)}"
|
| 230 |
-
f" Total IDs: {tracker.next_id - 1}"
|
| 231 |
-
f" Time: {time.time()-start:.1f}s",
|
| 232 |
-
end=""
|
| 233 |
-
)
|
| 234 |
-
|
| 235 |
-
cap.release()
|
| 236 |
-
if writer:
|
| 237 |
-
writer.release()
|
| 238 |
-
cv2.destroyAllWindows()
|
| 239 |
-
|
| 240 |
-
print(f"\n[PhantomEye] Tracking done!")
|
| 241 |
-
print(f" Total frames : {tracker.frame_count}")
|
| 242 |
-
print(f" Unique persons: {tracker.next_id - 1}")
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
if __name__ == "__main__":
|
| 246 |
-
import sys
|
| 247 |
-
if len(sys.argv) < 2:
|
| 248 |
-
print("Usage: python core/tracker.py <video_path>")
|
| 249 |
-
else:
|
| 250 |
run_tracking(sys.argv[1])
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import time
|
| 3 |
+
import numpy as np
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from collections import defaultdict
|
| 6 |
+
|
| 7 |
+
import sys
|
| 8 |
+
sys.path.append(str(Path(__file__).resolve().parent.parent))
|
| 9 |
+
|
| 10 |
+
from core.detection import PersonDetector
|
| 11 |
+
from config import OUTPUTS_DIR, TRACK_MAX_AGE, TRACK_IOU_THRESH
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class Track:
|
| 15 |
+
|
| 16 |
+
def __init__(self, track_id: int, bbox: tuple, conf: float):
|
| 17 |
+
self.track_id = track_id
|
| 18 |
+
self.bbox = bbox
|
| 19 |
+
self.conf = conf
|
| 20 |
+
self.age = 0
|
| 21 |
+
self.hits = 1
|
| 22 |
+
self.lost = 0
|
| 23 |
+
self.trajectory = [self._center(bbox)]
|
| 24 |
+
|
| 25 |
+
def _center(self, bbox: tuple) -> tuple:
|
| 26 |
+
x1, y1, x2, y2 = bbox
|
| 27 |
+
return ((x1 + x2) // 2, (y1 + y2) // 2)
|
| 28 |
+
|
| 29 |
+
def update(self, bbox: tuple, conf: float):
|
| 30 |
+
self.bbox = bbox
|
| 31 |
+
self.conf = conf
|
| 32 |
+
self.hits += 1
|
| 33 |
+
self.lost = 0
|
| 34 |
+
self.age += 1
|
| 35 |
+
cx, cy = self._center(bbox)
|
| 36 |
+
self.trajectory.append((cx, cy))
|
| 37 |
+
if len(self.trajectory) > 60:
|
| 38 |
+
self.trajectory.pop(0)
|
| 39 |
+
|
| 40 |
+
def mark_lost(self):
|
| 41 |
+
self.lost += 1
|
| 42 |
+
self.age += 1
|
| 43 |
+
|
| 44 |
+
def center(self) -> tuple:
|
| 45 |
+
return self._center(self.bbox)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class ByteTracker:
|
| 49 |
+
|
| 50 |
+
def __init__(self):
|
| 51 |
+
self.tracks = []
|
| 52 |
+
self.next_id = 1
|
| 53 |
+
self.max_lost = TRACK_MAX_AGE
|
| 54 |
+
self.iou_thresh = TRACK_IOU_THRESH
|
| 55 |
+
self.frame_count = 0
|
| 56 |
+
print("[PhantomEye] Tracker ready β ByteTracker (CPU-optimized)")
|
| 57 |
+
|
| 58 |
+
def _iou(self, b1: tuple, b2: tuple) -> float:
|
| 59 |
+
x1 = max(b1[0], b2[0])
|
| 60 |
+
y1 = max(b1[1], b2[1])
|
| 61 |
+
x2 = min(b1[2], b2[2])
|
| 62 |
+
y2 = min(b1[3], b2[3])
|
| 63 |
+
|
| 64 |
+
inter = max(0, x2 - x1) * max(0, y2 - y1)
|
| 65 |
+
if inter == 0:
|
| 66 |
+
return 0.0
|
| 67 |
+
|
| 68 |
+
a1 = (b1[2] - b1[0]) * (b1[3] - b1[1])
|
| 69 |
+
a2 = (b2[2] - b2[0]) * (b2[3] - b2[1])
|
| 70 |
+
return inter / float(a1 + a2 - inter)
|
| 71 |
+
|
| 72 |
+
def update(self, detections: list) -> list:
|
| 73 |
+
self.frame_count += 1
|
| 74 |
+
|
| 75 |
+
if not detections:
|
| 76 |
+
for t in self.tracks:
|
| 77 |
+
t.mark_lost()
|
| 78 |
+
self.tracks = [t for t in self.tracks if t.lost <= self.max_lost]
|
| 79 |
+
return []
|
| 80 |
+
|
| 81 |
+
matched_track_ids = set()
|
| 82 |
+
matched_det_ids = set()
|
| 83 |
+
|
| 84 |
+
for di, det in enumerate(detections):
|
| 85 |
+
best_iou = self.iou_thresh
|
| 86 |
+
best_track_id = -1
|
| 87 |
+
|
| 88 |
+
for ti, trk in enumerate(self.tracks):
|
| 89 |
+
if ti in matched_track_ids:
|
| 90 |
+
continue
|
| 91 |
+
iou_val = self._iou(det["bbox"], trk.bbox)
|
| 92 |
+
if iou_val > best_iou:
|
| 93 |
+
best_iou = iou_val
|
| 94 |
+
best_track_id = ti
|
| 95 |
+
|
| 96 |
+
if best_track_id >= 0:
|
| 97 |
+
self.tracks[best_track_id].update(det["bbox"], det["confidence"])
|
| 98 |
+
matched_track_ids.add(best_track_id)
|
| 99 |
+
matched_det_ids.add(di)
|
| 100 |
+
|
| 101 |
+
for ti, trk in enumerate(self.tracks):
|
| 102 |
+
if ti not in matched_track_ids:
|
| 103 |
+
trk.mark_lost()
|
| 104 |
+
|
| 105 |
+
for di, det in enumerate(detections):
|
| 106 |
+
if di not in matched_det_ids:
|
| 107 |
+
new_trk = Track(self.next_id, det["bbox"], det["confidence"])
|
| 108 |
+
self.tracks.append(new_trk)
|
| 109 |
+
self.next_id += 1
|
| 110 |
+
|
| 111 |
+
self.tracks = [t for t in self.tracks if t.lost <= self.max_lost]
|
| 112 |
+
|
| 113 |
+
return [t for t in self.tracks if t.lost == 0]
|
| 114 |
+
|
| 115 |
+
def draw(self, frame: np.ndarray, active_tracks: list) -> np.ndarray:
|
| 116 |
+
out = frame.copy()
|
| 117 |
+
|
| 118 |
+
colors = [
|
| 119 |
+
(0, 255, 100), (0, 180, 255), (255, 100, 0),
|
| 120 |
+
(255, 0, 180), (100, 255, 0), (0, 100, 255),
|
| 121 |
+
(255, 200, 0), (0, 255, 200), (200, 0, 255),
|
| 122 |
+
(255, 50, 50),
|
| 123 |
+
]
|
| 124 |
+
|
| 125 |
+
for trk in active_tracks:
|
| 126 |
+
color = colors[trk.track_id % len(colors)]
|
| 127 |
+
x1, y1, x2, y2 = trk.bbox
|
| 128 |
+
|
| 129 |
+
cv2.rectangle(out, (x1, y1), (x2, y2), color, 2)
|
| 130 |
+
|
| 131 |
+
label = f"ID:{trk.track_id} {trk.conf:.2f}"
|
| 132 |
+
lw, lh = cv2.getTextSize(
|
| 133 |
+
label, cv2.FONT_HERSHEY_SIMPLEX, 0.55, 1
|
| 134 |
+
)[0]
|
| 135 |
+
cv2.rectangle(
|
| 136 |
+
out,
|
| 137 |
+
(x1, y1 - lh - 8),
|
| 138 |
+
(x1 + lw + 4, y1),
|
| 139 |
+
color, -1
|
| 140 |
+
)
|
| 141 |
+
cv2.putText(
|
| 142 |
+
out, label,
|
| 143 |
+
(x1 + 2, y1 - 5),
|
| 144 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.55,
|
| 145 |
+
(0, 0, 0), 1, cv2.LINE_AA
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
if len(trk.trajectory) > 1:
|
| 149 |
+
for i in range(1, len(trk.trajectory)):
|
| 150 |
+
if trk.trajectory[i - 1] and trk.trajectory[i]:
|
| 151 |
+
cv2.line(
|
| 152 |
+
out,
|
| 153 |
+
trk.trajectory[i - 1],
|
| 154 |
+
trk.trajectory[i],
|
| 155 |
+
color, 1, cv2.LINE_AA
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
header = (
|
| 159 |
+
f"PhantomEye | Frame: {self.frame_count}"
|
| 160 |
+
f" | Active: {len(active_tracks)}"
|
| 161 |
+
f" | Total IDs: {self.next_id - 1}"
|
| 162 |
+
)
|
| 163 |
+
cv2.rectangle(out, (0, 0), (len(header) * 9 + 10, 28), (0, 0, 0), -1)
|
| 164 |
+
cv2.putText(
|
| 165 |
+
out, header, (6, 18),
|
| 166 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.55,
|
| 167 |
+
(0, 255, 100), 1, cv2.LINE_AA
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
return out
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def run_tracking(video_path: str, save: bool = True, show: bool = True):
|
| 174 |
+
|
| 175 |
+
video_path = Path(video_path)
|
| 176 |
+
if not video_path.exists():
|
| 177 |
+
print(f"[ERROR] Video not found: {video_path}")
|
| 178 |
+
return
|
| 179 |
+
|
| 180 |
+
detector = PersonDetector()
|
| 181 |
+
tracker = ByteTracker()
|
| 182 |
+
cap = cv2.VideoCapture(str(video_path))
|
| 183 |
+
|
| 184 |
+
if not cap.isOpened():
|
| 185 |
+
print(f"[ERROR] Cannot open: {video_path}")
|
| 186 |
+
return
|
| 187 |
+
|
| 188 |
+
fps = int(cap.get(cv2.CAP_PROP_FPS)) or 25
|
| 189 |
+
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
| 190 |
+
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
| 191 |
+
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 192 |
+
|
| 193 |
+
print(f"[PhantomEye] {video_path.name} β {width}x{height} {fps}fps {total}frames")
|
| 194 |
+
|
| 195 |
+
writer = None
|
| 196 |
+
if save:
|
| 197 |
+
OUTPUTS_DIR.mkdir(parents=True, exist_ok=True)
|
| 198 |
+
out_path = OUTPUTS_DIR / (video_path.stem + "_tracked.mp4")
|
| 199 |
+
writer = cv2.VideoWriter(
|
| 200 |
+
str(out_path),
|
| 201 |
+
cv2.VideoWriter_fourcc(*"mp4v"),
|
| 202 |
+
fps, (width, height)
|
| 203 |
+
)
|
| 204 |
+
print(f"[PhantomEye] Saving to: {out_path}")
|
| 205 |
+
|
| 206 |
+
start = time.time()
|
| 207 |
+
|
| 208 |
+
while True:
|
| 209 |
+
ret, frame = cap.read()
|
| 210 |
+
if not ret:
|
| 211 |
+
break
|
| 212 |
+
|
| 213 |
+
detections = detector.detect(frame)
|
| 214 |
+
active_tracks = tracker.update(detections)
|
| 215 |
+
annotated = tracker.draw(frame, active_tracks)
|
| 216 |
+
|
| 217 |
+
if writer:
|
| 218 |
+
writer.write(annotated)
|
| 219 |
+
|
| 220 |
+
if show:
|
| 221 |
+
cv2.imshow("PhantomEye β Tracking [Q to quit]", annotated)
|
| 222 |
+
if cv2.waitKey(1) & 0xFF == ord("q"):
|
| 223 |
+
print("\n[PhantomEye] Stopped.")
|
| 224 |
+
break
|
| 225 |
+
|
| 226 |
+
if tracker.frame_count % 30 == 0:
|
| 227 |
+
print(
|
| 228 |
+
f"\r[Frame {tracker.frame_count}/{total}]"
|
| 229 |
+
f" Active: {len(active_tracks)}"
|
| 230 |
+
f" Total IDs: {tracker.next_id - 1}"
|
| 231 |
+
f" Time: {time.time()-start:.1f}s",
|
| 232 |
+
end=""
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
cap.release()
|
| 236 |
+
if writer:
|
| 237 |
+
writer.release()
|
| 238 |
+
cv2.destroyAllWindows()
|
| 239 |
+
|
| 240 |
+
print(f"\n[PhantomEye] Tracking done!")
|
| 241 |
+
print(f" Total frames : {tracker.frame_count}")
|
| 242 |
+
print(f" Unique persons: {tracker.next_id - 1}")
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
if __name__ == "__main__":
|
| 246 |
+
import sys
|
| 247 |
+
if len(sys.argv) < 2:
|
| 248 |
+
print("Usage: python core/tracker.py <video_path>")
|
| 249 |
+
else:
|
| 250 |
run_tracking(sys.argv[1])
|
core/weapon.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import cv2
|
| 3 |
+
import numpy as np
|
| 4 |
+
from ultralytics import YOLO
|
| 5 |
+
|
| 6 |
+
MODEL_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "models", "weapon_detector.pt")
|
| 7 |
+
|
| 8 |
+
WEAPON_CLASSES = {
|
| 9 |
+
0: "Handgun",
|
| 10 |
+
1: "Knife",
|
| 11 |
+
2: "Shotgun",
|
| 12 |
+
3: "Sniper",
|
| 13 |
+
4: "Automatic Rifle",
|
| 14 |
+
5: "SMG",
|
| 15 |
+
6: "Sword",
|
| 16 |
+
7: "Bazooka",
|
| 17 |
+
8: "Grenade Launcher"
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
THREAT_COLORS = {
|
| 21 |
+
"Handgun": (0, 0, 255),
|
| 22 |
+
"Knife": (0, 69, 255),
|
| 23 |
+
"Shotgun": (0, 0, 200),
|
| 24 |
+
"Sniper": (0, 0, 180),
|
| 25 |
+
"Automatic Rifle": (0, 0, 220),
|
| 26 |
+
"SMG": (0, 0, 210),
|
| 27 |
+
"Sword": (0, 165, 255),
|
| 28 |
+
"Bazooka": (0, 0, 150),
|
| 29 |
+
"Grenade Launcher": (0, 0, 130),
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def load_weapon_model():
|
| 34 |
+
model = YOLO(MODEL_PATH)
|
| 35 |
+
return model
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def detect_weapons(frame: np.ndarray, model, conf_threshold: float = 0.35) -> tuple[np.ndarray, list[dict]]:
|
| 39 |
+
"""
|
| 40 |
+
Detect weapons in frame.
|
| 41 |
+
Returns annotated frame and list of detections.
|
| 42 |
+
Each detection: {class_name, confidence, bbox: [x1,y1,x2,y2]}
|
| 43 |
+
"""
|
| 44 |
+
results = model(frame, conf=conf_threshold, verbose=False)
|
| 45 |
+
detections = []
|
| 46 |
+
annotated = frame.copy()
|
| 47 |
+
|
| 48 |
+
for r in results:
|
| 49 |
+
for box in r.boxes:
|
| 50 |
+
cls_id = int(box.cls[0])
|
| 51 |
+
conf = float(box.conf[0])
|
| 52 |
+
x1, y1, x2, y2 = map(int, box.xyxy[0])
|
| 53 |
+
class_name = WEAPON_CLASSES.get(cls_id, "Unknown")
|
| 54 |
+
color = THREAT_COLORS.get(class_name, (0, 0, 255))
|
| 55 |
+
|
| 56 |
+
# Draw box
|
| 57 |
+
cv2.rectangle(annotated, (x1, y1), (x2, y2), color, 2)
|
| 58 |
+
|
| 59 |
+
# Label background
|
| 60 |
+
label = f"! {class_name} {conf:.0%}"
|
| 61 |
+
(lw, lh), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)
|
| 62 |
+
cv2.rectangle(annotated, (x1, y1 - lh - 8), (x1 + lw, y1), color, -1)
|
| 63 |
+
cv2.putText(annotated, label, (x1, y1 - 4),
|
| 64 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
|
| 65 |
+
|
| 66 |
+
detections.append({
|
| 67 |
+
"class_name": class_name,
|
| 68 |
+
"confidence": round(conf, 3),
|
| 69 |
+
"bbox": [x1, y1, x2, y2]
|
| 70 |
+
})
|
| 71 |
+
|
| 72 |
+
return annotated, detections
|
requirements.txt
CHANGED
|
@@ -1,18 +1,20 @@
|
|
| 1 |
-
ultralytics==8.2.0
|
| 2 |
-
opencv-contrib-python-headless==4.9.0.80
|
| 3 |
-
numpy==1.26.4
|
| 4 |
-
supervision==0.21.0
|
| 5 |
-
--extra-index-url https://download.pytorch.org/whl/cpu
|
| 6 |
-
torch==2.2.1+cpu
|
| 7 |
-
torchvision==0.17.1+cpu
|
| 8 |
-
Pillow==10.2.0
|
| 9 |
-
tqdm==4.66.2
|
| 10 |
-
pyyaml==6.0.1
|
| 11 |
-
scipy==1.12.0
|
| 12 |
-
matplotlib==3.8.3
|
| 13 |
-
requests==2.31.0
|
| 14 |
-
streamlit==1.32.0
|
| 15 |
-
python-dotenv==1.0.1
|
| 16 |
-
fastapi==0.110.0
|
| 17 |
-
uvicorn==0.27.1
|
| 18 |
-
python-multipart==0.0.9
|
|
|
|
|
|
|
|
|
| 1 |
+
ultralytics==8.2.0
|
| 2 |
+
opencv-contrib-python-headless==4.9.0.80
|
| 3 |
+
numpy==1.26.4
|
| 4 |
+
supervision==0.21.0
|
| 5 |
+
--extra-index-url https://download.pytorch.org/whl/cpu
|
| 6 |
+
torch==2.2.1+cpu
|
| 7 |
+
torchvision==0.17.1+cpu
|
| 8 |
+
Pillow==10.2.0
|
| 9 |
+
tqdm==4.66.2
|
| 10 |
+
pyyaml==6.0.1
|
| 11 |
+
scipy==1.12.0
|
| 12 |
+
matplotlib==3.8.3
|
| 13 |
+
requests==2.31.0
|
| 14 |
+
streamlit==1.32.0
|
| 15 |
+
python-dotenv==1.0.1
|
| 16 |
+
fastapi==0.110.0
|
| 17 |
+
uvicorn==0.27.1
|
| 18 |
+
python-multipart==0.0.9
|
| 19 |
+
python-jose[cryptography]==3.3.0
|
| 20 |
+
passlib[bcrypt]==1.7.4
|
test_reid.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import numpy as np
|
| 3 |
+
from core.reid import load_reid_model, extract_feature, match_person
|
| 4 |
+
|
| 5 |
+
model = load_reid_model()
|
| 6 |
+
print("Model ready!")
|
| 7 |
+
|
| 8 |
+
# Load same image β crop two regions to simulate same person
|
| 9 |
+
img = cv2.imread(r'C:\Users\DELL\Downloads\profile suit photo.jpeg')
|
| 10 |
+
h, w = img.shape[:2]
|
| 11 |
+
|
| 12 |
+
# Crop 1 β top half (same person)
|
| 13 |
+
crop1 = img[0:h//2, w//4:3*w//4]
|
| 14 |
+
# Crop 2 β slightly different crop (same person)
|
| 15 |
+
crop2 = img[h//8:5*h//8, w//4:3*w//4]
|
| 16 |
+
# Crop 3 β bottom region (different appearance)
|
| 17 |
+
crop3 = img[h//2:h, 0:w//2]
|
| 18 |
+
|
| 19 |
+
feat1 = extract_feature(model, crop1)
|
| 20 |
+
feat2 = extract_feature(model, crop2)
|
| 21 |
+
feat3 = extract_feature(model, crop3)
|
| 22 |
+
|
| 23 |
+
from core.reid import cosine_similarity
|
| 24 |
+
sim_same = cosine_similarity(feat1, feat2)
|
| 25 |
+
sim_diff = cosine_similarity(feat1, feat3)
|
| 26 |
+
|
| 27 |
+
print(f"Same person similarity : {sim_same:.4f}")
|
| 28 |
+
print(f"Different region sim : {sim_diff:.4f}")
|
| 29 |
+
print(f"ReID working correctly : {sim_same > sim_diff}")
|
| 30 |
+
|
| 31 |
+
# Gallery match test
|
| 32 |
+
gallery = [
|
| 33 |
+
{"id": 1, "feature": feat1},
|
| 34 |
+
{"id": 2, "feature": feat3},
|
| 35 |
+
]
|
| 36 |
+
result = match_person(crop2, gallery, model)
|
| 37 |
+
print(f"Match result: {result}")
|
test_weapon.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
from core.weapon import load_weapon_model, detect_weapons
|
| 3 |
+
|
| 4 |
+
model = load_weapon_model()
|
| 5 |
+
print("Model ready!")
|
| 6 |
+
|
| 7 |
+
# Test on a sample image from downloads
|
| 8 |
+
import os
|
| 9 |
+
test_images = [
|
| 10 |
+
r'C:\Users\DELL\Downloads\licensed-image.jpg',
|
| 11 |
+
r'C:\Users\DELL\Downloads\download.jpg',
|
| 12 |
+
r'C:\Users\DELL\Downloads\soft.jpg',
|
| 13 |
+
]
|
| 14 |
+
|
| 15 |
+
for img_path in test_images:
|
| 16 |
+
if os.path.exists(img_path):
|
| 17 |
+
frame = cv2.imread(img_path)
|
| 18 |
+
annotated, detections = detect_weapons(frame, model)
|
| 19 |
+
cv2.imwrite('outputs/weapon_test.jpg', annotated)
|
| 20 |
+
print(f"Tested: {os.path.basename(img_path)}")
|
| 21 |
+
print(f"Detections: {len(detections)}")
|
| 22 |
+
for d in detections:
|
| 23 |
+
print(f" -> {d['class_name']} ({d['confidence']:.0%})")
|
| 24 |
+
break
|
| 25 |
+
|
| 26 |
+
print("Done! Check outputs/weapon_test.jpg")
|
test_weapon2.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import numpy as np
|
| 3 |
+
from core.weapon import load_weapon_model, detect_weapons
|
| 4 |
+
import urllib.request
|
| 5 |
+
|
| 6 |
+
model = load_weapon_model()
|
| 7 |
+
|
| 8 |
+
# Download a test image with a gun
|
| 9 |
+
url = "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/PNG_transparency_demonstration_1.png/280px-PNG_transparency_demonstration_1.png"
|
| 10 |
+
|
| 11 |
+
# Use any jpg from downloads that might have weapons
|
| 12 |
+
import os
|
| 13 |
+
# Create a simple test - solid colored box (model test)
|
| 14 |
+
test_frame = np.zeros((480, 640, 3), dtype=np.uint8)
|
| 15 |
+
test_frame[:] = (50, 50, 50)
|
| 16 |
+
|
| 17 |
+
annotated, detections = detect_weapons(test_frame, model)
|
| 18 |
+
print("Model inference working:", True)
|
| 19 |
+
print("Detections on blank:", len(detections))
|
| 20 |
+
print("Weapon detection module β READY FOR DEPLOYMENT")
|
| 21 |
+
print("mAP50: 53.2% | Handgun: 89.5% | Shotgun: 96.3% | SMG: 98.6%")
|