Spaces:
Running
Running
Commit
·
6064a78
1
Parent(s):
e516e1a
setup
Browse files- .gitignore +176 -0
- Dockerfile +19 -0
- README copy.md +12 -0
- app/api/__init__.py +0 -0
- app/api/routes.py +78 -0
- app/core/__init__.py +0 -0
- app/core/config.py +11 -0
- app/core/firebase_config.py +41 -0
- app/core/logging_config.py +5 -0
- app/main.py +28 -0
- app/services/__init__.py +0 -0
- app/services/antispoof_service.py +62 -0
- app/services/audio_service.py +85 -0
- app/services/image_service.py +46 -0
- app/services/video_service.py +131 -0
- app/utils/__init__.py +0 -0
- app/utils/file_utils.py +55 -0
- app/utils/hash_utils.py +39 -0
- app/utils/image_utils.py +92 -0
- models/mobilenetv2_spoof_model.h5 +3 -0
- models/model_config.json +1 -0
- render.yaml +11 -0
- requirements.txt +17 -0
.gitignore
ADDED
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Created by https://www.toptal.com/developers/gitignore/api/python
|
2 |
+
# Edit at https://www.toptal.com/developers/gitignore?templates=python
|
3 |
+
|
4 |
+
### Python ###
|
5 |
+
# Byte-compiled / optimized / DLL files
|
6 |
+
__pycache__/
|
7 |
+
*.py[cod]
|
8 |
+
*$py.class
|
9 |
+
|
10 |
+
# C extensions
|
11 |
+
*.so
|
12 |
+
|
13 |
+
# Distribution / packaging
|
14 |
+
.Python
|
15 |
+
build/
|
16 |
+
develop-eggs/
|
17 |
+
dist/
|
18 |
+
downloads/
|
19 |
+
eggs/
|
20 |
+
.eggs/
|
21 |
+
lib/
|
22 |
+
lib64/
|
23 |
+
parts/
|
24 |
+
sdist/
|
25 |
+
var/
|
26 |
+
wheels/
|
27 |
+
share/python-wheels/
|
28 |
+
*.egg-info/
|
29 |
+
.installed.cfg
|
30 |
+
*.egg
|
31 |
+
MANIFEST
|
32 |
+
|
33 |
+
# PyInstaller
|
34 |
+
# Usually these files are written by a python script from a template
|
35 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
36 |
+
*.manifest
|
37 |
+
*.spec
|
38 |
+
|
39 |
+
# Installer logs
|
40 |
+
pip-log.txt
|
41 |
+
pip-delete-this-directory.txt
|
42 |
+
|
43 |
+
# Unit test / coverage reports
|
44 |
+
htmlcov/
|
45 |
+
.tox/
|
46 |
+
.nox/
|
47 |
+
.coverage
|
48 |
+
.coverage.*
|
49 |
+
.cache
|
50 |
+
nosetests.xml
|
51 |
+
coverage.xml
|
52 |
+
*.cover
|
53 |
+
*.py,cover
|
54 |
+
.hypothesis/
|
55 |
+
.pytest_cache/
|
56 |
+
cover/
|
57 |
+
|
58 |
+
# Translations
|
59 |
+
*.mo
|
60 |
+
*.pot
|
61 |
+
|
62 |
+
# Django stuff:
|
63 |
+
*.log
|
64 |
+
local_settings.py
|
65 |
+
db.sqlite3
|
66 |
+
db.sqlite3-journal
|
67 |
+
|
68 |
+
# Flask stuff:
|
69 |
+
instance/
|
70 |
+
.webassets-cache
|
71 |
+
|
72 |
+
# Scrapy stuff:
|
73 |
+
.scrapy
|
74 |
+
|
75 |
+
# Sphinx documentation
|
76 |
+
docs/_build/
|
77 |
+
|
78 |
+
# PyBuilder
|
79 |
+
.pybuilder/
|
80 |
+
target/
|
81 |
+
|
82 |
+
# Jupyter Notebook
|
83 |
+
.ipynb_checkpoints
|
84 |
+
|
85 |
+
# IPython
|
86 |
+
profile_default/
|
87 |
+
ipython_config.py
|
88 |
+
|
89 |
+
# pyenv
|
90 |
+
# For a library or package, you might want to ignore these files since the code is
|
91 |
+
# intended to run in multiple environments; otherwise, check them in:
|
92 |
+
# .python-version
|
93 |
+
|
94 |
+
# pipenv
|
95 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
96 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
97 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
98 |
+
# install all needed dependencies.
|
99 |
+
#Pipfile.lock
|
100 |
+
|
101 |
+
# poetry
|
102 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
103 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
104 |
+
# commonly ignored for libraries.
|
105 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
106 |
+
#poetry.lock
|
107 |
+
|
108 |
+
# pdm
|
109 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
110 |
+
#pdm.lock
|
111 |
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
112 |
+
# in version control.
|
113 |
+
# https://pdm.fming.dev/#use-with-ide
|
114 |
+
.pdm.toml
|
115 |
+
|
116 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
117 |
+
__pypackages__/
|
118 |
+
|
119 |
+
# Celery stuff
|
120 |
+
celerybeat-schedule
|
121 |
+
celerybeat.pid
|
122 |
+
|
123 |
+
# SageMath parsed files
|
124 |
+
*.sage.py
|
125 |
+
|
126 |
+
# Environments
|
127 |
+
.env
|
128 |
+
.venv
|
129 |
+
env/
|
130 |
+
venv/
|
131 |
+
ENV/
|
132 |
+
env.bak/
|
133 |
+
venv.bak/
|
134 |
+
|
135 |
+
# Spyder project settings
|
136 |
+
.spyderproject
|
137 |
+
.spyproject
|
138 |
+
|
139 |
+
# Rope project settings
|
140 |
+
.ropeproject
|
141 |
+
|
142 |
+
# mkdocs documentation
|
143 |
+
/site
|
144 |
+
|
145 |
+
# mypy
|
146 |
+
.mypy_cache/
|
147 |
+
.dmypy.json
|
148 |
+
dmypy.json
|
149 |
+
|
150 |
+
# Pyre type checker
|
151 |
+
.pyre/
|
152 |
+
|
153 |
+
# pytype static type analyzer
|
154 |
+
.pytype/
|
155 |
+
|
156 |
+
# Cython debug symbols
|
157 |
+
cython_debug/
|
158 |
+
|
159 |
+
# PyCharm
|
160 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
161 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
162 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
163 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
164 |
+
#.idea/
|
165 |
+
|
166 |
+
### Python Patch ###
|
167 |
+
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
|
168 |
+
poetry.toml
|
169 |
+
|
170 |
+
# ruff
|
171 |
+
.ruff_cache/
|
172 |
+
|
173 |
+
# LSP config files
|
174 |
+
pyrightconfig.json
|
175 |
+
|
176 |
+
# End of https://www.toptal.com/developers/gitignore/api/python
|
Dockerfile
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.10.9
|
2 |
+
|
3 |
+
WORKDIR /code
|
4 |
+
|
5 |
+
COPY ./requirements.txt /code/requirements.txt
|
6 |
+
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
7 |
+
|
8 |
+
COPY . /code/
|
9 |
+
|
10 |
+
# Expose the secret FIREBASE_KEY_BASE64 at buildtime and save it as a file
|
11 |
+
RUN --mount=type=secret,id=FIREBASE_KEY_BASE64,mode=0444,required=true \
|
12 |
+
cat /run/secrets/FIREBASE_KEY_BASE64 | base64 -d > /code/firebase_key.json
|
13 |
+
|
14 |
+
# Set the Google Application Credentials environment variable
|
15 |
+
ENV GOOGLE_APPLICATION_CREDENTIALS="/code/firebase_key.json"
|
16 |
+
|
17 |
+
# The FIREBASE_STORAGE_BUCKET will be available at runtime as an environment variable
|
18 |
+
|
19 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README copy.md
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
use python 3.10 for this project as the audio extraction library can work with this version only
|
2 |
+
|
3 |
+
create a virtual environment : .\venv\Scripts\activate
|
4 |
+
|
5 |
+
install the required libraries using : pip install -r requirements.txt
|
6 |
+
|
7 |
+
run the app using : uvicorn app.main:app --reload
|
8 |
+
|
9 |
+
The issue you are facing regarding while re uploading the same image or audio the path error shows up is die to mssing ffmpeg installation on your device the solution for the same :
|
10 |
+
- open cmd and type : winget install ffmpeg
|
11 |
+
- after the installation has been sucessfully done add the bin path to the System environment variables
|
12 |
+
- if u fail to find the path just type : where ffmpeg , in cmd and you will get the path
|
app/api/__init__.py
ADDED
File without changes
|
app/api/routes.py
ADDED
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, HTTPException, Response
|
2 |
+
from pydantic import BaseModel
|
3 |
+
from app.services import video_service, image_service, antispoof_service
|
4 |
+
from app.services.antispoof_service import antispoof_service
|
5 |
+
from app.services.image_service import compare_images
|
6 |
+
import logging
|
7 |
+
|
8 |
+
router = APIRouter()
|
9 |
+
|
10 |
+
class ContentRequest(BaseModel):
|
11 |
+
url: str
|
12 |
+
|
13 |
+
class CompareRequest(BaseModel):
|
14 |
+
url1: str
|
15 |
+
url2: str
|
16 |
+
|
17 |
+
@router.get("/health")
|
18 |
+
@router.head("/health")
|
19 |
+
async def health_check():
|
20 |
+
"""
|
21 |
+
Health check endpoint that responds to both GET and HEAD requests.
|
22 |
+
"""
|
23 |
+
return Response(content="OK", media_type="text/plain")
|
24 |
+
|
25 |
+
@router.post("/fingerprint")
|
26 |
+
async def create_fingerprint(request: ContentRequest):
|
27 |
+
try:
|
28 |
+
result = await video_service.fingerprint_video(request.url)
|
29 |
+
return {"message": "Fingerprint processing completed", "result": result}
|
30 |
+
except Exception as e:
|
31 |
+
logging.error(f"Error in fingerprint processing: {str(e)}")
|
32 |
+
raise HTTPException(status_code=500, detail=f"Error in fingerprint processing: {str(e)}")
|
33 |
+
|
34 |
+
@router.post("/verify_video_only")
|
35 |
+
async def verify_video_only(request: ContentRequest):
|
36 |
+
try:
|
37 |
+
result = await video_service.fingerprint_video(request.url)
|
38 |
+
return {"message": "Video verification completed", "result": result}
|
39 |
+
except Exception as e:
|
40 |
+
logging.error(f"Error in video verification: {str(e)}")
|
41 |
+
raise HTTPException(status_code=500, detail=f"Error in video verification: {str(e)}")
|
42 |
+
|
43 |
+
@router.post("/verify_liveness")
|
44 |
+
async def verify_liveness(request: ContentRequest):
|
45 |
+
try:
|
46 |
+
result = await antispoof_service.verify_liveness(request.url)
|
47 |
+
return {"message": "Liveness verification completed", "result": result}
|
48 |
+
except Exception as e:
|
49 |
+
logging.error(f"Error in liveness verification: {str(e)}")
|
50 |
+
raise HTTPException(status_code=500, detail=f"Error in liveness verification: {str(e)}")
|
51 |
+
|
52 |
+
@router.post("/compare_videos")
|
53 |
+
async def compare_videos_route(request: CompareRequest):
|
54 |
+
try:
|
55 |
+
result = await video_service.compare_videos(request.url1, request.url2)
|
56 |
+
return {"message": "Video comparison completed", "result": result}
|
57 |
+
except Exception as e:
|
58 |
+
logging.error(f"Error in video comparison: {str(e)}")
|
59 |
+
raise HTTPException(status_code=500, detail=f"Error in video comparison: {str(e)}")
|
60 |
+
|
61 |
+
@router.post("/verify_image")
|
62 |
+
async def verify_image_route(request: ContentRequest):
|
63 |
+
try:
|
64 |
+
result = await image_service.verify_image(request.url)
|
65 |
+
return {"message": "Image verification completed", "result": result}
|
66 |
+
except Exception as e:
|
67 |
+
logging.error(f"Error in image verification: {str(e)}")
|
68 |
+
raise HTTPException(status_code=500, detail=f"Error in image verification: {str(e)}")
|
69 |
+
|
70 |
+
@router.post("/compare_images")
|
71 |
+
async def compare_images_route(request: CompareRequest):
|
72 |
+
try:
|
73 |
+
# Call the image comparison service with the URLs from the request body
|
74 |
+
result = await compare_images(request.url1, request.url2)
|
75 |
+
return {"message": "Image comparison completed", "result": result}
|
76 |
+
except Exception as e:
|
77 |
+
logging.error(f"Error in image comparison: {str(e)}")
|
78 |
+
raise HTTPException(status_code=500, detail=f"Error in image comparison: {str(e)}")
|
app/core/__init__.py
ADDED
File without changes
|
app/core/config.py
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
from pathlib import Path
|
3 |
+
|
4 |
+
# Base directory of the project
|
5 |
+
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
6 |
+
|
7 |
+
|
8 |
+
# Anti-spoofing model and config paths
|
9 |
+
MODEL_PATH = os.path.join(BASE_DIR, "models", "mobilenetv2_spoof_model.h5")
|
10 |
+
CONFIG_PATH = os.path.join(BASE_DIR, "models", "model_config.json")
|
11 |
+
|
app/core/firebase_config.py
ADDED
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import base64
|
3 |
+
import json
|
4 |
+
from firebase_admin import credentials, initialize_app, storage
|
5 |
+
from dotenv import load_dotenv
|
6 |
+
|
7 |
+
load_dotenv()
|
8 |
+
|
9 |
+
_firebase_app = None
|
10 |
+
_firebase_bucket = None
|
11 |
+
|
12 |
+
def initialize_firebase():
|
13 |
+
global _firebase_app, _firebase_bucket
|
14 |
+
|
15 |
+
if _firebase_app is not None:
|
16 |
+
return _firebase_bucket
|
17 |
+
|
18 |
+
firebase_key_base64 = os.getenv('FIREBASE_KEY_BASE64')
|
19 |
+
if not firebase_key_base64:
|
20 |
+
raise ValueError("Environment variable FIREBASE_KEY_BASE64 is not set or is empty")
|
21 |
+
|
22 |
+
try:
|
23 |
+
print("Decoding Firebase key...")
|
24 |
+
firebase_key_json = base64.b64decode(firebase_key_base64).decode('utf-8')
|
25 |
+
firebase_key_dict = json.loads(firebase_key_json)
|
26 |
+
|
27 |
+
print("Initializing Firebase...")
|
28 |
+
cred = credentials.Certificate(firebase_key_dict)
|
29 |
+
|
30 |
+
# Initialize Firebase with a unique name if needed
|
31 |
+
_firebase_app = initialize_app(cred, {
|
32 |
+
'storageBucket': os.getenv('FIREBASE_STORAGE_BUCKET')
|
33 |
+
})
|
34 |
+
|
35 |
+
_firebase_bucket = storage.bucket(app=_firebase_app)
|
36 |
+
print("Firebase initialized successfully.")
|
37 |
+
return _firebase_bucket
|
38 |
+
except Exception as e:
|
39 |
+
raise RuntimeError(f"Error initializing Firebase: {e}")
|
40 |
+
|
41 |
+
firebase_bucket = initialize_firebase()
|
app/core/logging_config.py
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
|
3 |
+
def configure_logging():
|
4 |
+
logging.basicConfig(level=logging.INFO,
|
5 |
+
format='%(asctime)s - %(levelname)s - %(message)s')
|
app/main.py
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
from fastapi import FastAPI, Request
|
3 |
+
from fastapi.responses import JSONResponse
|
4 |
+
from app.api.routes import router
|
5 |
+
from app.core.logging_config import configure_logging
|
6 |
+
from app.core.firebase_config import initialize_firebase
|
7 |
+
import logging
|
8 |
+
|
9 |
+
app = FastAPI()
|
10 |
+
|
11 |
+
@app.on_event("startup")
|
12 |
+
async def startup_event():
|
13 |
+
configure_logging()
|
14 |
+
initialize_firebase()
|
15 |
+
|
16 |
+
app.include_router(router)
|
17 |
+
|
18 |
+
@app.exception_handler(Exception)
|
19 |
+
async def global_exception_handler(request: Request, exc: Exception):
|
20 |
+
logging.error(f"Unhandled exception: {str(exc)}", exc_info=True)
|
21 |
+
return JSONResponse(
|
22 |
+
status_code=500,
|
23 |
+
content={"message": "An unexpected error occurred. Please try again later."}
|
24 |
+
)
|
25 |
+
|
26 |
+
if __name__ == "__main__":
|
27 |
+
import uvicorn
|
28 |
+
uvicorn.run(app, host="0.0.0.0", port=8000, log_level="info")
|
app/services/__init__.py
ADDED
File without changes
|
app/services/antispoof_service.py
ADDED
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
from app.core.config import MODEL_PATH, CONFIG_PATH
|
3 |
+
from app.utils.file_utils import download_file, remove_temp_file
|
4 |
+
from fastapi import HTTPException
|
5 |
+
from app.core.firebase_config import firebase_bucket
|
6 |
+
from io import BytesIO
|
7 |
+
from PIL import Image
|
8 |
+
import tensorflow as tf
|
9 |
+
import json
|
10 |
+
import numpy as np
|
11 |
+
|
12 |
+
class AntispoofService:
|
13 |
+
def __init__(self):
|
14 |
+
self.model = None
|
15 |
+
self.config = None
|
16 |
+
self.load_model_and_config()
|
17 |
+
|
18 |
+
def load_model_and_config(self):
|
19 |
+
try:
|
20 |
+
self.model = tf.keras.models.load_model(MODEL_PATH)
|
21 |
+
with open(CONFIG_PATH, "r") as config_file:
|
22 |
+
self.config = json.load(config_file)
|
23 |
+
logging.info("Model and configuration loaded successfully.")
|
24 |
+
except Exception as e:
|
25 |
+
logging.error(f"Error loading model or config: {str(e)}")
|
26 |
+
raise
|
27 |
+
|
28 |
+
async def verify_liveness(self, image_url: str):
|
29 |
+
if self.model is None or self.config is None:
|
30 |
+
raise ValueError("Model or configuration not loaded")
|
31 |
+
|
32 |
+
firebase_filename = None
|
33 |
+
try:
|
34 |
+
firebase_filename = await download_file(image_url)
|
35 |
+
|
36 |
+
blob = firebase_bucket.blob(firebase_filename)
|
37 |
+
image_bytes = blob.download_as_bytes()
|
38 |
+
|
39 |
+
image = Image.open(BytesIO(image_bytes))
|
40 |
+
image = image.resize((self.config["img_width"], self.config["img_height"]))
|
41 |
+
image_array = np.array(image) / 255.0
|
42 |
+
image_array = np.expand_dims(image_array, axis=0)
|
43 |
+
|
44 |
+
prediction = self.model.predict(image_array)[0][0]
|
45 |
+
|
46 |
+
is_real = bool(prediction <= self.config["threshold"])
|
47 |
+
result = "Real" if is_real else "Spoof"
|
48 |
+
|
49 |
+
return {
|
50 |
+
"image_url": image_url,
|
51 |
+
"prediction_score": float(prediction),
|
52 |
+
"is_real": is_real,
|
53 |
+
"result": result
|
54 |
+
}
|
55 |
+
except Exception as e:
|
56 |
+
logging.error(f"Error processing image: {str(e)}")
|
57 |
+
raise HTTPException(status_code=500, detail=f"Error processing image: {str(e)}")
|
58 |
+
finally:
|
59 |
+
if firebase_filename:
|
60 |
+
await remove_temp_file(firebase_filename)
|
61 |
+
|
62 |
+
antispoof_service = AntispoofService()
|
app/services/audio_service.py
ADDED
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
import numpy as np
|
3 |
+
import librosa
|
4 |
+
import imagehash
|
5 |
+
from PIL import Image
|
6 |
+
import logging
|
7 |
+
import io
|
8 |
+
from pydub import AudioSegment
|
9 |
+
|
10 |
+
import ffmpeg
|
11 |
+
import numpy as np
|
12 |
+
logging.basicConfig(level=logging.DEBUG)
|
13 |
+
logger = logging.getLogger(__name__)
|
14 |
+
def extract_audio_features(video_bytes):
|
15 |
+
logger.info("Extracting audio features")
|
16 |
+
try:
|
17 |
+
# Attempt to extract audio using pydub
|
18 |
+
try:
|
19 |
+
logger.info("Attempting to extract audio using pydub")
|
20 |
+
audio = AudioSegment.from_file(io.BytesIO(video_bytes), format="mp4")
|
21 |
+
audio = audio.set_channels(1).set_frame_rate(44100)
|
22 |
+
samples = audio.get_array_of_samples()
|
23 |
+
audio_array = np.array(samples).astype(np.float32) / 32768.0
|
24 |
+
except Exception as pydub_error:
|
25 |
+
logger.warning(f"Pydub extraction failed: {str(pydub_error)}. Attempting ffmpeg extraction.")
|
26 |
+
|
27 |
+
# Fallback to ffmpeg if pydub fails
|
28 |
+
try:
|
29 |
+
out, _ = (
|
30 |
+
ffmpeg
|
31 |
+
.input('pipe:0')
|
32 |
+
.output('pipe:1', format='f32le', acodec='pcm_f32le', ac=1, ar='44100')
|
33 |
+
.run(input=video_bytes, capture_stdout=True, capture_stderr=True)
|
34 |
+
)
|
35 |
+
audio_array = np.frombuffer(out, np.float32)
|
36 |
+
except ffmpeg.Error as ffmpeg_error:
|
37 |
+
logger.error(f"FFmpeg extraction failed: {ffmpeg_error.stderr.decode()}")
|
38 |
+
raise
|
39 |
+
|
40 |
+
# Extract MFCC features
|
41 |
+
mfcc = librosa.feature.mfcc(y=audio_array, sr=44100, n_mfcc=13)
|
42 |
+
|
43 |
+
logger.info("Audio features extracted successfully")
|
44 |
+
return mfcc
|
45 |
+
|
46 |
+
except Exception as e:
|
47 |
+
logger.error(f"Error extracting audio features: {str(e)}", exc_info=True)
|
48 |
+
raise
|
49 |
+
|
50 |
+
def compute_audio_hash(features):
|
51 |
+
logging.info("Computing audio hash.")
|
52 |
+
if features is None:
|
53 |
+
return None
|
54 |
+
# Ensure the features are properly shaped for hashing
|
55 |
+
features_2d = features.reshape(features.shape[0], -1)
|
56 |
+
features_2d = (features_2d - np.min(features_2d)) / (np.max(features_2d) - np.min(features_2d))
|
57 |
+
features_2d = (features_2d * 255).astype(np.uint8)
|
58 |
+
return imagehash.phash(Image.fromarray(features_2d))
|
59 |
+
|
60 |
+
def compute_audio_hashes(video_bytes):
|
61 |
+
logger.info("Computing audio hashes")
|
62 |
+
try:
|
63 |
+
out, _ = (
|
64 |
+
ffmpeg
|
65 |
+
.input('pipe:0')
|
66 |
+
.output('pipe:1', format='f32le', acodec='pcm_f32le', ac=1, ar='44100')
|
67 |
+
.run(input=video_bytes, capture_stdout=True, capture_stderr=True)
|
68 |
+
)
|
69 |
+
|
70 |
+
audio_array = np.frombuffer(out, np.float32)
|
71 |
+
mfccs = librosa.feature.mfcc(y=audio_array, sr=44100, n_mfcc=13)
|
72 |
+
|
73 |
+
audio_hashes = []
|
74 |
+
for mfcc in mfccs.T:
|
75 |
+
audio_hash = imagehash.average_hash(Image.fromarray(mfcc.reshape(13, 1)))
|
76 |
+
audio_hashes.append(str(audio_hash))
|
77 |
+
|
78 |
+
logger.info("Finished computing audio hashes.")
|
79 |
+
return audio_hashes
|
80 |
+
except ffmpeg.Error as e:
|
81 |
+
logger.error(f"FFmpeg error in compute_audio_hashes: {e.stderr.decode()}")
|
82 |
+
raise
|
83 |
+
except Exception as e:
|
84 |
+
logger.error(f"Error in compute_audio_hashes: {str(e)}", exc_info=True)
|
85 |
+
raise
|
app/services/image_service.py
ADDED
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from app.utils.image_utils import verify_image_format, process_image, compare_images as compare_images_util
|
2 |
+
from fastapi import HTTPException
|
3 |
+
import logging
|
4 |
+
from app.utils.file_utils import download_file, remove_temp_file
|
5 |
+
|
6 |
+
async def verify_image(image_url: str):
|
7 |
+
firebase_filename = None
|
8 |
+
try:
|
9 |
+
firebase_filename = await download_file(image_url)
|
10 |
+
verify_image_format(firebase_filename)
|
11 |
+
|
12 |
+
image_hash = process_image(firebase_filename)
|
13 |
+
return {"image_hash": image_hash}
|
14 |
+
except Exception as e:
|
15 |
+
logging.error(f"Error verifying image: {str(e)}", exc_info=True)
|
16 |
+
raise HTTPException(status_code=500, detail=f"Error verifying image: {str(e)}")
|
17 |
+
finally:
|
18 |
+
if firebase_filename:
|
19 |
+
await remove_temp_file(firebase_filename)
|
20 |
+
|
21 |
+
async def compare_images(image_url1: str, image_url2: str):
|
22 |
+
firebase_filename1 = None
|
23 |
+
firebase_filename2 = None
|
24 |
+
try:
|
25 |
+
# Download the files from their URLs and store them in Firebase
|
26 |
+
firebase_filename1 = await download_file(image_url1)
|
27 |
+
firebase_filename2 = await download_file(image_url2)
|
28 |
+
|
29 |
+
# Verify the image format for both images
|
30 |
+
verify_image_format(firebase_filename1)
|
31 |
+
verify_image_format(firebase_filename2)
|
32 |
+
|
33 |
+
# Compare the images using the utility function
|
34 |
+
comparison_result = compare_images_util(firebase_filename1, firebase_filename2)
|
35 |
+
|
36 |
+
# Return the comparison result
|
37 |
+
return comparison_result
|
38 |
+
except Exception as e:
|
39 |
+
logging.error(f"Error comparing images: {str(e)}", exc_info=True)
|
40 |
+
raise HTTPException(status_code=500, detail=f"Error comparing images: {str(e)}")
|
41 |
+
finally:
|
42 |
+
# Remove temporary files from Firebase after processing
|
43 |
+
if firebase_filename1:
|
44 |
+
await remove_temp_file(firebase_filename1)
|
45 |
+
if firebase_filename2:
|
46 |
+
await remove_temp_file(firebase_filename2)
|
app/services/video_service.py
ADDED
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import cv2
|
2 |
+
import ffmpeg
|
3 |
+
import numpy as np
|
4 |
+
from scipy.fftpack import dct
|
5 |
+
import imagehash
|
6 |
+
from PIL import Image
|
7 |
+
import logging
|
8 |
+
import logging
|
9 |
+
from app.utils.hash_utils import compute_video_hash, compute_frame_hashes
|
10 |
+
from app.services.audio_service import extract_audio_features, compute_audio_hash, compute_audio_hashes
|
11 |
+
from app.utils.file_utils import download_file, remove_temp_file, get_file_stream
|
12 |
+
import io
|
13 |
+
|
14 |
+
import tempfile
|
15 |
+
import os
|
16 |
+
logging.basicConfig(level=logging.DEBUG)
|
17 |
+
logger = logging.getLogger(__name__)
|
18 |
+
def validate_video_bytes(video_bytes):
|
19 |
+
try:
|
20 |
+
# If video_bytes is already a BytesIO object, use it directly
|
21 |
+
# Otherwise, create a new BytesIO object from the bytes
|
22 |
+
if not isinstance(video_bytes, io.BytesIO):
|
23 |
+
video_bytes = io.BytesIO(video_bytes)
|
24 |
+
|
25 |
+
# Reset the BytesIO object to the beginning
|
26 |
+
video_bytes.seek(0)
|
27 |
+
|
28 |
+
# Create a temporary file to store the video data
|
29 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as temp_file:
|
30 |
+
temp_file.write(video_bytes.read())
|
31 |
+
temp_file_path = temp_file.name
|
32 |
+
|
33 |
+
# Use ffprobe to get video information
|
34 |
+
probe = ffmpeg.probe(temp_file_path)
|
35 |
+
|
36 |
+
# Clean up the temporary file
|
37 |
+
os.unlink(temp_file_path)
|
38 |
+
|
39 |
+
# Check for audio stream
|
40 |
+
audio_stream = next((stream for stream in probe['streams'] if stream['codec_type'] == 'audio'), None)
|
41 |
+
|
42 |
+
if audio_stream is None:
|
43 |
+
logger.warning("No audio stream found in the file")
|
44 |
+
return False
|
45 |
+
return True
|
46 |
+
except ffmpeg.Error as e:
|
47 |
+
logger.error(f"Error validating video bytes: {e.stderr.decode()}")
|
48 |
+
return False
|
49 |
+
except Exception as e:
|
50 |
+
logger.error(f"Unexpected error in validate_video_bytes: {str(e)}")
|
51 |
+
return False
|
52 |
+
|
53 |
+
async def extract_video_features(firebase_filename):
|
54 |
+
logging.info("Extracting video features")
|
55 |
+
video_stream = get_file_stream(firebase_filename)
|
56 |
+
video_bytes = video_stream.getvalue()
|
57 |
+
|
58 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as temp_file:
|
59 |
+
temp_file.write(video_bytes)
|
60 |
+
temp_file_path = temp_file.name
|
61 |
+
|
62 |
+
cap = cv2.VideoCapture(temp_file_path)
|
63 |
+
|
64 |
+
features = []
|
65 |
+
while True:
|
66 |
+
ret, frame = cap.read()
|
67 |
+
if not ret:
|
68 |
+
break
|
69 |
+
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
70 |
+
resized = cv2.resize(gray, (32, 32))
|
71 |
+
dct_frame = dct(dct(resized.T, norm='ortho').T, norm='ortho')
|
72 |
+
features.append(dct_frame[:8, :8].flatten())
|
73 |
+
|
74 |
+
cap.release()
|
75 |
+
os.unlink(temp_file_path)
|
76 |
+
|
77 |
+
logging.info("Finished extracting video features.")
|
78 |
+
return np.array(features), video_bytes
|
79 |
+
|
80 |
+
async def fingerprint_video(video_url):
|
81 |
+
logging.info(f"Fingerprinting video: {video_url}")
|
82 |
+
firebase_filename = None
|
83 |
+
try:
|
84 |
+
firebase_filename = await download_file(video_url)
|
85 |
+
video_stream = get_file_stream(firebase_filename)
|
86 |
+
video_bytes = video_stream.getvalue()
|
87 |
+
|
88 |
+
video_features, _ = await extract_video_features(firebase_filename)
|
89 |
+
|
90 |
+
if validate_video_bytes(io.BytesIO(video_bytes)):
|
91 |
+
audio_features = extract_audio_features(video_bytes)
|
92 |
+
audio_hashes = compute_audio_hashes(video_bytes)
|
93 |
+
collective_audio_hash = compute_audio_hash(audio_features)
|
94 |
+
else:
|
95 |
+
logging.warning("No audio stream found or invalid video. Skipping audio feature extraction.")
|
96 |
+
audio_hashes = []
|
97 |
+
collective_audio_hash = None
|
98 |
+
|
99 |
+
video_hash = compute_video_hash(video_features)
|
100 |
+
frame_hashes = compute_frame_hashes(firebase_filename)
|
101 |
+
|
102 |
+
logging.info("Finished fingerprinting video.")
|
103 |
+
|
104 |
+
return {
|
105 |
+
'frame_hashes': frame_hashes,
|
106 |
+
'audio_hashes': audio_hashes,
|
107 |
+
'robust_audio_hash': str(collective_audio_hash) if collective_audio_hash else None,
|
108 |
+
'robust_video_hash': str(video_hash),
|
109 |
+
}
|
110 |
+
finally:
|
111 |
+
if firebase_filename:
|
112 |
+
await remove_temp_file(firebase_filename)
|
113 |
+
|
114 |
+
async def compare_videos(video_url1, video_url2):
|
115 |
+
fp1 = await fingerprint_video(video_url1)
|
116 |
+
fp2 = await fingerprint_video(video_url2)
|
117 |
+
|
118 |
+
video_similarity = 1 - (imagehash.hex_to_hash(fp1['robust_video_hash']) - imagehash.hex_to_hash(fp2['robust_video_hash'])) / 64.0
|
119 |
+
audio_similarity = 1 - (imagehash.hex_to_hash(fp1['robust_audio_hash']) - imagehash.hex_to_hash(fp2['robust_audio_hash'])) / 64.0
|
120 |
+
|
121 |
+
overall_similarity = (video_similarity + audio_similarity) / 2
|
122 |
+
is_same_content = overall_similarity > 0.9 # You can adjust this threshold
|
123 |
+
|
124 |
+
logging.info(f"Comparison result - Video Similarity: {video_similarity}, Audio Similarity: {audio_similarity}, Overall Similarity: {overall_similarity}, Is Same Content: {is_same_content}")
|
125 |
+
|
126 |
+
return {
|
127 |
+
"video_similarity": video_similarity,
|
128 |
+
"audio_similarity": audio_similarity,
|
129 |
+
"overall_similarity": overall_similarity,
|
130 |
+
"is_same_content": is_same_content
|
131 |
+
}
|
app/utils/__init__.py
ADDED
File without changes
|
app/utils/file_utils.py
ADDED
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import aiohttp
|
2 |
+
import uuid
|
3 |
+
import logging
|
4 |
+
from urllib.parse import urlparse
|
5 |
+
from app.core.firebase_config import firebase_bucket
|
6 |
+
import io
|
7 |
+
|
8 |
+
async def download_file(url: str) -> str:
|
9 |
+
parsed_url = urlparse(url)
|
10 |
+
file_extension = parsed_url.path.split('.')[-1]
|
11 |
+
|
12 |
+
if not file_extension:
|
13 |
+
file_extension = 'tmp'
|
14 |
+
|
15 |
+
filename = f"{uuid.uuid4()}.{file_extension}"
|
16 |
+
|
17 |
+
try:
|
18 |
+
async with aiohttp.ClientSession() as session:
|
19 |
+
async with session.get(url) as response:
|
20 |
+
if response.status != 200:
|
21 |
+
raise Exception(f"Failed to download file: HTTP {response.status}")
|
22 |
+
|
23 |
+
content = await response.read()
|
24 |
+
blob = firebase_bucket.blob(filename)
|
25 |
+
blob.upload_from_string(content, content_type=response.headers.get('content-type'))
|
26 |
+
|
27 |
+
logging.info(f"File downloaded and saved to Firebase: {filename}")
|
28 |
+
return filename
|
29 |
+
except Exception as e:
|
30 |
+
logging.error(f"Error downloading file: {str(e)}")
|
31 |
+
raise
|
32 |
+
|
33 |
+
async def remove_temp_file(filename: str):
|
34 |
+
try:
|
35 |
+
blob = firebase_bucket.blob(filename)
|
36 |
+
blob.delete()
|
37 |
+
logging.info(f"Temporary file deleted from Firebase: {filename}")
|
38 |
+
except Exception as e:
|
39 |
+
logging.error(f"Error deleting temporary file from Firebase: {str(e)}")
|
40 |
+
|
41 |
+
def get_file_content(filename: str) -> bytes:
|
42 |
+
try:
|
43 |
+
blob = firebase_bucket.blob(filename)
|
44 |
+
return blob.download_as_bytes()
|
45 |
+
except Exception as e:
|
46 |
+
logging.error(f"Error getting file content from Firebase: {str(e)}")
|
47 |
+
raise
|
48 |
+
|
49 |
+
def get_file_stream(filename: str) -> io.BytesIO:
|
50 |
+
try:
|
51 |
+
content = get_file_content(filename)
|
52 |
+
return io.BytesIO(content)
|
53 |
+
except Exception as e:
|
54 |
+
logging.error(f"Error getting file stream from Firebase: {str(e)}")
|
55 |
+
raise
|
app/utils/hash_utils.py
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import cv2
|
2 |
+
import numpy as np
|
3 |
+
import imagehash
|
4 |
+
from PIL import Image
|
5 |
+
import logging
|
6 |
+
from app.utils.file_utils import get_file_stream
|
7 |
+
|
8 |
+
def compute_video_hash(features):
|
9 |
+
logging.info("Computing video hash.")
|
10 |
+
return imagehash.phash(Image.fromarray(np.mean(features, axis=0).reshape(8, 8)))
|
11 |
+
|
12 |
+
import tempfile
|
13 |
+
import os
|
14 |
+
|
15 |
+
def compute_frame_hashes(firebase_filename):
|
16 |
+
logging.info("Computing frame hashes")
|
17 |
+
video_stream = get_file_stream(firebase_filename)
|
18 |
+
video_bytes = video_stream.getvalue()
|
19 |
+
|
20 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as temp_file:
|
21 |
+
temp_file.write(video_bytes)
|
22 |
+
temp_file_path = temp_file.name
|
23 |
+
|
24 |
+
cap = cv2.VideoCapture(temp_file_path)
|
25 |
+
|
26 |
+
frame_hashes = []
|
27 |
+
while True:
|
28 |
+
ret, frame = cap.read()
|
29 |
+
if not ret:
|
30 |
+
break
|
31 |
+
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
32 |
+
img_hash = imagehash.average_hash(Image.fromarray(gray))
|
33 |
+
frame_hashes.append(str(img_hash))
|
34 |
+
|
35 |
+
cap.release()
|
36 |
+
os.unlink(temp_file_path)
|
37 |
+
|
38 |
+
logging.info("Finished computing frame hashes.")
|
39 |
+
return frame_hashes
|
app/utils/image_utils.py
ADDED
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import cv2
|
2 |
+
import numpy as np
|
3 |
+
from typing import Union
|
4 |
+
from PIL import Image
|
5 |
+
from io import BytesIO
|
6 |
+
import imghdr
|
7 |
+
from fastapi import HTTPException
|
8 |
+
from app.utils.file_utils import get_file_content
|
9 |
+
|
10 |
+
SUPPORTED_IMAGE_FORMATS = ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff', '.webp']
|
11 |
+
|
12 |
+
def verify_image_format(filename: str):
|
13 |
+
content = get_file_content(filename)
|
14 |
+
file_ext = '.' + (imghdr.what(BytesIO(content)) or '')
|
15 |
+
|
16 |
+
if file_ext not in SUPPORTED_IMAGE_FORMATS:
|
17 |
+
raise HTTPException(status_code=400, detail=f"Unsupported image format. Supported formats are: {', '.join(SUPPORTED_IMAGE_FORMATS)}")
|
18 |
+
|
19 |
+
def preprocess_image(image: Union[str, np.ndarray, Image.Image], hash_size: int = 32) -> np.ndarray:
|
20 |
+
if isinstance(image, str):
|
21 |
+
content = get_file_content(image)
|
22 |
+
img = Image.open(BytesIO(content))
|
23 |
+
img = strip_metadata(img)
|
24 |
+
image = np.array(img)
|
25 |
+
elif isinstance(image, Image.Image):
|
26 |
+
image = strip_metadata(image)
|
27 |
+
image = np.array(image)
|
28 |
+
|
29 |
+
if len(image.shape) == 3:
|
30 |
+
image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
|
31 |
+
|
32 |
+
image = cv2.resize(image, (hash_size, hash_size), interpolation=cv2.INTER_AREA)
|
33 |
+
image = cv2.normalize(image, None, 0, 255, cv2.NORM_MINMAX)
|
34 |
+
|
35 |
+
return image
|
36 |
+
|
37 |
+
def strip_metadata(img: Image.Image) -> Image.Image:
|
38 |
+
data = list(img.getdata())
|
39 |
+
img_without_exif = Image.new(img.mode, img.size)
|
40 |
+
img_without_exif.putdata(data)
|
41 |
+
return img_without_exif
|
42 |
+
|
43 |
+
def perceptual_image_hash(image: Union[str, np.ndarray, Image.Image], hash_size: int = 32) -> str:
|
44 |
+
processed_image = preprocess_image(image, hash_size)
|
45 |
+
dct = cv2.dct(np.float32(processed_image))
|
46 |
+
dct_low = dct[:8, :8]
|
47 |
+
median = np.median(dct_low[1:])
|
48 |
+
|
49 |
+
hash_value = ''
|
50 |
+
for i in range(8):
|
51 |
+
for j in range(8):
|
52 |
+
hash_value += '1' if dct_low[i, j] > median else '0'
|
53 |
+
|
54 |
+
return hash_value
|
55 |
+
|
56 |
+
def hamming_distance(hash1: str, hash2: str) -> int:
|
57 |
+
return sum(c1 != c2 for c1, c2 in zip(hash1, hash2))
|
58 |
+
|
59 |
+
def are_images_similar(hash1: str, hash2: str, threshold: int = 5) -> bool:
|
60 |
+
distance = hamming_distance(hash1, hash2)
|
61 |
+
return distance <= threshold
|
62 |
+
|
63 |
+
def process_image(filename: str):
|
64 |
+
try:
|
65 |
+
content = get_file_content(filename)
|
66 |
+
img = Image.open(BytesIO(content))
|
67 |
+
image_hash = perceptual_image_hash(img)
|
68 |
+
|
69 |
+
return image_hash
|
70 |
+
except Exception as e:
|
71 |
+
raise HTTPException(status_code=500, detail=f"Error processing image: {str(e)}")
|
72 |
+
|
73 |
+
def compare_images(filename1: str, filename2: str):
|
74 |
+
try:
|
75 |
+
content1 = get_file_content(filename1)
|
76 |
+
content2 = get_file_content(filename2)
|
77 |
+
img1 = Image.open(BytesIO(content1))
|
78 |
+
img2 = Image.open(BytesIO(content2))
|
79 |
+
hash1 = perceptual_image_hash(img1)
|
80 |
+
hash2 = perceptual_image_hash(img2)
|
81 |
+
|
82 |
+
are_similar = are_images_similar(hash1, hash2)
|
83 |
+
distance = hamming_distance(hash1, hash2)
|
84 |
+
|
85 |
+
return {
|
86 |
+
"image1_hash": hash1,
|
87 |
+
"image2_hash": hash2,
|
88 |
+
"are_similar": are_similar,
|
89 |
+
"hamming_distance": distance
|
90 |
+
}
|
91 |
+
except Exception as e:
|
92 |
+
raise HTTPException(status_code=500, detail=f"Error comparing images: {str(e)}")
|
models/mobilenetv2_spoof_model.h5
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:59a8eb471e37ffa98ca5984c18e8ab3c0fa38e1ccf407c6114b327b824a1839e
|
3 |
+
size 32110960
|
models/model_config.json
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
{"img_width": 224, "img_height": 224, "class_labels": {"real": 0, "spoof": 1}, "threshold": 0.5}
|
render.yaml
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
services:
|
2 |
+
- type: web
|
3 |
+
name: credify
|
4 |
+
env: python
|
5 |
+
buildCommand: |
|
6 |
+
apt-get update && apt-get install -y ffmpeg
|
7 |
+
chmod +x $(which ffmpeg)
|
8 |
+
ffmpeg -version
|
9 |
+
pip install --upgrade pip
|
10 |
+
pip install --use-pep517 -r requirements.txt
|
11 |
+
startCommand: uvicorn app.main:app --host 0.0.0.0 --port $PORT
|
requirements.txt
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
aiohttp==3.10.5
|
2 |
+
fastapi==0.109.2
|
3 |
+
# ffmpeg==1.4
|
4 |
+
ffmpeg-python==0.2.0
|
5 |
+
firebase_admin==6.5.0
|
6 |
+
imagehash==4.3.1
|
7 |
+
librosa==0.10.2.post1
|
8 |
+
numpy>=1.23.5,<2.0.0
|
9 |
+
Pillow==10.4.0
|
10 |
+
pydantic==2.6.1
|
11 |
+
pydub==0.25.1
|
12 |
+
python-dotenv==1.0.1
|
13 |
+
scipy==1.14.1
|
14 |
+
tensorflow==2.15.0
|
15 |
+
uvicorn==0.30.6
|
16 |
+
opencv-python==4.8.0.74
|
17 |
+
python-magic==0.4.27
|